1. 程式人生 > >Vue.js 原始碼學習筆記

Vue.js 原始碼學習筆記

最近饒有興致的又把最新版 Vue.js 的原始碼學習了一下,覺得真心不錯,個人覺得 Vue.js 的程式碼非常之優雅而且精闢,作者本身可能無 (bu) 意 (xie) 提及這些。那麼,就讓我來吧:)

程式結構梳理

Vue 程式結構

Vue.js 是一個非常典型的 MVVM 的程式結構,整個程式從最上層大概分為

  1. 全域性設計:包括全域性介面、預設選項等
  2. vm 例項設計:包括介面設計 (vm 原型)、例項初始化過程設計 (vm 建構函式)

這裡面大部分內容可以直接跟 Vue.js 的官方 API 參考文件對應起來,但文件裡面沒有且值得一提的是建構函式的設計,下面是我摘出的建構函式最核心的工作內容。

Vue 例項初始化

整個例項初始化的過程中,重中之重就是把資料 (Model) 和檢視 (View) 建立起關聯關係。Vue.js 和諸多 MVVM 的思路是類似的,主要做了三件事:

  1. 通過 observer 對 data 進行了監聽,並且提供訂閱某個資料項的變化的能力
  2. 把 template 解析成一段 document fragment,然後解析其中的 directive,得到每一個 directive 所依賴的資料項及其更新方法。比如 v-text="message" 被解析之後 (這裡僅作示意,實際程式邏輯會更嚴謹而複雜):

    1. 所依賴的資料項 this.$data.message,以及
    2. 相應的檢視更新方法 node.textContent = this.$data.message
  3. 通過 watcher 把上述兩部分結合起來,即把 directive 中的資料依賴訂閱在對應資料的 observer 上,這樣當資料變化的時候,就會觸發 observer,進而觸發相關依賴對應的檢視更新方法,最後達到模板原本的關聯效果。

所以整個 vm 的核心,就是如何實現 observer, directive (parser), watcher 這三樣東西

檔案結構梳理

Vue.js 原始碼都存放在專案的 src 目錄中,我們主要關注一下這個目錄 (事實上 test/unit/specs 目錄也值得一看,它是對應著每個原始檔的測試用例)。

src 目錄下有多個並列的資料夾,每個資料夾都是一部分獨立而完整的程式設計。不過在我看來,這些目錄之前也是有更立體的關係的:

Vue 檔案結構

  • 首先是 api/* 目錄,這幾乎是最“上層”的介面封裝,實際的實現都埋在了其它資料夾裡
  • 然後是 instance/init.js,如果大家希望自頂向下瞭解所有 Vue.js 的工作原理的話,建議從這個檔案開始看起

    • instance/scope.js:資料初始化,相關的子程式 (目錄) 有 observer/*watcher.jsbatcher.js,而 observer/dep.js 又是資料觀察和檢視依賴相關聯的關鍵
    • instance/compile.js:檢視初始化,相關的子程式 (目錄) 有 compiler/*directive.jsparsers/*
  • 其它核心要素:directives/*element-directives/*filters/*transition/*
  • 當然還有 util/* 目錄,工具方法集合,其實還有一個類似的 cache.js
  • 最後是 config.js 預設配置項

篇幅有限,如果大家有意“通讀” Vue.js 的話,個人建議順著上面的整體介紹來閱讀賞析。

接下來是一些自己覺得值得一提的程式碼細節

一些不容錯過的程式碼/程式細節

this._eventsCount 是什麼?

一開始看 instance/init.js 的時候,我立刻注意到一個細節,就是 this._eventsCount = {} 這句,後面還有註釋

eventsCount1

for $broadcast optimization

非常好奇,然後帶著疑問繼續看了下去,直到看到 api/events.js 中 $broadcast 方法的實現,才知道這是為了避免不必要的深度遍歷:在有廣播事件到來時,如果當前 vm 的 _eventsCount 為 0,則不必向其子 vm 繼續傳播該事件。而且這個檔案稍後也有 _eventsCount 計數的實現方式。

eventsCount2

eventsCount3

這是一種很巧妙同時也可以在很多地方運用的效能優化方法。

資料更新的 diff 機制

前陣子有很多關於檢視更新效率的討論,我猜主要是因為 virtual dom 這個概念的提出而導致的吧。這次我詳細看了一下 Vue.js 的相關實現原理。

實際上,檢視更新效率的焦點問題主要在於大列表的更新和深層資料更新這兩方面,而被熱烈討論的主要是前者 (後者是因為需求小還是沒爭議我就不得而知了)。所以這裡著重介紹一下 directives/repeat.js 裡對於列表更新的相關程式碼。

diff1

首先 diff(data, oldVms) 這個函式的註釋對整個比對更新機制做了個簡要的闡述,大概意思是先比較新舊兩個列表的 vm 的資料的狀態,然後差量更新 DOM。

diff2

第一步:遍歷新列表裡的每一項,如果該項的 vm 之前就存在,則打一個 _reused 的標 (這個欄位我一開始看 init.js 的時候也是困惑的…… 看到這裡才明白意思),如果不存在對應的 vm,則建立一個新的。

diff3

第二步:遍歷舊列表裡的每一項,如果 _reused 的標沒有被打上,則說明新列表裡已經沒有它了,就地銷燬該 vm。

diff4

第三步:整理新的 vm 在視圖裡的順序,同時還原之前打上的 _reused 標。就此列表更新完成。

順帶提一句 Vue.js 的元素過渡動畫處理 (v-transition) 也設計得非常巧妙,感興趣的自己看吧,就不展開介紹了

元件的 [keep-alive] 特性

keepAlive1

keepAlive2

Vue.js 為其元件設計了一個 [keep-alive] 的特性,如果這個特性存在,那麼在元件被重複建立的時候,會通過快取機制快速建立元件,以提升檢視更新的效能。程式碼在 directives/component.js

資料監聽機制

如何監聽某一個物件屬性的變化呢?我們很容易想到 Object.defineProperty 這個 API,為此屬性設計一個特殊的 getter/setter,然後在 setter 裡觸發一個函式,就可以達到監聽的效果。

ob

不過陣列可能會有點麻煩,Vue.js 採取的是對幾乎每一個可能改變資料的方法進行 prototype 更改:

ob_array1

但這個策略主要面臨兩個問題:

  1. 無法監聽資料的 length,導致 arr.length 這樣的資料改變無法被監聽
  2. 通過角標更改資料,即類似 arr[2] = 1 這樣的賦值操作,也無法被監聽

為此 Vue.js 在文件中明確提示不建議直接角標修改資料

ob_array2

同時 Vue.js 提供了兩個額外的“糖方法” $set 和 $remove 來彌補這方面限制帶來的不便。整體上看這是個取捨有度的設計。我個人之前在設計資料繫結庫的時候也採取了類似的設計 (一個半途而廢的內部專案就不具體獻醜了),所以比較認同也有共鳴。

path 解析器的狀態機設計

首先要說 parsers 資料夾裡有各種“財寶”等著大家挖掘!認真看一看一定不會後悔的

parsers/path.js 主要的職責是可以把一個 JSON 資料裡的某一個“路徑”下的資料取出來,比如:

var path = 'a.b[1].v'
var obj = {
  a: {
    b: [
      {v: 1},
      {v: 2},
      {v: 3}
    ]
  }
}
parse(obj, path) // 2

所以對 path 字串的解析成為了它的關鍵。Vue.js 是通過狀態機管理來實現對路徑的解析的:

state1

咋一看很頭大,不過如果再稍微梳理一下:

state graph

也許看得更清楚一點了,當然也能發現其中有一點小問題,就是原始碼中 inIdent 這個狀態是具有二義性的,它對應到了圖中的三個地方,即 in ident 和兩個 in (quoted) ident

實際上,我在看程式碼的過程中順手提交了這個 bug,作者眼明手快,當天就進行了修復,現在最新的程式碼裡已經不是這個樣子了:

state2

而且狀態機標識由字串換成了數字常量,解析更準確的同時執行效率也會更高。

一點自己的思考

首先是檢視的解析過程,Vue.js 的策略是把 element 或 template string 先統一轉換成 document fragment,然後再分解和解析其中的子元件和 directives。我覺得這裡有一定的效能優化空間,畢竟 DOM 操作相比之餘純 JavaScript 運算還是會慢一些。

然後是基於移動端的思考,Vue.js 雖確實已經非常非常小巧了 (min+gzip 之後約 22 kb),但它是否可以更小,繼續抽象出常用的核心功能,同時更快速,也是個值得思考的問題。

第三我非常喜歡通過 Vue.js 進行模組化開發的模式,Vue 是否也可以藉助類似 web components + virtual dom 的形態把這樣的開發模式帶到更多的領域,也是件很有意義的事情。

總結

Vue.js 裡的程式碼細節還不僅於此,比如:

  • cache.js 裡的快取機制設計和場景運用 (如在 parsers/path.js 中)
  • parsers/template.js 裡的 cloneNode 方法重寫和對 HTML 自動補全機制的相容
  • 在開發和生產環境分別用註釋結點和不可見文字結點作為檢視的“佔位符”等等

自己也在閱讀程式碼,瞭解 Vue.js 的同時學到了很多東西,同時我覺得程式碼實現只是 Vue.js 優秀的要素之一,整體的程式設計、API 設計、細節的取捨、專案的工程考量都非常棒!