tweak error handling

This commit is contained in:
Evan You 2016-07-15 17:22:53 -04:00
parent 93fb4df3c4
commit 2732fec59e
6 changed files with 68 additions and 65 deletions

View File

@ -4,7 +4,7 @@ import config from '../config'
import VNode, { emptyVNode } from '../vdom/vnode'
import { normalizeChildren } from '../vdom/helpers'
import {
warn, bind, isObject, toObject,
warn, formatComponentName, bind, isObject, toObject,
nextTick, resolveAsset, _toString, toNumber
} from '../util/index'
@ -48,19 +48,38 @@ export function renderMixin (Vue: Class<Component>) {
_parentVnode
} = vm.$options
if (staticRenderFns && !this._staticTrees) {
this._staticTrees = []
if (staticRenderFns && !vm._staticTrees) {
vm._staticTrees = []
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
this.$vnode = _parentVnode
vm.$vnode = _parentVnode
// resolve slots. becaues slots are rendered in parent scope,
// we set the activeInstance to parent.
if (_renderChildren) {
resolveSlots(vm, _renderChildren)
}
// render self
let vnode = render.call(vm._renderProxy, vm.$createElement)
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
warn(`Error when rendering ${formatComponentName(vm)}:`)
}
/* istanbul ignore else */
if (config.errorHandler) {
config.errorHandler.call(null, e, vm)
} else {
if (config._isServer) {
throw e
} else {
setTimeout(() => { throw e }, 0)
}
}
// return previous vnode to prevent render error causing blank component
vnode = vm._vnode
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {

View File

@ -82,9 +82,9 @@ function runSchedulerQueue (queue: Array<Watcher>) {
if (circular[id] > config._maxUpdateCount) {
warn(
'You may have an infinite update loop ' + (
watcher === watcher.vm && watcher.vm._watcher
? `in a component render function.`
: `in watcher with expression "${watcher.expression}"`
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)

View File

@ -83,33 +83,7 @@ export default class Watcher {
*/
get () {
pushTarget(this)
let value: any
try {
value = this.getter.call(this.vm, this.vm)
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
if (this.user) {
warn(
'Error when evaluating watcher with getter: ' + this.expression,
this.vm
)
} else {
warn(
'Error during component render',
this.vm
)
}
}
/* istanbul ignore else */
if (config.errorHandler) {
config.errorHandler.call(null, e, this.vm)
} else {
console.error(e)
}
// return old value when evaluation fails so the current UI is preserved
// if the error was somehow handled by user
value = this.value
}
const value = this.getter.call(this.vm, this.vm)
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {

View File

@ -1,5 +1,4 @@
import config from '../config'
import { hyphenate } from 'shared/util'
let warn
let formatComponentName
@ -9,21 +8,28 @@ if (process.env.NODE_ENV !== 'production') {
warn = (msg, vm) => {
if (hasConsole && (!config.silent)) {
console.error('[Vue warn]: ' + msg + (vm ? formatComponentName(vm) : ''))
console.error(`[Vue warn]: ${msg} ` + (
vm ? formatLocation(formatComponentName(vm)) : ''
))
}
}
formatComponentName = vm => {
if (vm.$root === vm) {
return ' (found in root instance)'
return 'root instance'
}
const name = vm._isVue
? vm.$options.name || vm.$options._componentTag
: vm.name
return name
? ' (found in component: <' + hyphenate(name) + '>)'
: ' (found in anonymous component. Use the "name" option for better debugging messages)'
return name ? `component <${name}>` : `anonymous component`
}
const formatLocation = str => {
if (str === 'anonymous component') {
str += ` - use the "name" option for better debugging messages.)`
}
return `(found in ${str})`
}
}
export { warn }
export { warn, formatComponentName }

View File

@ -276,4 +276,31 @@ describe('Component', () => {
expect(vm.$el.className).toBe('test red')
}).then(done)
})
it('catch component render error and preserve previous vnode', done => {
const spy = jasmine.createSpy()
Vue.config.errorHandler = spy
const vm = new Vue({
data: {
a: {
b: 123
}
},
render (h) {
return h('div', [this.a.b])
}
}).$mount()
expect(vm.$el.textContent).toBe('123')
expect(spy).not.toHaveBeenCalled()
vm.a = null
waitForUpdate(() => {
expect('Error when rendering root instance').toHaveBeenWarned()
expect(spy).toHaveBeenCalled()
expect(vm.$el.textContent).toBe('123') // should preserve rendered DOM
vm.a = { b: 234 }
}).then(() => {
expect(vm.$el.textContent).toBe('234') // should be able to recover
Vue.config.errorHandler = null
}).then(done)
})
})

View File

@ -178,27 +178,4 @@ describe('Watcher', () => {
new Watcher(vm, 'd.e + c', spy)
expect('Failed watching path:').toHaveBeenWarned()
})
it('catch getter error', () => {
Vue.config.errorHandler = spy
const err = new Error()
const vm = new Vue({
render () { throw err }
}).$mount()
expect('Error during component render').toHaveBeenWarned()
expect(spy).toHaveBeenCalledWith(err, vm)
Vue.config.errorHandler = null
})
it('catch user watcher error', () => {
Vue.config.errorHandler = spy
new Watcher(vm, function () {
return this.a.b.c
}, () => {}, { user: true })
expect('Error when evaluating watcher').toHaveBeenWarned()
expect(spy).toHaveBeenCalled()
expect(spy.calls.argsFor(0)[0] instanceof TypeError).toBe(true)
expect(spy.calls.argsFor(0)[1]).toBe(vm)
Vue.config.errorHandler = null
})
})