Vue原始碼閱讀 – 檔案結構與執行機制

發表於2018-10-10

vue已是目前國內前端web端三分天下之一,同時也作為本人主要技術棧之一,在日常使用中知其然也好奇著所以然,另外最近的社群湧現了一大票vue原始碼閱讀類的文章,在下借這個機會從大家的文章和討論中汲取了一些營養,同時對一些閱讀原始碼時的想法進行總結,出產一些文章,作為自己思考的總結,本人水平有限,歡迎留言討論~

目標Vue版本:2.5.17-beta.0

vue原始碼註釋:github.com/SHERlocked9…

宣告:文章中原始碼的語法都使用 Flow,並且原始碼根據需要都有刪節(為了不被迷糊 @_@),如果要看完整版的請進入上面的github地址,本文是系列文章,文章地址見底部~

0. 前備知識

  • Flow
  • ES6語法
  • 常用的設計模式
  • 柯里化等函數語言程式設計思想

這裡推介幾篇前備文章:JS 靜態型別檢查工具 FlowECMAScript 6 入門 – 阮一峰JS中的柯里化JS 觀察者模式JS 利用高階函式實現函式快取(備忘模式)

1. 檔案結構

檔案結構在vue的CONTRIBUTING.md中有介紹,這邊直接翻譯過來:

幾個重要的目錄:

  • compiler: 編譯,用來將template轉化為render函式
  • core: Vue的核心程式碼,包括響應式實現、虛擬DOM、Vue例項方法的掛載、全域性方法、抽象出來的通用元件等
  • platform: 不同平臺的入口檔案,主要是 web 平臺和 weex 平臺的,不同平臺有其特殊的構建過程,當然我們的重點是 web 平臺
  • server: 服務端渲染(SSR)的相關程式碼,SSR 主要把元件直接渲染為 HTML 並由 Server 端直接提供給 Client 端
  • sfc: 主要是 .vue 檔案解析的邏輯
  • shared: 一些通用的工具方法,有一些是為了增加程式碼可讀性而設定的

其中在platform下src/platforms/web/entry-runtime.js檔案作為執行時構建的入口,ESM方式輸出 dist/vue.runtime.esm.js,CJS方式輸出 dist/vue.runtime.common.js,UMD方式輸出 dist/vue.runtime.js,不包含模板 template 到 render 函式的編譯器 src/platforms/web/entry-runtime-with-compiler.js檔案作為執行時構建的入口,ESM方式輸出 dist/vue.esm.js,CJS方式輸出 dist/vue.common.js,UMD方式輸出 dist/vue.js,包含compiler

2. 入口檔案

任何前端專案都可以從 package.json 檔案看起,先來看看它的 script.dev 就是我們執行 npm run dev 的時候它的命令列:

這裡的 rollup 是一個類似於 webpack 的JS模組打包器,事實上 Vue - v1.0.10 版本之前用的還是 webpack ,其後改成了 rollup ,如果想知道為什麼換成 rollup ,可以看看 尤雨溪本人的回答,總的來說就是為了打出來的包體積小一點,初始化速度快一點。

可以看到這裡 rollup 去執行 scripts/config.js 檔案,並且給了個引數 TARGET:web-full-dev,那來看看 scripts/config.js 裡面是啥

format 編譯方式說明:

  • es: ES Modules,使用ES6的模板語法輸出
  • cjs: CommonJs Module,遵循CommonJs Module規範的檔案輸出
  • amd: AMD Module,遵循AMD Module規範的檔案輸出
  • umd: 支援外鏈規範的檔案輸出,此檔案可以直接使用script標籤

這裡的 web-full-dev 就是對應剛剛我們在命令列裡傳入的命令,那麼 rollup 就會按下面的 entry 入口檔案開始去打包,還有其他很多命令和其他各種輸出方式和格式可以自行檢視一下原始碼。

因此本文主要的關注點在包含 compiler 編譯器的 src/platforms/web/entry-runtime-with-compiler.js 檔案,在生產和開發環境中我們使用 vue-loader 來進行 template 的編譯從而不需要帶 compiler 的包,但是為了更好的理解原理和流程還是推介從帶 compiler 的入口檔案看起。

先看看這個檔案,這裡匯入了個 Vue ,看看它從哪來的

繼續看

keep moving

keep moving*2

當我們 new Vue( ) 的時候,實際上呼叫的就是這個建構函式,可以從這裡開始看了。

3. 執行機制

這裡我用xmind粗略的畫了一張執行機制圖,基本上後面的分析都在這張圖上面的某些部分了

本文 Vue 例項都是用 vm 來表示

Vue原始碼閱讀 – 檔案結構與執行機制

上面這個圖可以分為多個部分細加閱讀,具體的實現我們在後面的文章中詳細討論,這裡先貼一部分原始碼嚐嚐鮮

3.1 初始化 _init( )

Vue原始碼閱讀 – 檔案結構與執行機制

當我們在 main.js 裡 new Vue( ) 後,Vue 會呼叫建構函式的 _init( ) 方法,這個方法是位於 core/instance/index.js 的 initMixin( ) 方法中定義的

我們可以看看 init( ) 這個方法到底進行了哪些初始化:

這裡 _init() 方法中會對當前 vm 例項進行一系列初始化設定,比較重要的是初始化 State 的方法 initState(vm) 的時候進行 data/props 的響應式化,這就是傳說中的通過 Object.defineProperty() 方法對需要響應式化的物件設定 getter/setter,以此為基礎進行依賴蒐集(Dependency Collection),達到資料變化驅動檢視變化的目的。

最後檢測 vm.$options 上面有沒有 el 屬性,如果有的話使用 vm.$mount 方法掛載 vm,形成資料層和檢視層的聯絡。這也是如果沒有提供 el 選項就需要自己手動 vm.$mount('#app') 的原因。

我們看到 created 鉤子是在掛載 $mount 之前呼叫的,所以我們在 created 鉤子觸發之前是無法操作 DOM 的,這是因為還沒有渲染到 DOM 上。

3.2 掛載 $mount( )

Vue原始碼閱讀 – 檔案結構與執行機制

掛載方法 vm.$mount( ) 在多個地方有定義,是根據不同打包方式和平臺有關的,src/platform/web/entry-runtime-with-compiler.jssrc/platform/web/runtime/index.jssrc/platform/weex/runtime/index.js,我們的關注點在第一個檔案,但在 entry-runtime-with-compiler.js 檔案中會首先把 runtime/index.js 中的 $mount 方法儲存下來,並在最後用 call 執行:

在 Vue 2.0 版本中,所有 Vue 的元件的渲染最終都需要 render 方法,無論我們是用單檔案 .vue 方式開發元件,還是寫了 el 或者 template 屬性,最終都會轉換成 render 方法。這裡的 compileToFunctions 就是把 template 編譯為 render 的方法,後面會介紹。

這裡的 el 一開始如果不是DOM元素的話會被 query 方法換成DOM元素再被傳給 mountComponent 方法,我們繼續看 mountComponent 的定義:

mountComponent 方法裡例項化了一個渲染 Watcher,並且傳入了一個 updateComponent ,這個方法:() => { vm._update(vm._render(), hydrating) } 首先使用 _render 方法生成 VNode,再呼叫 _update 方法更新DOM。可以看看檢視更新部分的介紹

這裡呼叫了幾個鉤子,他們的時機可以關注一下。

3.3 編譯 compile( )

如果在需要轉換 render 的場景下,比如我們寫的 template ,將會被 compiler 轉換為 render 函式,這其中會有幾個步驟組成:

Vue原始碼閱讀 – 檔案結構與執行機制

入口位於剛剛 src/platform/web/entry-runtime-with-compiler.js 的 compileToFunctions 方法:

繼續看這裡的 createCompiler 方法:

這裡可以看到有三個重要的過程 parseoptimizegenerate,之後生成了 render 方法程式碼。

  • parse:會用正則等方式解析 template 模板中的指令、class、style等資料,形成抽象語法樹 AST
  • optimize:優化AST,生成模板AST樹,檢測不需要進行DOM改變的靜態子樹,減少 patch 的壓力
  • generate:把 AST 生成 render 方法的程式碼

3.4 響應式化 observe( )

Vue作為一個MVVM框架,我們知道它的 Model 層和 View 層之間的橋樑 ViewModel 是做到資料驅動的關鍵,Vue的響應式是通過 Object.defineProperty 來實現,給被響應式化的物件設定 getter/setter ,當 render 函式被渲染的時候會觸發讀取響應式化物件的 getter 進行依賴收集,而在修改響應式化物件的時候會觸發設定 settersetter 方法會 notify 它之前收集到的每一個 watcher 來告訴他們自己的值更新了,從而觸發 watcherupdatepatch 更新檢視。

Vue原始碼閱讀 – 檔案結構與執行機制

響應式化的入口位於 src/core/instance/init.js 的 initState 中:

它非常規律的定義了幾個方法來初始化 propsmethodsdatacomputedwathcer,這裡只看 initData 方法,來窺一豹

首先判斷了下 data 是不是函式,是則取返回值不是則取自身,之後有一個 observe 方法對 data 進行處理,看看這個方法

這個方法主要用 data 去例項化一個 Observer 物件例項,Observer 是一個 Class,Observer 的建構函式使用 defineReactive 方法給物件的鍵響應式化,它給物件的屬性遞迴新增 getter/setter,用於依賴收集和 notify 更新,這個方法大概是這樣的

3.5 檢視更新 patch( )

Vue原始碼閱讀 – 檔案結構與執行機制

當使用 defineReactive 方法將物件響應式化後,當 render 函式被渲染的時候,會讀取響應化物件的 getter 從而觸發 getter 進行 watcher 依賴的收集,而在修改響應化物件的值的時候,會觸發 setter 通知 notify 之前收集的依賴,通知自己已被修改,請按需重新渲染檢視。被通知的 watcher 呼叫 update 方法去更新檢視,位於上面介紹過的傳遞給 new Watcher( )updateComponent 方法中,這個方法會呼叫 update 方法去 patch 更新檢視。

這個 _render 方法生成虛擬 Node, _update 方法中的會將新的 VNode 與舊的 VNode 一起傳入 patch

_update 呼叫 __patch__ 方法,它主要是對新老 VNode 進行比較 patchVnode,經過 diff 演算法得出它們的差異直接,最後這些差異的對應 DOM 進行更新。

到這裡基本上一個主要的流程就介紹完了,我們大概瞭解了一個 Vue 從一個建構函式的例項化開始是如何運轉的,後面會展開來討論一下各個部分的內容,在下才疏學淺,未免紕漏,歡迎大家討論~


本文是系列文章,隨後會更新後面的部分,共同進步~

  1. Vue原始碼閱讀 – 檔案結構與執行機制
  2. Vue原始碼閱讀 – 依賴收集原理
  3. Vue原始碼閱讀 – 批量非同步更新與nextTick原理

網上的帖子大多深淺不一,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~

相關文章