vue 源码详解(三): 渲染初始化 initRender 、生命周期的调用 callHook 、异常处理机制
1 渲染初始化做了什么
在 Vue
实例上初始化了一些渲染需要用的属性和方法:
- 将组件的插槽编译成虚拟节点 DOM 树, 以列表的形式挂载到
vm
实例,初始化作用域插槽为空对象; - 将模板的编译函数(把模板编译成虚拟 DOM 树)挂载到
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,
context: ?Component
): { [key: string]: Array }
*/
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)
}
}
2 生命周期的调用 callHook
完成渲染的初始化, 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]
}
3 异常处理机制
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
; - 如果调用成功, 且返回值不与 false 严格相等(源码中通过
===
判断的), 则继续调用全局的异常处理函数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;
keyCodes: { [key: string]: number | Array };
// 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;
};
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)