在 Vue
实例上初始化了一些渲染需要用的属性和方法:
vm
实例,初始化作用域插槽为空对象;vm
的 _c
和 $createElement
属性;$attrs
和 $listeners
定义成响应式的。$attrs
和 $listeners
在高阶组件中用的比较多, 可能普通的同学很少用到。后面我会单独写一篇文章来介绍$attrs
和 $listeners
的用法。
// node_modules\vue\src\core\instance\render.js export function initRender (vm: Component) { vm._vnode = null // the root of the child tree 子组件的虚拟 DOM 树的根节点 vm._staticTrees = null // v-once cached trees const options = vm.$options const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree 父组件在父组件虚拟 DOM 树中的占位节点 const renderContext = parentVnode && parentVnode.context /* resolveSlots ( children: ?Array<VNode>, context: ?Component ): { [key: string]: Array<VNode> } */ vm.$slots = resolveSlots(options._renderChildren, renderContext) vm.$scopedSlots = emptyObject // bind the createElement fn to this instance // so that we get proper render context inside it. // args order: tag, data, children, normalizationType, alwaysNormalize // internal version is used by render functions compiled from templates vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // normalization is always applied for the public version, used in // user-written render functions. vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) // $attrs & $listeners are exposed for easier HOC creation. // they need to be reactive so that HOCs using them are always updated const parentData = parentVnode && parentVnode.data /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => { !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm) }, true) defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => { !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm) }, true) } else { defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true) defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true) } }
完成渲染的初始化, vm
开始调用 beforeCreate
这个生命周期。
用户使用的 beforeCreate
、 created
等钩子在 Vue
中是以数组的形式保存的,可以看成是一个任务队列。 即每个生命周期钩子函数都是 beforeCreate : [fn1, fn2, fn3, ... , fnEnd]
这种结构, 当调用 callHook(vm, 'beforeCreate')
时, 以当前组件的 vm
为 this
上下文依次执行生命周期钩子函数中的每一个函数。 每个生命周期钩子都是一个任务队列的原因是, 举个例子, 比如我们的组件已经写了一个 beforeCreate
生命周期钩子, 但是可以通过 Vue.mixin
继续向当前实例增加 beforeCreate
钩子。
#7573 disable dep collection when invoking lifecycle hooks
翻译过来是, 当触发生命周期钩子时, 禁止依赖收集
。 通过 pushTarget
、 popTarget
两个函数完成。 pushTarget
将当前依赖项置空, 并向依赖列表推入一个空的依赖, 等到 beforeCreate
中任务队列运行完毕,再通过 popTarget
将刚才加入的空依赖删除。至于什么是依赖和收集依赖, 放在状态初始化的部分吧。
callHook(vm, 'beforeCreate')
调用后, const handlers = vm.$options[hook]
即读取到了当前 vm
实例上的任务队列,然后通过 for
循环依次传递给 invokeWithErrorHandling(handlers[i], vm, null, vm, info)
进行处理, 调用 invokeWithErrorHandling
的好处是如果发生异常, 则会统一报错处理。
export function callHook (vm: Component, hook: string) { // #7573 disable dep collection when invoking lifecycle hooks pushTarget() const handlers = vm.$options[hook] const info = `${hook} hook` if (handlers) { for (let i = 0, j = handlers.length; i < j; i++) { invokeWithErrorHandling(handlers[i], vm, null, vm, info) } } if (vm._hasHookEvent) { vm.$emit('hook:' + hook) } popTarget() }
// node_modules\vue\src\core\observer\dep.js // The current target watcher being evaluated. // This is globally unique because only one watcher // can be evaluated at a time. Dep.target = null const targetStack = [] export function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target } export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] }
Vue
有一套异常处理机制, 所有的异常都在这里处理。
Vue 中的异常处理机制有个特点, 就是一旦有一个组件报错,Vue 会收集当前组件到根组件上所有的异常处理函数, 并从子组件开始, 层层触发, 直至执行完成全局异常处理; 如果用户不想层层上报, 可以通过配置某个组件上的 errorCaptured
返回布尔类型的值 false
即可。下面是从组建中截取的一段代码,用以演示如何停止错误继续上报上层组件:
export default { data() { return { // ... 属性列表 } } errorCaptured(cur, err, vm, info) { console.log(cur, err, vm, info) return false // 返回布尔类型的值 `false` 即可终止异常继续上报, 并且不再触发全局的异常处理函数 }, }
在 Vue
的全局 api 中有个 Vue.config
在这里可以配置 Vue 的行为特性, 可以通过 Vue.config.errorHandler
配置异常处理函数, 也可以在调用 new Vue()
时通过 errorCaptured
传递, 还可以通过 Vue.mixin
将错误处理混入到当前组件。执行时先执行 vm.$options.errorCaptured
上的异常处理函数, 然后根据 errorCaptured
的返回值是否与布尔值 false
严格相等来决定是否执行 Vue.config.errorHandler
异常处理函数, 实际运用中这两个配置其中一个即可。 我们可以根据异常类型,确定是否将信息展示给用户、是否将异常提交给服务器等操作。下面是一个简单的示例:
Vue.config.errorHandler = (cur, err, vm, info)=> { console.log(cur, err, vm, info) alert(2) } new Vue({ errorCaptured(cur, err, vm, info) { console.log(cur, err, vm, info) alert(1) }, router, store, render: h => h(App) }).$mount('#app')
调用声明周期的钩子,是通过 callHook(vm, 'beforeCreate')
进行调用的, 而 callHook
最终都调用了 invokeWithErrorHandling
这个函数, 以 callHook(vm, 'beforeCreate')
为例, 在遍历执行 beforeCreate
中的任务队列时, 每个任务函数都会被传递到 invokeWithErrorHandling
这个函数中。
export function invokeWithErrorHandling ( handler: Function, // 生命周期中的任务函数 context: any, // 任务函数 `handlers[i]` 执行时的上下文 args: null | any[], // 任务函数 `handlers[i]`执行时的参数, 以数组的形式传入, 因为最终通过 apply 调用 vm: any, // 当前组件的实例对象 info: string // 抛给用户的异常信息的描述文本 ) { // 生命周期处理 }
以 invokeWithErrorHandling(handlers[i], vm, null, vm, info)
这个调用为例,第一个参数 handlers[i]
即任务函数; 第二个参数 vm 表示任务函数 handlers[i]
执行时的上下文, 也就是函数执行时 this
指向的对象,对于生命周期函数而言, this
全都指向当前组件; 第三个参数 null
表示任务函数 handlers[i]
执行时,没有参数; 第四个参数 vm 表示当前组件的实例; 第五个参数表示异常发生时抛出给用户的异常信息。
invokeWithErrorHandling 的核心处理是 res = args ? handler.apply(context, args) : handler.call(context)
,若调用成功, 则直接返回当前任务函数的返回值 res
; 否则调用 handleError(e, vm, info)
函数处理异常。
接下来继续看 handleError
的逻辑。 Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
翻译过来的意思是 在执行异常处理函数时, 不再追踪 deps 的变化,以避免发生无限次数渲染的情况
, 处理方法与触发生命周期函数时的处理方法一直, 也是通过 pushTarget, popTarget
这两个函数处理。
然后,从当前组件开始,逐级查找父组件,直至查找到根组件, 对于所有被查到的上层组件, 都会读取其 $options.errorCaptured
中配置的异常处理函数。
处理过程为 :
hooks[i].call(cur, err, vm, info)
,Vue.config
配置的 errorHandler
函数;
false
则异常处理终止, 不再调用全局的异常处理函数 globalHandleError
;===
判断的), 则继续调用全局的异常处理函数 globalHandleError
;globalHandleError
时发生异常, 则通过默认的处理函数 logError
进行处理, 通过 console.error
将异常信息输出到控制台。// node_modules\vue\src\core\util\error.js /* @flow */ import config from '../config' import { warn } from './debug' import { inBrowser, inWeex } from './env' import { isPromise } from 'shared/util' import { pushTarget, popTarget } from '../observer/dep' export function handleError (err: Error, vm: any, info: string) { // Deactivate deps tracking while processing error handler to avoid possible infinite rendering. // See: https://github.com/vuejs/vuex/issues/1505 pushTarget() try { if (vm) { let cur = vm while ((cur = cur.$parent)) { const hooks = cur.$options.errorCaptured if (hooks) { for (let i = 0; i < hooks.length; i++) { try { const capture = hooks[i].call(cur, err, vm, info) === false if (capture) return } catch (e) { globalHandleError(e, cur, 'errorCaptured hook') } } } } } globalHandleError(err, vm, info) } finally { popTarget() } } // invokeWithErrorHandling(handlers[i], vm, null, vm, info) export function invokeWithErrorHandling ( handler: Function, context: any, args: null | any[], vm: any, info: string ) { let res try { res = args ? handler.apply(context, args) : handler.call(context) if (res && !res._isVue && isPromise(res) && !res._handled) { res.catch(e => handleError(e, vm, info + ` (Promise/async)`)) // issue #9511 // avoid catch triggering multiple times when nested calls res._handled = true } } catch (e) { handleError(e, vm, info) } return res } function globalHandleError (err, vm, info) { if (config.errorHandler) { try { return config.errorHandler.call(null, err, vm, info) } catch (e) { // if the user intentionally throws the original error in the handler, // do not log it twice if (e !== err) { logError(e, null, 'config.errorHandler') } } } logError(err, vm, info) } function logError (err, vm, info) { if (process.env.NODE_ENV !== 'production') { warn(`Error in ${info}: "${err.toString()}"`, vm) } /* istanbul ignore else */ if ((inBrowser || inWeex) && typeof console !== 'undefined') { console.error(err) } else { throw err } }
Vue 支持的可配置选项:
// node_modules\vue\src\core\config.js /* @flow */ import { no, noop, identity } from 'shared/util' import { LIFECYCLE_HOOKS } from 'shared/constants' export type Config = { // user optionMergeStrategies: { [key: string]: Function }; silent: boolean; productionTip: boolean; performance: boolean; devtools: boolean; errorHandler: ?(err: Error, vm: Component, info: string) => void; warnHandler: ?(msg: string, vm: Component, trace: string) => void; ignoredElements: Array<string | RegExp>; keyCodes: { [key: string]: number | Array<number> }; // platform isReservedTag: (x?: string) => boolean; isReservedAttr: (x?: string) => boolean; parsePlatformTagName: (x: string) => string; isUnknownElement: (x?: string) => boolean; getTagNamespace: (x?: string) => string | void; mustUseProp: (tag: string, type: ?string, name: string) => boolean; // private async: boolean; // legacy _lifecycleHooks: Array<string>; }; export default ({ /** * Option merge strategies (used in core/util/options) */ // $flow-disable-line optionMergeStrategies: Object.create(null), /** * Whether to suppress warnings. */ silent: false, /** * Show production mode tip message on boot? */ productionTip: process.env.NODE_ENV !== 'production', /** * Whether to enable devtools */ devtools: process.env.NODE_ENV !== 'production', /** * Whether to record perf */ performance: false, /** * Error handler for watcher errors */ errorHandler: null, /** * Warn handler for watcher warns */ warnHandler: null, /** * Ignore certain custom elements */ ignoredElements: [], /** * Custom user key aliases for v-on */ // $flow-disable-line keyCodes: Object.create(null), /** * Check if a tag is reserved so that it cannot be registered as a * component. This is platform-dependent and may be overwritten. */ isReservedTag: no, /** * Check if an attribute is reserved so that it cannot be used as a component * prop. This is platform-dependent and may be overwritten. */ isReservedAttr: no, /** * Check if a tag is an unknown element. * Platform-dependent. */ isUnknownElement: no, /** * Get the namespace of an element */ getTagNamespace: noop, /** * Parse the real tag name for the specific platform. */ parsePlatformTagName: identity, /** * Check if an attribute must be bound using property, e.g. value * Platform-dependent. */ mustUseProp: no, /** * Perform updates asynchronously. Intended to be used by Vue Test Utils * This will significantly reduce performance if set to false. */ async: true, /** * Exposed for legacy reasons */ _lifecycleHooks: LIFECYCLE_HOOKS }: Config)