挖掘隱藏在原始碼中的Vue技巧!
前言
最近關於Vue
的技巧文章大熱,我自己也寫過一篇(vue開發中的"騷操作"),但這篇文章的技巧是能在Vue
的文件中找到蛛絲馬跡的,而有些文章說的技巧在Vue
文件中根本找不到蹤跡!這是為什麼呢?
當我開始閱讀原始碼的時候,我才發現,其實這些所謂的技巧就是對原始碼的理解而已。
下面我分享一下我的收穫。
隱藏在原始碼中的技巧
我們知道,在使用Vue
時,要使用new
關鍵字進行呼叫,這就說明Vue
是一個建構函式。所以源頭就是定義Vue
建構函式的地方!
在src/core/instance/index.js
中找到了這個建構函式
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(thisinstanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue)
在建構函式中,只做一件事——執行this._init(options)
。
而_init()
函式是在initMixin(Vue)
中定義的
export function initMixin (Vue: Class<Component>) { Vue.prototype._init= function (options?: Object) { // ... _init 方法的函式體,此處省略 } }
以此為主線,來看看在這過程中有什麼好玩的技巧。
解構賦值子元件data的引數
按照官方文件,我們一般是這樣寫子元件data
選項的:
props: ['parentData'], data () { return { childData: this.parentData } }
但你知道嗎,也是可以這麼寫:
data (vm) { return { childData: vm.parentData } } // 或者使用解構賦值data ({ parentData }) { return { childData: parentData } }
通過解構賦值的方式將props
裡的變數傳給data
函式中,也就是說data
函式的引數就是當前例項物件。
這是因為data
函式的執行是用call()
方法強制綁定了當前例項物件。這發生在data
合併的階段,接下來去看看,說不定還有一些別的收穫!
在_init()
函式中主要是執行一系列的初始化,其中options
選項的合併是初始化的基礎。
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
在Vue
例項上添加了$options
屬性,在那些初始化方法中,無一例外的都使用到了例項的$options
屬性,即vm.$options
。
其中合併data
就是在mergeOption
中進行的。
strats.data = function ( parentVal: any, childVal: any, vm?: Component ): ?Function { if (!vm) { if (childVal && typeof childVal !== 'function') { process.env.NODE_ENV !== 'production' && warn( 'The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm ) return parentVal } return mergeDataOrFn(parentVal, childVal) } return mergeDataOrFn(parentVal, childVal, vm) }
上面程式碼是data
選項的合併策略函式,首先通過判斷是否存在vm
,來判斷是否為父子元件,存在vm
則為父元件。不管怎麼,最後都是返回mergeDataOrFn
的執行結果。區別在於處理父元件時,透傳vm
。
接下來看看mergeDataOrFn
函式。
export function mergeDataOrFn ( parentVal: any, childVal: any, vm?: Component ): ?Function { if (!vm) { // in a Vue.extend merge, both should be functions if (!childVal) { return parentVal } if (!parentVal) { return childVal } // when parentVal & childVal are both present, // we need to return a function that returns the // merged result of both functions... no need to // check if parentVal is a function here because // it has to be a function to pass previous merges. return function mergedDataFn () { return mergeData( typeof childVal === 'function' ? childVal.call(this, this) : childVal, typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal ) } } 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) } else { return defaultData } } } }
函式整體是由if
判斷分支語句塊組成,對vm
進行判斷,也使得mergeDataOrFn
也能區分父子元件。
return function mergedDataFn () { return mergeData( typeof childVal === 'function' ? childVal.call(this, this) : childVal, typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal ) }
來看這一段,當父子元件的data
選項同時存在,那麼就返回mergedDataFn
函式。mergedDataFn
函式又返回mergeData
函式。
在mergeData函式中,執行父子元件的data
選項函式,注意這裡的childVal.call(this, this)
和parentVal.call(this, this)
,關鍵在於call(this, this)
,可以看到,第一個this
指定了data
函式的作用域,而第二個this
就是傳遞給data
函式的引數。這就是開頭能用解構賦值的原理。
接著往下看!
注意因為函式已經返回了(return)
,所以mergedDataFn
函式還沒有執行。
以上就是處理子元件的data
選項時所做的事,可以發現在處理子元件選項時返回的總是一個函式。
說完了處理子元件選項的情況,再看看處理非子元件選項的情況,也就是使用new
操作符建立例項時的情況。
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) } else { return defaultData } } }
如果走else
分支的話那麼就直接返回mergedInstanceDataFn
函式。其中父子元件data
選項函式的執行也是用了call(vm, vm)
方法,強制綁定當前例項物件。
const instanceData = typeof childVal === 'function' ? childVal.call(vm, vm) : childVal const defaultData = typeof parentVal === 'function' ? parentVal.call(vm, vm) : parentVal
注意此時的mergedInstanceDataFn
函式同樣還沒有執行。所以mergeDataFn
函式永遠返回一個函式。
為什麼這麼強調返回的是一個函式呢?也就是說strats.data
最終結果是一個函式?
這是因為,通過函式返回的資料物件,保證了每個元件例項都要有一個唯一的資料副本,避免了元件間資料互相影響。
這個mergeDataFn
就是後面的初始化階段處理執行的。mergeDataFn
返回是mergeData(childVal, parentVal)
的執行結果才是真正合並父子元件的data
選項。也就是到了初始化階段才是真正合並,這是因為props
和inject
這兩個選項的初始化是先於data
選項的,這就保證了能夠使用props
初始化data
中的資料。
這才能在data
選項中呼叫props
或者inject
的值!
生命週期鉤子可以寫成陣列形式
生命週期鉤子可以寫成陣列形式,不信你可以試試!
created: [ function () { console.log('first') }, function () { console.log('second') }, function () { console.log('third') } ]
這啥能這麼寫?來看看生命週期鉤子的合併處理!
mergeHook是用於合併生命週期鉤子。
/** * Hooks and props are merged as arrays. */ function mergeHook ( parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function> ): ?Array<Function> { return childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal } LIFECYCLE_HOOKS.forEach(hook => { strats[hook] = mergeHook })其實從註釋中也能發現
Hooks and props are merged as arrays
.
使用forEach
遍歷LIFECYCLE_HOOKS
常量,說明LIFECYCLE_HOOKS
是一個數組。LIFECYCLE_HOOKS
來自於shared/constants.js
檔案。
export const LIFECYCLE_HOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated', 'errorCaptured' ]
所以那段forEach
語句,它的作用就是在strats
策略物件上新增用來合併各個生命週期鉤子選項的函式。
return childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal
函式體由三組三目運算子組成,在經過mergeHook
函式處理之後,元件選項的生命週期鉤子函式被合併成一個數組。
在第一個三目運算子中,首先判斷是否有childVal
,即元件的選項是否寫了生命週期鉤子函式,如果沒有則直接返回了parentVal
,這裡有一個預設的假定,就是如果有parentVal
那麼一定是個陣列,如果沒有parentVal
那麼strats[hooks]
函式根本不會執行。以created
生命週期鉤子函式為例:
new Vue({ created: function () { console.log('created') } })
對於strats.created
策略函式來講,childVal
就是例子中的created
選項,它是一個函式。parentVal
應該是Vue.options.created
,但Vue.options.created
是不存在的,所以最終經過strats.created
函式的處理將返回一個數組:
options.created = [ function () { console.log('created') } ]
再看下面的例子:
const Parent = Vue.extend({ created: function () { console.log('parentVal') } }) const Child = new Parent({ created: function () { console.log('childVal') } })
其中Child
是使用new Parent
生成的,所以對於Child
來講,childVal
是:
created: function () { console.log('childVal') }
而parentVal
已經不是Vue.options.created
了,而是Parent.options.created
,那麼Parent.options.created
是什麼呢?它其實是通過Vue.extend
函式內部的mergeOptions
處理過的,所以它應該是這樣的:
Parent.options.created = [ created: function () { console.log('parentVal') } ]
經過mergeHook函式處理,關鍵在那句:parentVal.concat(childVal)
,將parentVal
和childVal
合併成一個數組。所以最終結果如下:
[ created: function () { console.log('parentVal') }, created: function () { console.log('childVal') } ]
另外注意第三個三目運算子:
: Array.isArray(childVal) ? childVal : [childVal]
它判斷了childVal
是不是陣列,這說明了生命週期鉤子是可以寫成陣列的。這就是開頭所說的原理!
生命週期鉤子的事件偵聽器
大家可能不知道什麼叫做「生命週期鉤子的事件偵聽器」?,其實Vue
元件是可以這麼寫的:
<child @hook:created="childCreated" @hook:mounted="childMounted" />
在初始化中,使用callhook(vm, 'created')
函式執行created
生命週期函式,接下來瞧一瞧callhook()
的實現方法:
export function callHook (vm: Component, hook: string) { // #7573 disable dep collection when invoking lifecycle hooks pushTarget() const handlers = vm.$options[hook] if (handlers) { for (let i = 0, j = handlers.length; i < j; i++) { try { handlers[i].call(vm) } catch (e) { handleError(e, vm, `${hook} hook`) } } } if (vm._hasHookEvent) { vm.$emit('hook:' + hook) } popTarget() }
callhook()
函式接收兩個引數:
- 例項物件;
- 要呼叫的生命週期鉤子的名稱;
首先快取生命週期函式:
const handlers = vm.$options[hook]
如果執行callHook(vm, created)
,那麼就相當於:
const handlers = vm.$options.created
剛剛介紹過,對於生命週期鉤子選項最終會被合併處理成一個數組,所以得到的handlers
就是一個生命週期鉤子的陣列。接著執行的是這段程式碼:
if (handlers) { for (let i = 0, j = handlers.length; i < j; i++) { try { handlers[i].call(vm) } catch (e) { handleError(e, vm, `${hook} hook`) } } }
最後注意到callHook
函式的最後有這樣一段程式碼:
if (vm._hasHookEvent) { vm.$emit('hook:' + hook) }
其中vm._hasHookEvent
是在initEvents
函式中定義的,它的作用是判斷是否存在「生命週期鉤子的事件偵聽器」,初始化值為false
代表沒有,當元件檢測到存在生命週期鉤子的事件偵聽器時,會將vm._hasHookEvent
設定為true
。
生命週期鉤子的事件偵聽器,就是開頭說的:
<child @hook:created="childCreated" @hook:mounted="childMounted" />
使用hook:
加生命週期鉤子名稱的方式來監聽元件相應的生命週期鉤子。
總結
1、子元件data選項函式是有引數的,而且是當前的例項物件;
2、生命週期鉤子是可以寫成陣列形式,按順序執行;
3、可以使用生命週期鉤子的事件偵聽器來註冊生命週期函式
「不過沒在官方文件中寫明的方法,不建議使用」。
作者: zhangwinwin
連結:挖掘隱藏在原始碼中的Vue技巧!
來源:github