0. 背景
目前在丁香醫生的業務中,我會負責一個基於Vue全家桶的WebApp專案。
一直有件不太弄得明白的事:在每個元件的template標籤裡,都會使用dataReady
來進行渲染控制。例如像下面這樣,請求完了以後再渲染頁面。
## 模板部分
<template>
<div class="wrap"
v-if="dataReady">
</div>
</template>
## Script部分
async created() {
await this.makeSomeRequest();
this.dataReady = true;
},
複製程式碼
但是實際上,我在元件的data選項裡並沒有定義dataReady
屬性。
於是,我查了查入口檔案main.js
中,有這麼句話
Vue.mixin({
data() {
return {
dataReady: false
};
}
// 以下省略
});
複製程式碼
為什麼一個在全域性定義的變數,在每個元件裡都可以用呢?Vue是怎麼做到的呢?
於是,在翻了一堆資料和原始碼之後,有點兒答案了。
1. 前置知識
由於部分前置知識解釋起來很複雜,因此我直接以結論的形式給出:
- Vue是個建構函式,通過
new Vue
創造出來的是根例項 - 所有的單檔案元件,都是通過Vue.extend擴充套件出來的子類。
- 每個在父元件的標籤中template標籤,或者render函式中渲染的元件,是對應子類的例項。
2. 先從Vue.mixin看起
原始碼長這樣:
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
複製程式碼
很簡單,把當前上下文物件的options和傳入的引數做一次擴充套件嘛。
所以做事的,其實是mergeOptions
這個函式,它把Vue類上的靜態屬性options擴充套件了。
那我們看看mergeOptions
,到底做了什麼。
3. Vue類上用mergeOptions進行選項合併
找到mergeOptions
原始碼,記住一下。
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// 中間好長一串程式碼,都跳過不看,暫時和data屬性沒關係。
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)
}
return options
}
複製程式碼
這個mergeOptions
函式,其實就只是在傳入的options
物件上,遍歷自身的屬性,來執行mergeField
函式,然後返回一個新的options。
那麼問題就變化成了:mergeField
到底做了什麼?我們看它的程式碼。
// 找到合併策略函式
const strat = strats[key] || defaultStrat
// 執行合併策略函式
options[key] = strat(parent[key], child[key], vm, key)
複製程式碼
現在回憶一下,
- parent是什麼?—— 在這個例子裡,是Vue.options
- child是什麼?對,就是使用mixin方法時傳入的引數物件。
- 那麼key是什麼? —— 是在parents或者child物件上的某個屬性的鍵。
好,可以確認的是,child物件上,一定包含一個key為data的屬性。
行咯,那我們找找看什麼是strats.data
。
strats.data = function (
// parentVal,在這個例子裡,是Vue自身的options選項上的data屬性,有可能不存在
parentVal: any,
// childVal,在這個例子裡,是mixin方法傳入的選項物件中的data屬性
childVal: any,
vm?: Component
): ?Function {
// 回想一下Vue.mixin的程式碼,會發現vm為空
if (!vm) {
if (childVal && typeof childVal !== 'function') {
// 這個錯誤眼熟嗎?想想如果你剛才.mixin的時候,傳入的data如果不是函式,是不是就報錯了?
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
}
// 這條語句的返回值,將會在mergeField函式中,作為options.data的值。
return mergeDataOrFn(parentVal, childVal)
}
// 在這個例子裡,下面這行不會執行,為什麼?自己想想。
return mergeDataOrFn(parentVal, childVal, vm)
}
複製程式碼
OK,那我們再來看看,mergeDataOrFn
,到底是什麼。
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// childVal是剛剛mixin方法的引數中的data屬性,一個函式
if (!childVal) {
return parentVal
}
// parentVal是Vue.options.data屬性,然鵝Vue屬性並沒有自帶的data屬性
if (!parentVal) {
return childVal
}
// 下邊也不用看了,到這裡就返回了。
} else {
// 這裡不用看先,反正你也沒有傳遞vm引數嘛
}
}
複製程式碼
所以,是不是最終就是這麼句話
Vue.options.data = function data(){
return {
dataReady: false
}
}
複製程式碼
4. 從Vue類 -> 子類
話說,剛剛這個data屬性,明明加在了Vue.options上,憑啥Vue的那些單檔案元件,也就是子類,它們的例項裡也能用啊?
這就要講到Vue.extend
函式了,它是用來擴充套件子類的,平時我們寫的一個個SFC單檔案元件,其實都是Vue類的子類。
Vue.extend = function (extendOptions: Object): Function {
const Super = this
// 你不用關心中間還有一些程式碼
const Sub = function VueComponent (options) {
this._init(options)
}
// 繼承
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 注意這裡也執行了options函式,做了選項合併工作。
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// 你不用關心中間還有一些程式碼
// 把子類返回出去了。
return Sub;
}
複製程式碼
- extendOptions是什麼?
其實就是我們在單檔案元件裡寫的東西,它可能長這樣
export default {
// 當然,也可能沒有data函式
data(){
return{
id: 0
}
},
methods: {
handleClick(){
}
}
}
複製程式碼
- Super.options是什麼?
在我們專案裡,是沒有出現Vue -> Parent -> Child
這樣的多重繼承關係的,所以可以認為Super.options
,就是前面說的Vue.options
!
記得嗎?在執行完了Vue.mixin之後,Vue.options
有data屬性噢。
5. Vue類 -> 子類時的mergeOptions
這時候再來看
Sub.options = mergeOptions(
Super.options,
extendOptions
)
複製程式碼
我們再次回到mergeOptions
函式。
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// 省略上面一些檢查和規範化
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)
}
return options
}
複製程式碼
就和剛才一樣,還是會返回一個options,並且給到Sub.options
。
其中options.data屬性,仍然會被strats.data
策略函式執行一遍,但這次流程未必一樣。
注意,parentVal
是Vue.options.data
,而childVal可能是一個data
函式,也可能為空。為什麼?去問前面的extendOptions
啊,它傳的引數啊。
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
// 省略
}
// 沒問題,還是執行這一句。
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
複製程式碼
我們可以看到,流程基本一致,還是執行return mergeDataOrFn(parentVal, childVal)
。
我們再看這個mergeDataOrFn
。
首先假定childVal為空。
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// 到這裡就返回了
if (!childVal) {
return parentVal
}
} else {
// 省略
}
}
複製程式碼
所以如果extendOptions
沒傳data屬性(一個函式),那麼他就會使用parentVal,也就是Vue.options.data
。
所以,可以簡單理解為
Sub.options.data = Vue.options.data = function data(){
return {
dataReady: false
}
}
複製程式碼
那要是extendOptions
傳了個data函式呢?我們可以在mergeDataOrFn這個函式裡繼續找
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
複製程式碼
返回的是個函式,考慮到這裡的childVal和parentVal都是函式,我們可以簡化一下程式碼
// 現在假設子類的data選項長這樣
function subData(){
return{
id: 0
}
}
function vueData(){
return {
dataReady: false
}
}
// Sub得到了什麼?
Sub.options.data = function data(){
return mergeData(
subData.call(this, this),
vueData.call(this, this)
)
}
複製程式碼
請想一下這裡的this是什麼,在結尾告訴你。
在Sub類進行一次例項化的時候,Sub.options.data
會進行執行。所以會得到這個形式的結果。
return mergeData({ id: 0 }, { dataReady: false })
複製程式碼
具體mergeData的原理也很簡單:遍歷key + 深度合併;而如果key同名的話,就不會執行覆蓋。具體的去看下mergeData
這個函式好了,這不是本文重點。
具體怎麼執行例項化,怎麼執行data
函式的,有興趣的可以自己去了解,簡單說下,和三個函式有關:
- Vue.prototype._init
- initState
- initData
7. 尾聲
現在你理解,為什麼每個元件裡,都會有一個dataReady: false
了嗎?
其實一句話概括起來,就是:Vue類上的data函式(我稱為parentDataFn)會與子類的data函式(我稱為childDataFn)合併,得到一個新函式,這個新函式會會在子類在例項化時執行,且同時執行parentDataFn和childDataFn,並返回合併後的data物件。
順便,剛才
Sub.options.data = function mergedDataFn(){
return mergeData(
subData.call(this, this),
vueData.call(this, this)
)
}
複製程式碼
這裡的this,是一個Sub類的例項。
8. 結語
說實在的,之前會自己在做完工作以後,寫一點文章,讓自己能夠更好地理解自己到底學到了什麼,比如:
但是都是很簡單的“技能記錄”或者“基礎探究”。
而這次,則是第一次嘗試理解像Vue原始碼這樣的複雜系統,很擔心很多地方會誤導人,所以特別感謝以下參考資料:
- 對MergeOption的解析: Vue技術內幕
- Vue-mixin合併的應用:在 Vue.js 中使用Mixin。
如果還有什麼說得不太對,還請多提些意見。
最後,丁香醫生前端團隊正在招人。
對招聘有意向或者疑問的話,可以在知乎上私信作者。
作者:丁香園 前端工程師 @Kevin Wong