目前在丁香醫生的業務中,我會負責一個基於Vue全家桶的WebApp項目。css
一直有件不太弄得明白的事:在每一個組件的template標籤裏,都會使用dataReady
來進行渲染控制。例如像下面這樣,請求完了之後再渲染頁面。html
## 模板部分
<template>
<div class="wrap"
v-if="dataReady">
</div>
</template>
## Script部分
async created() {
await this.makeSomeRequest();
this.dataReady = true;
},
複製代碼
可是實際上,我在組件的data選項裏並無定義dataReady
屬性。前端
因而,我查了查入口文件main.js
中,有這麼句話vue
Vue.mixin({
data() {
return {
dataReady: false
};
}
// 如下省略
});
複製代碼
爲何一個在全局定義的變量,在每一個組件裏均可以用呢?Vue是怎麼作到的呢?bash
因而,在翻了一堆資料和源碼以後,有點兒答案了。前端工程師
因爲部分前置知識解釋起來很複雜,所以我直接以結論的形式給出:async
new Vue
創造出來的是根實例源碼長這樣:函數
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
複製代碼
很簡單,把當前上下文對象的options和傳入的參數作一次擴展嘛。post
因此作事的,實際上是mergeOptions
這個函數,它把Vue類上的靜態屬性options擴展了。ui
那咱們看看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)
複製代碼
如今回憶一下,
好,能夠確認的是,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
}
}
複製代碼
話說,剛剛這個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;
}
複製代碼
其實就是咱們在單文件組件裏寫的東西,它可能長這樣
export default {
// 固然,也可能沒有data函數
data(){
return{
id: 0
}
},
methods: {
handleClick(){
}
}
}
複製代碼
在咱們項目裏,是沒有出現Vue -> Parent -> Child
這樣的多重繼承關係的,因此能夠認爲Super.options
,就是前面說的Vue.options
!
記得嗎?在執行完了Vue.mixin以後,Vue.options
有data屬性噢。
這時候再來看
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
函數的,有興趣的能夠本身去了解,簡單說下,和三個函數有關:
如今你理解,爲何每一個組件裏,都會有一個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類的實例。
說實在的,以前會本身在作完工做之後,寫一點文章,讓本身可以更好地理解本身到底學到了什麼,好比:
可是都是很簡單的「技能記錄」或者「基礎探究」。
而此次,則是第一次嘗試理解像Vue源碼這樣的複雜系統,很擔憂不少地方會誤導人,因此特別感謝如下參考資料:
若是還有什麼說得不太對,還請多提些意見。
最後,丁香醫生前端團隊正在招人。
對招聘有意向或者疑問的話,能夠在知乎上私信做者。
做者:丁香園 前端工程師 @Kevin Wong