vue 原始碼詳解(三): 渲染初始化 initRender 、生命週期的呼叫 callHook 、異常處理機制
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<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 生命週期的呼叫 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<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)