vue2.x原始碼解析系列二: Vue元件初始化過程概要

言川發表於2019-02-26

這裡分析的是當前(2018/07/25)最新版 V2.5.16 的原始碼,如果你想一遍看一遍參閱原始碼,請務必記得切換到此版本,不然可能存在微小的差異。

vue2.x原始碼解析系列二: Vue元件初始化過程概要

大家都知道,我們的應用是一個由Vue元件構成的一棵樹,其中每一個節點都是一個 Vue 元件。我們的每一個Vue元件是如何被建立出來的,建立的過程經歷了哪些步驟呢?把這些都搞清楚,那麼我們對Vue的整個原理將會有很深入的理解。

從入口函式開始,有比較複雜的引用關係,為了方便大家理解,我畫了一張圖可以直觀地看出他們之間的關係:

vue2.x原始碼解析系列二: Vue元件初始化過程概要

建立Vue例項的兩步

我們建立一個Vue例項,只需要兩行程式碼:

而這兩步分別經歷了一個比較複雜的構建過程:

  1. 建立類:建立一個 Vue 建構函式,以及他的一系列原型方法和類方法
  2. 建立例項:建立一個 Vue 例項,初始化他的資料,事件,模板等

下面我們分別解析這兩個階段,其中每個階段 又分為好多個 步驟

第一階段:建立Vue類

第一階段是要建立一個Vue類,因為我們這裡用的是原型而不是ES6中的class宣告,所以拆成了三步來實現:

  1. 建立一個建構函式 Vue
  2. Vue.prototype 上建立一系列例項屬性方法,比如 this.$data
  3. Vue 上建立一些全域性方法,比如 Vue.use 可以註冊外掛

我們匯入 Vue 建構函式 import Vue from ‘vue’ 的時候(new Vue(options) 之前),會生成一個Vue的建構函式,這個建構函式本身很簡單,但是他上面會新增一系列的例項方法和一些全域性方法,讓我們跟著程式碼來依次看看如何一步步構造一個 Vue 類的,我們要明白每一步大致是做什麼的,但是這裡先不深究,因為我們會在接下來幾章具體講解每一步都做了什麼,這裡我們先有一個大致的概念即可。

我們看程式碼先從入口開始,這是我們在瀏覽器環境最常用的一個入口,也就是我們 import Vue 的時候直接匯入的,它很簡單,直接返回了 從 platforms/web/runtime/index/js 中得到的 Vue 建構函式,具體程式碼如下:

platforms/web/entry-runtime.js

可以看到,這裡不是 Vue 建構函式的定義地方,而是返回了從下面一步得到的Vue建構函式,但是做了一些平臺相關的操作,比如內建 directives 註冊等。這裡就會有人問了,為什麼不直接定義一個建構函式,而是這樣不停的傳遞呢?因為 vue 有不同的執行環境,而每一個環境又有帶不帶 compiler 等不同版本,所以環境的不同以及版本的不同都會導致 Vue 類會有一些差異,那麼這裡會通過不同的步驟來處理這些差異,而所有的環境版本都要用到的核心程式碼是相同的,因此這些相同的程式碼就統一到 core/中了。

完整程式碼和我加的註釋如下:

platforms/web/runtime/index.js

上面的程式碼終於把平臺和配置相關的邏輯都處理完了,我們可以進入到了 core 目錄,這裡是Vue元件的核心程式碼,我們首先進入 core/index檔案,發現 Vue 建構函式也不是在這裡定義的。不過這裡有一點值得注意的就是,這裡呼叫了一個 initGlobalAPI 函式,這個函式是新增一些全域性屬性方法到 Vue 上,也就是類方法,而不是例項方法。具體他是做什麼的我們後面再講

core/index.js

core/instance/index.js 這裡才是真正的建立了 Vue 建構函式的地方,雖然程式碼也很簡單,就是建立了一個建構函式,然後通過mixin把一堆例項方法新增上去。

core/instance/index.js 完整程式碼如下:

下面我們分成兩段來講解這些程式碼分別幹了什麼。

這裡才是真正的Vue建構函式,注意其實很簡單,忽略在開發模式下的警告外,只執行了一行程式碼 this._init(options)。可想而知,Vue初始化必定有很多工作要做,比如資料的響應化、事件的繫結等,在第二階段我們會詳細講解這個函式到底做了什麼。這裡我們暫且跳過它。

上面這五個函式其實都是在Vue.prototype上新增了一些屬性方法,讓我們先找一個看看具體的程式碼,比如initMixin 就是新增 _init 函式,沒錯正是我們建構函式中呼叫的那個 this._init(options) 哦,它裡面主要是呼叫其他的幾個初始化方法,因為比較簡單,我們直接看程式碼:

core/instance/init.js

另外的幾個同樣都是在 Vue.prototype 上新增了一些方法,這裡暫時先不一個個貼程式碼,總結一下如下:

  1. core/instance/state.js,主要是新增了 $data,$props,$watch,$set,$delete 幾個屬性和方法
  2. core/instance/events.js,主要是新增了 $on,$off,$once,$emit 三個方法
  3. core/instance/lifecycle.js,主要新增了 _update, $forceUpdate, $destroy 三個方法
  4. core/instance/renderMixin.js,主要新增了 $nextTick_render 兩個方法以及一大堆renderHelpers

還記得我們跳過的在core/index.js中 新增 globalAPI的程式碼嗎,前面的程式碼都是在 Vue.prototype 上新增例項屬性,讓我們回到 core/index 檔案,這一步需要在 Vue 上新增一些全域性屬性方法。前面講到過,是通過 initGlobalAPI 來新增的,那麼我們直接看看這個函式的樣子:

至此,我們就構建出了一個 Vue 類,這個類上的方法都已經新增完畢。這裡再次強調一遍,這個階段只是新增方法而不是執行他們,具體執行他們是要到第二階段的。總結一下,我們建立的Vue類都包含了哪些內容:

上述就是我們的 Vue 類的全部了,有一些特別細小的點暫時沒有列出來,如果你在後面看程式碼的時候,發現有哪個函式不知道在哪定義的,可以參考這裡。那麼讓我們進入第二個階段:建立例項階段

第二階段:建立 Vue 例項

我們通過 new Vue(options) 來建立一個例項,例項的建立,肯定是從建構函式開始的,然後會進行一系列的初始化操作,我們依次看一下建立過程都進行了什麼初始化操作:

core/instance/index.js, 建構函式本身只進行了一個操作,就是呼叫 this._init(options) 進行初始化,這個在前面也提到過,這裡就不貼程式碼了。

core/instance/init.js 中會進行真正的初始化操作,讓我們詳細看一下這個函式具體都做了些什麼。

先看看它的完整程式碼:

我們來一段一段看看上面的程式碼分別作了什麼。

這段程式碼首先生成了一個全域性唯一的id。然後如果是非生產環境並且開啟了 performance,那麼會呼叫 mark 進行performance標記,這段程式碼就是開發模式下收集效能資料的,因為和Vue本身的執行原理無關,我們先跳過。

上面這段程式碼,暫時先不用管_isComponent,暫時只需要知道我們自己開發的時候使用的元件,都不是 _isComponent,所以我們會進入到 else語句中。這裡主要是進行了 options的合併,最終生成了一個 $options 屬性。下一章我們會詳細講解 options 合併的時候都做了什麼,這裡我們只需要暫時知道,他是把建構函式上的options和我們建立元件時傳入的配置 options 進行了一個合併就可以了。正是由於合併了這個全域性的 options 所以我們在可以直接在元件中使用全域性的 directives

這段程式碼可能看起來比較奇怪,這個 renderProxy 是幹嘛的呢,其實就是定義了在 render 函式渲染模板的時候,訪問屬性的時候的一個代理,可以看到生產環境下就是自己。

開發環境下作了一個什麼操作呢?暫時不用關心,反正知道渲染模板的時候上下文就是 vm 也就是 this 就行了。如果有興趣可以看看非生產環境,作了一些友好的報錯提醒等。

這裡只需要記住,在生產環境下,模板渲染的上下文就是 vm就行了。

這一段程式碼承擔了元件初始化的大部分工作。我直接把每一步的作用寫在註釋裡面了。 把這幾個函式都弄懂,那麼我們也就差不多弄懂了Vue的整個工作原理,而我們接下來的幾篇文章,其實都是從這幾個函式中的某一個開始的。

開始mount,注意這裡如果是我們的options中指定了 el 才會在這裡進行 $mount,而一般情況下,我們是不設定 el 而是通過直接呼叫 $mount("#app") 來觸發的。比如一般我們都是這樣的:

以上就是Vue例項的初始化過程。因為在 create 階段和 $mount 階段都很複雜,所以後面會分幾個章節來分別詳細講解。下一篇,讓我們從最神祕的資料響應化說起。

相關文章