Vue原理解析(二):快速搞懂new Vue()時(shí)到底做了什么?(上)

上一篇:Vue原理解析(一):Vue到底是什么?

上一章節(jié)我們知道了在new Vue()時(shí),內(nèi)部會(huì)執(zhí)行一個(gè)this._init()方法,這個(gè)方法是在initMixin(Vue)內(nèi)定義的:

export function initMixin(Vue) {
  Vue.prototype._init = function(options) {
    ...
  }
}

當(dāng)執(zhí)行new Vue()執(zhí)行后,觸發(fā)的一系列初始化都在_init方法中啟動(dòng),它的實(shí)現(xiàn)如下:

let uid = 0

Vue.prototype._init = function(options) {

  const vm = this
  vm._uid = uid++  // 唯一標(biāo)識(shí)
  
  vm.$options = mergeOptions(  // 合并options
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
  ...
  initLifecycle(vm) // 開始一系列的初始化
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm)
  initState(vm)
  initProvide(vm)
  callHook(vm, 'created')
  ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

先需要交代下,每一個(gè)組件都是一個(gè)Vue構(gòu)造函數(shù)的子類,這個(gè)之后會(huì)說(shuō)明為何如此。從上往下我們一步步看,首先會(huì)定義_uid屬性,這是為每個(gè)組件每一次初始化時(shí)做的一個(gè)唯一的私有屬性標(biāo)識(shí),有時(shí)候會(huì)有些作用。

有一個(gè)使用它的小例子,找到一個(gè)組件所有的兄弟組件并剔除自己:

<div>
  ...
  <child-components />
  <child-components />  // 找到它的兄弟組件
  ... 其他組件
  <child-components />
</div>

首先要找的組件需要定義name屬性,當(dāng)然定義name屬性也是一個(gè)好的書寫習(xí)慣。首先通過(guò)自己的父組件($parent)的所有子組件($children)過(guò)濾出相同name集合的組件,這個(gè)時(shí)候他們就是同一個(gè)組件了,雖然它們name相同,但是_uid不同,最后在集合內(nèi)根據(jù)_uid剔除掉自己即可。

合并options配置

回到主線任務(wù),接著會(huì)合并options并在實(shí)例上掛載一個(gè)$options屬性。合并什么東西了?這里是分兩種情況的:

  1. 初始化new Vue

在執(zhí)行new Vue構(gòu)造函數(shù)時(shí),參數(shù)就是一個(gè)對(duì)象,也就是用戶的自定義配置;會(huì)將它和vue之前定義的原型方法,全局API屬性;還有全局的Vue.mixin內(nèi)的參數(shù),將這些都合并成為一個(gè)新的options,最后賦值給一個(gè)的新的屬性$options

  1. 子組件初始化

如果是子組件初始化,除了合并以上那些外,還會(huì)將父組件的參數(shù)進(jìn)行合并,如有父組件定義在子組件上的eventprops等等。

經(jīng)過(guò)合并之后就可以通過(guò)this.$options.data訪問(wèn)到用戶定義的data函數(shù),this.$options.name訪問(wèn)到用戶定義的組件名稱,這個(gè)合并后的屬性很重要,會(huì)被經(jīng)常使用到。

接下里會(huì)順序的執(zhí)行一堆初始化方法,首先是這三個(gè):

1. initLifecycle(vm)
2. initEvents(vm)
3. initRender(vm)

1. initLifecycle(vm): 主要作用是確認(rèn)組件的父子關(guān)系和初始化某些實(shí)例屬性。

export function initLifecycle(vm) {
  const options = vm.$options  // 之前合并的屬性
  
  let parent = options.parent;
  if (parent && !options.abstract) { //  找到第一個(gè)非抽象父組件
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
  
  vm.$parent = parent  // 找到后賦值
  vm.$root = parent ? parent.$root : vm  // 讓每一個(gè)子組件的$root屬性都是根組件
  
  vm.$children = []
  vm.$refs = {}
  
  vm._watcher = null
  ...
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

vue是組件式開發(fā)的,所以當(dāng)前實(shí)例可能會(huì)是其他組件的子組件的同時(shí)也可能是其他組件的父組件。

首先會(huì)找到當(dāng)前組件第一個(gè)非抽象類型的父組件,所以如果當(dāng)前組件有父級(jí)且當(dāng)前組件不是抽象組件就一直向上查找,直至找到后將找到的父級(jí)賦值給實(shí)例屬性vm.$parent,然后將當(dāng)前實(shí)例push到找到的父級(jí)的$children實(shí)例屬性內(nèi),從而建立組件的父子關(guān)系。接下來(lái)的一些_開頭是私有實(shí)例屬性我們記住是在這里定義的即可,具體意思也是以后用到的時(shí)候再做說(shuō)明。

2. initEvents(vm): 主要作用是將父組件在使用v-on@注冊(cè)的自定義事件添加到子組件的事件中心中。

首先看下這個(gè)方法定義的地方:

export function initEvents (vm) {
  vm._events = Object.create(null)  // 事件中心
  ...
  const listeners = vm.$options._parentListeners  // 經(jīng)過(guò)合并options得到的
  if (listeners) {
    updateComponentListeners(vm, listeners) 
  }
}

我們首先要知道在vue中事件分為兩種,他們的處理方式也各有不同:

2.1 原生事件

在執(zhí)行initEvents之前的模板編譯階段,會(huì)判斷遇到的是html標(biāo)簽還是組件名,如果是html標(biāo)簽會(huì)在轉(zhuǎn)為真實(shí)dom之后使用addEventListener注冊(cè)瀏覽器原生事件。綁定事件是掛載dom的最后階段,這里只是初始化階段,這里主要是處理自定義事件相關(guān),也就是另外一種,這里聲明下,大家不要理會(huì)錯(cuò)了執(zhí)行順序。

2.2 自定義事件

在經(jīng)歷過(guò)合并options階段后,子組件就可以從vm.$options._parentListeners讀取到父組件傳過(guò)來(lái)的自定義事件:

<child-components @select='handleSelect' />

傳過(guò)來(lái)的事件數(shù)據(jù)格式是{select:function(){}}這樣的,在initEvents方法內(nèi)定義vm._events用來(lái)存儲(chǔ)傳過(guò)來(lái)的事件集合。

內(nèi)部執(zhí)行的方法updateComponentListeners(vm, listeners)主要是執(zhí)行updateListeners方法。這個(gè)方法有兩個(gè)執(zhí)行時(shí)機(jī),首先是現(xiàn)在的初始化階段,還一個(gè)就是最后patch時(shí)的原生事件也會(huì)用到。它的作用是比較新舊事件的列表來(lái)確定事件的添加和移除以及事件修飾符的處理,現(xiàn)在主要看自定義事件的添加,它的作用是借助之前定義的$on$emit方法,完成父子組件事件的通信,(詳細(xì)的原理說(shuō)明會(huì)在之后的全局API章節(jié)統(tǒng)一說(shuō)明)。首先使用$onvm.events事件中心下創(chuàng)建一個(gè)自定義事件名的數(shù)組集合項(xiàng),數(shù)組內(nèi)的每一項(xiàng)都是對(duì)應(yīng)事件名的回調(diào)函數(shù),例如:

vm._events.select = [function handleSelect(){}, ...]  // 可以有多個(gè)

注冊(cè)完成之后,使用$emit方法執(zhí)行事件:

this.$emit('select')

首先會(huì)讀取到事件中心內(nèi)$emit方法第一個(gè)參數(shù)select的對(duì)象的數(shù)組集合,然后將數(shù)組內(nèi)每個(gè)回調(diào)函數(shù)順序執(zhí)行一遍即完成了$emit做的事情。

不知道大家有沒(méi)有注意到this.$emit這個(gè)方法是在當(dāng)前組件實(shí)例觸發(fā)的,所以事件的原理可能跟大部分人理解的不一樣,并不是父組件監(jiān)聽,子組件往父組件去派發(fā)事件。

而是子組件往自身的實(shí)例上派發(fā)事件,只是因?yàn)榛卣{(diào)函數(shù)是在父組件的作用域下定義的,所以執(zhí)行了父組件內(nèi)定義的方法,就造成了父子之間事件通信的假象。知道這個(gè)原理特性后,我們可以做一些更cool的事情,例如:

<div>
  <parent-component>  // $on添加事件
    <child-component-1>
      <child-component-2>
        <child-component-3 />  // $emit觸發(fā)事件
      </child-component-2>
    </child-components-1>
  </parent-component>
</div>

我們可不可以在parent-component內(nèi)使用$on添加事件到當(dāng)前實(shí)例的事件中心,而在child-components-3內(nèi)找到parent-component的組件實(shí)例并在它的事件中心觸發(fā)對(duì)應(yīng)的事件實(shí)現(xiàn)跨組件通信了,答案是可以了!這一原理發(fā)現(xiàn)再開發(fā)組件庫(kù)時(shí)會(huì)有一定幫助。

3. initRender(vm): 主要作用是掛載可以將render函數(shù)轉(zhuǎn)為vnode的方法。

export function initRender(vm) {
  vm._vnode = null
  ...
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)  //轉(zhuǎn)化編譯器的
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)  // 轉(zhuǎn)化手寫的
  ...
}

主要作用是掛載vm._cvm.$createElement兩個(gè)方法,它們只是最后一個(gè)參數(shù)不同,這兩個(gè)方法都可以將render函數(shù)轉(zhuǎn)為vnode,從命名大家應(yīng)該可以看出區(qū)別,vm._c轉(zhuǎn)換的是通過(guò)編譯器將template轉(zhuǎn)換而來(lái)的render函數(shù);而vm.$createElement轉(zhuǎn)換的是用戶自定義的render函數(shù),比如:

new Vue({
  data: {
    msg: 'hello Vue!'
  },
  render(h) { // 這里的 h 就是vm.$createElement
    return h('span', this.msg);  
  }
}).$mount('#app');

render函數(shù)的參數(shù)h就是vm.$createElement方法,將內(nèi)部定義的樹形結(jié)構(gòu)數(shù)據(jù)轉(zhuǎn)為Vnode的實(shí)例。

4. callHook(vm, 'beforeCreate')

終于我們要執(zhí)行實(shí)例的第一個(gè)生命周期鉤子beforeCreate,這里callHook的原理是怎樣的,我們之后的生命周期章節(jié)會(huì)說(shuō)明,現(xiàn)在這里只需要知道它會(huì)執(zhí)行用戶自定義的生命周期方法,如果有mixin混入的也一并執(zhí)行。

好吧,實(shí)例的第一個(gè)生命周期鉤子階段的初始化工作完成了,一句話來(lái)主要說(shuō)明下他們做了什么事情:

  • initLifecycle(vm):確認(rèn)組件(也是vue實(shí)例)的父子關(guān)系
  • initEvents(vm):將父組件的自定義事件傳遞給子組件
  • initRender(vm):提供將render函數(shù)轉(zhuǎn)為vnode的方法
  • beforeCreate:執(zhí)行組件的beforeCreate鉤子函數(shù)

最后還是以一道vue容易被問(wèn)道的面試題作為本章節(jié)的結(jié)束吧:

面試官微笑而又不失禮貌的問(wèn)道:

  • 請(qǐng)問(wèn)可以在beforeCreate鉤子內(nèi)通過(guò)this訪問(wèn)到data中定義的變量么,為什么以及請(qǐng)問(wèn)這個(gè)鉤子可以做什么?

懟回去:

  • 是不可以訪問(wèn)的,因?yàn)樵?code>vue初始化階段,這個(gè)時(shí)候data中的變量還沒(méi)有被掛載到this上,這個(gè)時(shí)候訪問(wèn)值會(huì)是undefinedbeforeCreate這個(gè)鉤子在平時(shí)業(yè)務(wù)開發(fā)中用的比較少,而像插件內(nèi)部的instanll方法通過(guò)Vue.use方法安裝時(shí)一般會(huì)選在beforeCreate這個(gè)鉤子內(nèi)執(zhí)行,vue-routervuex就是這么干的。

下一篇:Vue原理解析(三):快速搞懂new Vue()時(shí)到底做了什么?(下)

順手點(diǎn)個(gè)贊或關(guān)注唄,找起來(lái)也方便~

分享一個(gè)筆者自己寫的組件庫(kù),哪天可能會(huì)用的上了 ~ ↓

你可能會(huì)用的上的一個(gè)vue功能組件庫(kù),持續(xù)完善中...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,801評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,800評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,996評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評(píng)論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,233評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,702評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容