vue 源碼詳解(三): 渲染初始化 initRender 、生命周期的調(diào)用 callHook 、異常處理機(jī)制
1 渲染初始化做了什么
在 Vue 實(shí)例上初始化了一些渲染需要用的屬性和方法:
- 將組件的插槽編譯成虛擬節(jié)點(diǎn) DOM 樹, 以列表的形式掛載到
vm實(shí)例,初始化作用域插槽為空對象; - 將模板的編譯函數(shù)(把模板編譯成虛擬 DOM 樹)掛載到
vm的_c和$createElement屬性; - 最后把父組件傳遞過來的
$attrs和$listeners定義成響應(yīng)式的。
$attrs 和 $listeners 在高階組件中用的比較多, 可能普通的同學(xué)很少用到。后面我會單獨(dú)寫一篇文章來介紹$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 樹的根節(jié)點(diǎn)
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree 父組件在父組件虛擬 DOM 樹中的占位節(jié)點(diǎn)
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)
}
}
2 生命周期的調(diào)用 callHook
完成渲染的初始化, vm 開始調(diào)用 beforeCreate 這個(gè)生命周期。
用戶使用的 beforeCreate 、 created 等鉤子在 Vue 中是以數(shù)組的形式保存的,可以看成是一個(gè)任務(wù)隊(duì)列。 即每個(gè)生命周期鉤子函數(shù)都是 beforeCreate : [fn1, fn2, fn3, ... , fnEnd] 這種結(jié)構(gòu), 當(dāng)調(diào)用 callHook(vm, 'beforeCreate') 時(shí), 以當(dāng)前組件的 vm 為 this 上下文依次執(zhí)行生命周期鉤子函數(shù)中的每一個(gè)函數(shù)。 每個(gè)生命周期鉤子都是一個(gè)任務(wù)隊(duì)列的原因是, 舉個(gè)例子, 比如我們的組件已經(jīng)寫了一個(gè) beforeCreate 生命周期鉤子, 但是可以通過 Vue.mixin 繼續(xù)向當(dāng)前實(shí)例增加 beforeCreate 鉤子。
#7573 disable dep collection when invoking lifecycle hooks 翻譯過來是, 當(dāng)觸發(fā)生命周期鉤子時(shí), 禁止依賴收集。 通過 pushTarget 、 popTarget 兩個(gè)函數(shù)完成。 pushTarget 將當(dāng)前依賴項(xiàng)置空, 并向依賴列表推入一個(gè)空的依賴, 等到 beforeCreate 中任務(wù)隊(duì)列運(yùn)行完畢,再通過 popTarget 將剛才加入的空依賴刪除。至于什么是依賴和收集依賴, 放在狀態(tài)初始化的部分吧。
callHook(vm, 'beforeCreate') 調(diào)用后, const handlers = vm.$options[hook] 即讀取到了當(dāng)前 vm 實(shí)例上的任務(wù)隊(duì)列,然后通過 for 循環(huán)依次傳遞給 invokeWithErrorHandling(handlers[i], vm, null, vm, info) 進(jìn)行處理, 調(diào)用 invokeWithErrorHandling 的好處是如果發(fā)生異常, 則會統(tǒng)一報(bào)錯(cuò)處理。
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 異常處理機(jī)制
Vue 有一套異常處理機(jī)制, 所有的異常都在這里處理。
Vue 中的異常處理機(jī)制有個(gè)特點(diǎn), 就是一旦有一個(gè)組件報(bào)錯(cuò),Vue 會收集當(dāng)前組件到根組件上所有的異常處理函數(shù), 并從子組件開始, 層層觸發(fā), 直至執(zhí)行完成全局異常處理; 如果用戶不想層層上報(bào), 可以通過配置某個(gè)組件上的 errorCaptured 返回布爾類型的值 false 即可。下面是從組建中截取的一段代碼,用以演示如何停止錯(cuò)誤繼續(xù)上報(bào)上層組件:
export default {
data() {
return {
// ... 屬性列表
}
}
errorCaptured(cur, err, vm, info) {
console.log(cur, err, vm, info)
return false // 返回布爾類型的值 `false` 即可終止異常繼續(xù)上報(bào), 并且不再觸發(fā)全局的異常處理函數(shù)
},
}
在 Vue 的全局 api 中有個(gè) Vue.config 在這里可以配置 Vue 的行為特性, 可以通過 Vue.config.errorHandler 配置異常處理函數(shù), 也可以在調(diào)用 new Vue() 時(shí)通過 errorCaptured 傳遞, 還可以通過 Vue.mixin 將錯(cuò)誤處理混入到當(dāng)前組件。執(zhí)行時(shí)先執(zhí)行 vm.$options.errorCaptured 上的異常處理函數(shù), 然后根據(jù) errorCaptured 的返回值是否與布爾值 false嚴(yán)格相等來決定是否執(zhí)行 Vue.config.errorHandler 異常處理函數(shù), 實(shí)際運(yùn)用中這兩個(gè)配置其中一個(gè)即可。 我們可以根據(jù)異常類型,確定是否將信息展示給用戶、是否將異常提交給服務(wù)器等操作。下面是一個(gè)簡單的示例:
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')
調(diào)用聲明周期的鉤子,是通過 callHook(vm, 'beforeCreate') 進(jìn)行調(diào)用的, 而 callHook 最終都調(diào)用了 invokeWithErrorHandling 這個(gè)函數(shù), 以 callHook(vm, 'beforeCreate') 為例, 在遍歷執(zhí)行 beforeCreate 中的任務(wù)隊(duì)列時(shí), 每個(gè)任務(wù)函數(shù)都會被傳遞到 invokeWithErrorHandling 這個(gè)函數(shù)中。
export function invokeWithErrorHandling (
handler: Function, // 生命周期中的任務(wù)函數(shù)
context: any, // 任務(wù)函數(shù) `handlers[i]` 執(zhí)行時(shí)的上下文
args: null | any[], // 任務(wù)函數(shù) `handlers[i]`執(zhí)行時(shí)的參數(shù), 以數(shù)組的形式傳入, 因?yàn)樽罱K通過 apply 調(diào)用
vm: any, // 當(dāng)前組件的實(shí)例對象
info: string // 拋給用戶的異常信息的描述文本
) {
// 生命周期處理
}
以 invokeWithErrorHandling(handlers[i], vm, null, vm, info) 這個(gè)調(diào)用為例,第一個(gè)參數(shù) handlers[i] 即任務(wù)函數(shù); 第二個(gè)參數(shù) vm 表示任務(wù)函數(shù) handlers[i] 執(zhí)行時(shí)的上下文, 也就是函數(shù)執(zhí)行時(shí) this 指向的對象,對于生命周期函數(shù)而言, this 全都指向當(dāng)前組件; 第三個(gè)參數(shù) null 表示任務(wù)函數(shù) handlers[i] 執(zhí)行時(shí),沒有參數(shù); 第四個(gè)參數(shù) vm 表示當(dāng)前組件的實(shí)例; 第五個(gè)參數(shù)表示異常發(fā)生時(shí)拋出給用戶的異常信息。
invokeWithErrorHandling 的核心處理是 res = args ? handler.apply(context, args) : handler.call(context) ,若調(diào)用成功, 則直接返回當(dāng)前任務(wù)函數(shù)的返回值 res ; 否則調(diào)用 handleError(e, vm, info) 函數(shù)處理異常。
接下來繼續(xù)看 handleError 的邏輯。 Deactivate deps tracking while processing error handler to avoid possible infinite rendering. 翻譯過來的意思是 在執(zhí)行異常處理函數(shù)時(shí), 不再追蹤 deps 的變化,以避免發(fā)生無限次數(shù)渲染的情況, 處理方法與觸發(fā)生命周期函數(shù)時(shí)的處理方法一直, 也是通過 pushTarget, popTarget 這兩個(gè)函數(shù)處理。
然后,從當(dāng)前組件開始,逐級查找父組件,直至查找到根組件, 對于所有被查到的上層組件, 都會讀取其 $options.errorCaptured 中配置的異常處理函數(shù)。
處理過程為 :
-
hooks[i].call(cur, err, vm, info), - 如果在這一步又發(fā)生了異常則調(diào)用通過
Vue.config配置的errorHandler函數(shù);- 如果調(diào)用成功并且返回
false則異常處理終止, 不再調(diào)用全局的異常處理函數(shù)globalHandleError; - 如果調(diào)用成功, 且返回值不與 false 嚴(yán)格相等(源碼中通過
===判斷的), 則繼續(xù)調(diào)用全局的異常處理函數(shù)globalHandleError; - 如果調(diào)用
globalHandleError時(shí)發(fā)生異常, 則通過默認(rèn)的處理函數(shù)logError進(jìn)行處理, 通過console.error將異常信息輸出到控制臺。
- 如果調(diào)用成功并且返回
// 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 支持的可配置選項(xiàng):
// 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)