Vue.js 源碼學習筆記5 組件思想的實現

組件化

組件化,就是把頁面拆分成多個組件 (component),每個組件依賴的 CSS、JavaScript、模板、圖片等資源放在一起開發和維護。組件是資源獨立的,組件在系統內部可復用,組件和組件之間可以嵌套。

import Vue from 'vue'
import App from './App.vue'

var app = new Vue({
  el: '#app',
  // 這里的 h 是 createElement 方法
  render: h => h(App)
})

初始化一個 vue 頁面也是 通過 render 函數去渲染的,不同的這次通過 createElement 傳的參數是一個組件而不是一個原生的標簽

組件 轉 vNode

createComponent

如果是一個普通的 html 標簽,像上一章的例子那樣是一個普通的 div,則會實例化一個普通 VNode 節點,否則通過 createComponent 方法創建一個組件 VNode

一個 App 對象,它本質上是一個 Component 類型,那么它會走到上述代碼的 else 邏輯,直接通過 createComponent 方法來創建 vnode。

針對組件渲染這個 case 主要就 3 個關鍵步驟:
構造子類構造函數,安裝組件鉤子函數和實例化 vnode。

構造子類構造函數

Vue.extend 的作用就是構造一個 Vue 的子類,它使用一種非常經典的原型繼承的方式把一個純對象轉換一個繼承于 Vue 的構造器 Sub 并返回,然后對 Sub 這個對象本身擴展了一些屬性,如擴展 options、添加全局 API 等;并且對配置中的 props 和 computed 做了初始化工作;最后對于這個 Sub 構造函數做了緩存,避免多次執行 Vue.extend 的時候對同一個子組件重復構造。(這里理解比較蛋疼)

安裝組件鉤子函數

Vue.js 使用的 Virtual DOM 參考的是開源庫 snabbdom,它的一個特點是在 VNode 的 patch 流程中對外暴露了各種時機的鉤子函數,方便我們做一些額外的事情,Vue.js 也是充分利用了這一點

實例化 VNode

最后一步非常簡單,通過 new VNode 實例化一個 vnode 并返回。需要注意的是和普通元素節點的 vnode 不同,組件的 vnode 是沒有 children 的

組件生成DOM的區別 patch

當我們通過 createComponent 創建了組件 VNode,接下來會走到 vm._update,執行 vm. _patch _ 去把 VNode 轉換成真正的 DOM 節點。在 組件中創建方式與創建普通節點不太一樣

之前先 將組件 VUE實例化 通過 new Vue(options) 實例化子組件

因為實際上 JavaScript 是一個單線程,Vue 整個初始化是一個深度遍歷的過程,在實例化子組件的過程中,它需要知道當前上下文的 Vue 實例是什么,并把它作為子組件的父 Vue 實例。

再創建一個父節點占位符,然后再遍歷所有子 VNode 遞歸調用 createElm,在遍歷的過程中,如果遇到子 VNode 是一個組件的 VNode,則重復本節開始的過程,這樣通過一個遞歸的方式就可以完整地構建了整個組件樹。

編寫一個組件實際上是編寫一個 JavaScript 對象,對象的描述就是各種配置,之前我們提到在 _init 的最初階段執行的就是 merge options 的邏輯

合并配置

到底啥是合并配置?

個人可以簡單理解為 將Vue 的內置組件目前有 <keep-alive>、<transition> 和 <transition-group> 組件 與 自己用戶編寫的組件合并 (把一些內置組件擴展到 Vue.options.components 上)

對于鉤子函數,他們的合并策略都是 mergeHook 函數。一旦 parent 和 child 都定義了相同的鉤子函數,那么它們會把 2 個鉤子函數合并成一個數組。
通過執行 mergeField 函數,把合并后的結果保存到 options 對象中,最終返回它。

vm.$options 的值差不多是如下這樣

vm.$options = {
  components: { },
  created: [
    function created() {
      console.log('parent created') 
    }
  ],
  directives: { },
  filters: { },
  _base: function Vue(options) {
    // ...
  },
  el: "#app",
  render: function (h) {  
    //...
  }
}
關于合并$options

Vue 初始化階段對于 options 的合并過程就是這樣,我們需要知道對于 options 的合并有 2 種方式,子組件初始化過程通過 initInternalComponent 方式要比外部初始化 Vue 通過 mergeOptions 的過程要快,合并完的結果保留在 vm.$options 中。

縱觀一些庫、框架的設計幾乎都是類似的,自身定義了一些默認配置,同時又可以在初始化階段傳入一些定義配置,然后去 merge 默認配置,來達到定制化不同需求的目的。

生命周期

每個 Vue 實例在被創建之前都要經過一系列的初始化過程。例如需要設置數據監聽、編譯模板、掛載實例到 DOM、在數據變化時更新 DOM 等。同時在這個過程中也會運行一些叫做生命周期鉤子的函數,給予用戶機會在一些特定的場景下添加他們自己的代碼。


VUE生命周期
callHook

源碼中最終執行生命周期的函數都是調用 callHook 方法,它的定義在 src/core/instance/lifecycle 中:

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 函數的邏輯很簡單,根據傳入的字符串 hook,去拿到 vm.$options[hook] 對應的回調函數數組,然后遍歷執行,執行的時候把 vm 作為函數執行的上下文

在上一節中,我們詳細地介紹了 Vue.js 合并 options 的過程,各個階段的生命周期的函數也被合并到 vm.$options 里,并且是一個數組。因此 callhook 函數的功能就是調用某個生命周期鉤子注冊的所有回調函數。

beforeCreate & created

beforeCreate 和 created 函數都是在實例化 Vue 的階段,在 _init 方法中執行的,它的定義在 src/core/instance/init.js 中:

Vue.prototype._init = function (options?: Object) {
  // ...
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')  //  beforeCreate
  initInjections(vm) // resolve injections before data/props
  initState(vm) // 初始化 props、data、methods、watch、computed 等屬性
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created') //  created
  // ...
}

beforeCreate 和 created 的鉤子調用是在 initState 的前后,initState 的作用是初始化 props、data、methods、watch、computed 等屬性
那么顯然 beforeCreate 的鉤子函數中就不能獲取到 props、data 中定義的值,也不能調用 methods 中定義的函數

在這倆個鉤子函數執行的時候,并沒有渲染 DOM,所以我們也不能夠訪問 DOM
如果是需要訪問 props、data 等數據的話,就需要使用 created 鉤子函數。
等下要說的 vue-router 和 vuex 的時候會發現它們都混合了 beforeCreatd 鉤子函數。

beforeMount & mounted

顧名思義,beforeMount 鉤子函數發生在 mount,也就是 DOM 掛載之前,它的調用時機是在 mountComponent 函數中,定義在 src/core/instance/lifecycle.js 中

在執行 vm._render() 函數渲染 VNode 之前,執行了 beforeMount 鉤子函數,在執行完 vm._update() 把 VNode patch 到真實 DOM 后,執行 mouted 鉤子。

這里對 mouted 鉤子函數執行有一個判斷邏輯,vm.$vnode 如果為 null,則表明這不是一次組件的初始化過程,而是我們通過外部 new Vue 初始化過程

beforeUpdate & updated

beforeUpdate 和 updated 的鉤子函數執行時機都應該是在數據更新的時候。 這個以后再章節中再提

beforeDestroy & destroyed

beforeDestroy 和 destroyed 鉤子函數的執行時機在組件銷毀的階段。
beforeDestroy 鉤子函數的執行時機是在 destroy 函數執行最開始的地方,接著執行了一系列的銷毀動作,包括從 parent 的 $children 中刪掉自身,刪除 watcher,當前渲染的 VNode 執行銷毀鉤子函數等,執行完畢后再調用 destroy 鉤子函數。

組件注冊

在 Vue.js 中,除了它內置的組件如 keep-alive、component、transition、transition-group 等,其它用戶自定義組件在使用前必須注冊。

全局注冊
Vue.component('my-component', {
  // 選項
  // ***要注冊一個全局組件,可以使用 Vue.component(tagName, options)***
})
局部注冊
import HelloWorld from './components/HelloWorld'

export default {
  components: {
    HelloWorld
  }
}

異步組件

在我們平時的開發工作中,為了減少首屏代碼體積,往往會把一些非首屏的組件設計成異步組件,按需加載。Vue 也原生支持了異步組件的能力。

Vue.component('async-example', function (resolve, reject) {
   // 這個特殊的 require 語法告訴 webpack
   // 自動將編譯后的代碼分割成不同的塊,
   // 這些塊將通過 Ajax 請求自動下載。
   require(['./my-async-component'], resolve)
})

示例中可以看到,Vue 注冊的組件不再是一個對象,而是一個工廠函數,函數有兩個參數 resolve 和 reject,函數內部用 setTimout 模擬了異步,實際使用可能是通過動態請求異步組件的 JS 地址,最終通過執行 resolve 方法,它的參數就是我們的異步組件對象。
高級異步組件 可以擴展為3種方式 暫時先不搞~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容