Vue 源碼淺析:初始化 init 過程


Vue 源碼淺析:初始化 init 過程

前言

Vue 源碼淺析,分三塊大內容:初始化、數據動態響應、模板渲染。

這系列算不上逐行解析,示例的代碼可能只佔源碼一小部分,但相信根據二八法則,搞清這些內容或許可以撐起 80% 對源碼的理解程度。

我藉此機會,在玩 Vue3.0 之前開始最後一段 Vue2 的學習收尾,同時把這些學習總結分享給各位。

離網上那些 Vue 深入淺析的文章還有很多差距,如有不對之處,請各位指正。

最後,因為頭條排版原因限制可能部分格式不太友好,大家可以點擊尾部的“點擊原文”查看。

從 Vue 構造函數開始

我們寫 Vue 代碼時,都是通過新建 Vue 實例開始:

<code>var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
});/<code>

那麼就先找到 Vue 的函數定義:

<code>// source-code\\vue\\src\\core\\instance\\index.js
function Vue (options) {
//...
this._init(options)
}
initMixin(Vue)
//.../<code>

這個對象的引用 this._init 就是之後通過 initMixin 方法中聲明好的 Vue.prototype._init 原型方法。

<code>// source-code\\vue\\src\\core\\instance\\init.js
export function initMixin (Vue: Class<component>) {
Vue.prototype._init = function (options?: Object) {
//...
}
}/<component>/<code>

那麼我們接下來的一切都是以此 _init 為起點展開。

處理 options

跳過一些目前不涉及的邏輯,我們看下對象引用 vm.$options 到底是怎麼取得的:

<code>Vue.prototype._init = function (options?: Object) {
const vm: Component = this
//...
vm.$options = mergeOptions(

resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}/<code>

內部通過調用 mergeOptions 方法得到最終的 vm.$options。

接下來細看 mergeOptions 方法:

<code>// source-code\\vue\\src\\core\\\\util\\options.js
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
//...
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}

const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)

}
/<code>

標準化部分屬性 options

涉及 vue 中的:props、inject、directives:

<code>normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)/<code>

為什麼需要標準化呢?

就是為了給我們提供多種編寫代碼的方式,最後按照規定的數據結構標準化。

下面分別貼出對應上述三者的相關代碼,相信很容易知道它們在做什麼:

<code>function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
if (Array.isArray(props)) {
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
name = camelize(val)
res[name] = { type: null }
}
}
} else if (isPlainObject(props)) {
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}

}
}/<code>

比如,props: ['name', 'nick-name'] 會被轉成如下形式:

Vue 源碼淺析:初始化 init 過程


同樣,inject 和 directives 也會對用戶的簡寫方式做標準化處理,這裡不做過多描述。

比如:為 inject 中的字段屬性添加 from 字段;為 directives 中的函數定義,初始化 bind 和 update 方法。

遞歸合併 mergeOptions

會根據當前實例的 extends、mixins 和 parent 中的屬性做合併操作:

<code>if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}/<code>

當然此處的 mergeOptions 還是遞歸調用本方法。所以此方法不是重點,核心在後面的方法:mergeFields


合併字段 mergeField

邏輯非常明顯,遍歷 parent 上的屬性 key,然後根據某種策略,將該屬性 key 掛到 options 對象上;之後,遍歷 child (本 Vue 對象)屬性 key,只要不是 parent 的屬性,也一併加到 options 上:

<code>const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}/<code>


策略 strat

上面提到了某種策略,其實就是特定寫了幾種父子合併取值優先級的判斷。

顯示最基本的 defaultStrat 默認策略:

<code>const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}/<code>

child 屬性不存在,則直接使用 parent 屬性。

剩下就是根據特殊屬性,來定義的策略:

  • strats.el , strats.propsData //defaultStrat
  • strats.data //mergeDataOrFn
  • strats[hook] //concat
  • strats[ASSET_TYPES+'s'] (components,directives,filters) //extend
  • strats.watch //concat
  • strats.props,strats.methods,strats.inject,strats.computed //extend
  • strats.provide //mergeDataOrFn

涉及很多我們常用的 vue 屬性,但拋去一些特殊的策略,某些策略還是有共性的,比如都調用了:mergeDataOrFn。

能看到 mergeDataOrFn 中核心代碼,主要根據 call 來執行對應的屬性值 function:

<code>export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
//...

} else {
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData) //(child,parent)
} else {
return defaultData
}
}
}
}/<code>

最後將結果,扔給 mergeData 方法:

<code>function mergeData (to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal

const keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from)

for (let i = 0; i < keys.length; i++) {
key = keys[i]
// in case the object is already observed...
if (key === '__ob__') continue
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) { //特別說明 hasOwn 是根據 hasOwnProperty 做的;忽略 prototype 屬性
set(to, key, fromVal)
} else if (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal)
}
}
return to
}/<code>

如果 child 中沒有 key 屬性,則將 parent 屬性賦值給它。

當如果 child or parent 是一個對象時,則會繼續遞歸 mergeData ,直至全部處理完。

特別說明 hasOwn 方法是根據 hasOwnProperty 做的,會忽略 prototype 屬性,所以在 set 方法中會有特別的處理:

<code>export function set (target: Array | Object, key: any, val: any): any {

if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__

if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
/<code>

這是生命週期對應的策略,會遍歷所有的生命週期方法,並把父子的週期方法做 concat 操作:

<code>function mergeHook (
parentVal: ?Array<function>,
childVal: ?Function | ?Array<function>
): ?Array<function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)

? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}/<function>/<function>/<function>/<code>

對於 ASSET_TYPES 類型以及 props、methods、inject、computed 策略,則會做 extend 繼承操作。

<code>export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}/<code>

最後 watch 會稍微複雜寫,直接看代碼:

<code>strats.watch = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
//...
const ret = {}
extend(ret, parentVal)
for (const key in childVal) {
let parent = ret[key]
const child = childVal[key]
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
ret[key] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child]
}
return ret
}/<code>

同時保留 parent、child 的 watch 屬性,畢竟他們都要工作。通過 concat 和生命週期處理方式一樣,都保存起來。

proxy 代理

目前我們這裡不是 Vue 3.0 ,還未涉及新增加的 proxy 新特性,但在目前的版本中,已經有相關 proxy 的功能,不過對於非 production 環境沒做強制要求,在開發環境中也只是做些 warn 之類的功能。

<code>if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}/<code>

阮一峰老師的 es6 文章已經對 proxy 做了很細緻的說明,所以這裡不再對 has、get 之類的功能做補充,只是貼出相關代碼:

<code>const hasHandler = {
has (target, key) {
const has = key in target
const isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
if (!has && !isAllowed) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return has || !isAllowed
}
}

const getHandler = {
get (target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return target[key]
}
}

initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options

const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}/<code>

注意,如果瀏覽器不支持 proxy 特性,最後將執行到:vm._renderProxy = vm


一些初始化工作

接下來對生命週期、事件、渲染函數做些初始化工作,不是太重要,這裡簡單示意下:

<code>initLifecycle(vm)
initEvents(vm)
initRender(vm)/<code>

調用生命週期方法 callHook

之後,我們將見到第一次調用生命週期 beforeCreate 方法;當然初始化數據響應狀態的流程後,還會調用 created 方法。

<code>//...
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
//.../<code>

我們看下 callHook 怎麼工作:

<code>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()
}/<code>
<code>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)
//...
} catch (e) {
handleError(e, vm, info)
}
return res
}/<code>

能看到 call 對應的生命週期名字後,就會通過 invokeWithErrorHandling 方法內的來執行對應的生命週期方法,並且通過 try/catch 來捕獲出現的錯誤。

不過重要的是,還是要知道不同生命週期方法在整個 Vue 運行過程中的切入點(這裡先貼出此處兩個方法):

Vue 源碼淺析:初始化 init 過程


解析依賴和注入

正如官網所述:

provide 和 inject 主要在開發高階插件/組件庫時使用。並不推薦用於普通應用程序代碼中。

可能我們平時的開發代碼很少用到,現在看下初始化 init 中,他們是如何被初始化的。


雖然 inject 先於 provide 初始化,但必須現有 provide 這個蛋(父類提供),看下做了什麼:

<code>export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}/<code>

定義了 vm._provided 屬性,它將在後面交給對應注入的 inject 屬性 key 運作。


注入 injection

先看下入口方法 initInjections

<code>export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
//...
}/<code>
<code>export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)

for (let i = 0; i < keys.length; i++) {
const key = keys[i]

// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
}/<code>

遍歷 inject 對象上的內容(在 options 合併時,已經做了參數的標準化。比如,具備了 from 參數),把對象上的屬性名 key,在 vm._provided 對象上尋找,父類是否有提供過依賴。

如果找到,變將賦值給當前 inject 屬性 key 對應的 value,當然沒有找到,則會執行 inject 定義的 default:

<code>result[key] = source._provided[provideKey]/<code>

不過還沒完,因為我們知道 inject 注入的依賴,可以在 vue 中不同的地方使用(比如,官網示例提到的生命週期方法,和 data 屬性),並且賦予了數據響應能力,就是執行了如下方法(具體分析後續章節展開):

<code>export function initInjections (vm: Component) {
//...
defineReactive(vm, key, result[key])
//...
}/<code>

初始化狀態 state

備註下,initState 方法先於 initProvide,這裡文章排版,放在此處說明:

<code>initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props/<code>

initState 方法內涉及了 vue 中,我們常用的屬性:prop、methods、data、computed、watch,這些都是具備數據動態響應的能力,所以解釋起來會比較複雜,下篇繼續:

<code>// source-code\\vue\\src\\core\\instance\\state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}/<code>


分享到:


相關文章: