Evan You пре 9 година
родитељ
комит
2732fec59e

+ 24 - 5
src/core/instance/render.js

@@ -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)) {

+ 3 - 3
src/core/observer/scheduler.js

@@ -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
         )

+ 1 - 27
src/core/observer/watcher.js

@@ -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) {

+ 13 - 7
src/core/util/debug.js

@@ -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 }

+ 27 - 0
test/unit/features/component/component.spec.js

@@ -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)
+  })
 })

+ 0 - 23
test/unit/modules/observer/watcher.spec.js

@@ -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
-  })
 })