[譯] Vue.js 內部原理淺析

原文:medium.com/js-imaginea…html

說到 JavaScript 框架,Vue.js 絕對是個熱門的 UI 框架(譯註:截至本文翻譯時其 Github 155k ⭐️ & 23k 🍴, 關注數已經超過了 React)。於我來講 Vue.js 最吸引人的地方在於 -- 其學習曲線,很是之低。我的角度來說,我感受就像正在作着 jQuery 一類的事情。鼓搗幾天以後,你就能開始創建應用了。vue

一年前我開始探索 Vue.js 並創建了一些應用。可是幾天前,一股深刻了解 Vue.js 代碼的渴望在我心中升騰。我翻閱了 Github 上的源碼並進行了多輪調試以瞭解其底層運行機制。這也是本文中我要寫的東西。git

因此,讓咱們來點乾貨,本文將嘗試給你以下 4 個問題的答案:github

  1. 當你建立一個 Vue.js 實例時發生了什麼?
  2. 模板內部都在發生着什麼?
  3. Virtual DOM 有何意義?
  4. 當一個屬性改變時模板是如何再次渲染的?

Vue 組件中包含一個模板(template),而模板在出如今瀏覽器裏以前必須經歷多個階段。咱們來編寫一個短小的模板,並以之做爲一個例子驅動本文的進行。算法

<div id="app">
  <span v-if="dynamic">Dynamic text</span>
  <span><p>Static text</p></span>
  <button @click="toggleFlag">Toggle Dynamic</button>
</div>
複製代碼

組件的 JS logic 就不寫出來了,由於模板自己已經能夠自解釋。瀏覽器

編譯階段

Vue compiler 讀取一個組件的模板,使之經歷下圖所示的 parsing、optimizing、codegen 階段並最終建立一個渲染函數。該渲染函數的職責就是建立一個 VNode,而該 VNode 會被 Virtual DOM 的 patch 過程用來建立真實 DOM。緩存

解析階段app

在編譯的這個階段對特定組件中的置標語言模板進行解析。正如你能在下圖中見到的,首先 parser 會將模板解析成 HTML parser,隨後轉成 AST(即 抽象語法樹)。框架

parsing 階段以後的 AST

AST 包含了諸如 attributes、parent、children、tag 等等的信息。解析過程當中也會將 directives 以相似元素的方式處理。諸如 v-forv-ifv-once 等結構化的 directives 會被表現爲一個特定元素 AST 中的 key-value 對。如咱們模板中的 v-if,在解析後將被推入 attrsMap 中變成形如 {v-if: 「dynamic」} 的對象。dom

優化階段

optimizer 的目標就是遍歷生成的 AST 並探測純靜態的子樹,即 DOM 中不會改變的那些部分。以下圖所示,這些元素將被標記爲 static。

優化後的 AST

一旦檢測到靜態子樹,Vue 便將其提高爲常量,從而不會在每次從新渲染時爲其生成新鮮的節點。這些節點也會在 Virtual DOM 的 patch 過程當中被徹底地跳過。

Codegen 階段

編譯的最後一個階段就是 Codegen,該階段將建立真正的渲染函數以用於 patch 過程。

render function 的層次結構

在上圖中,能夠看到模板的層次結構已經被轉換成了渲染函數的層次結構。基於 optimizer 打過的 static 標記,Codegen 將渲染函數分叉爲兩個獨立的函數。一個是普通的渲染函數,另外一個是靜態渲染函數。

最後,當真正的渲染過程觸發時,渲染函數將被用於建立 VNode。

注意:若是你使用了一個構建步驟,如單文件組件時,模板的編譯將提早發生。

observer 和 watcher — 反應式組件

Observer

Vue 會在底層遍歷全部咱們定義在 data 中的屬性,並經過 Object.defineProperty 將它們轉換爲 getter/setters。

當任何 data 屬性獲得一個新值時,set 函數將會通知 Watchers

Watcher

當一個 Vue 應用被初始化時,會爲每一個組件建立一個 Watcher。Watcher 會解析一個表達式,收集訂閱者並在表達式的值變化時觸發回調。這個作法被同時用在了 $watch API 和 directives 上。每一個組件實例都有一個相應的 watcher 實例,用以將渲染組件期間「觸及」的任何屬性記錄爲依賴項(譯註:在 getter 裏收集會訪問到的依賴數據)。其後,當一個依賴項的 setter 被觸發,它就會通知到 watcher,並最終觸發 patch 過程。

不管什麼時候,當一個數據的改變被觀察到,就會開啓一個隊列並緩存本輪事件循環中發生的全部數據改變。全部 watchers 都被添加到此隊列中。每一個 watcher 有一個獨特的自增 Id,這樣若是相同的 watcher 被觸發屢次,它只會在被使用前被推送到隊列中一次。由於 watchers 要以從 parent 到 child 的順序運行,因此隊列也會被排序。

在內部,Vue 會爲異步排隊嘗試使用原生的 Promise.thenMessageChannel,實在不行就用 setTimeout(fn, 0)

nextTick 函數會消耗掉隊列中的全部 watchers。在那以後,渲染過程將經過 watcher 的 run() 函數被初始化。

patch 過程

patch 過程基本上就是一個使用 Virtual DOM 和真實 DOM 高效交互的過程。一個 Virtual DOM 就是表示一個 DOM(文檔對象模型 - Document Object Model) 的 JavaScript 對象。Vue.js 在內部使用了 snabbdom 庫。因此,讓咱們看看 patch 過程當中到底發生了什麼。

整個過程就是個關於兩相對比新舊 VNode (Virtual DOM Node) 的遊戲。

其算法將以以下方式運行 --

  1. 首先檢查舊 VNode 是否存在,若不存在則爲每一個 VNode 建立 DOM 元素。當你首次登陸到應用中而且第一次渲染過程初始化時,就是舊 VNode 不存在的時候。
  2. 反過來講,若是舊 VNode 存在的話,比較新舊 VNode 的 children 的過程就將啓動 -- 普通的節點將在 DOM 中保持原狀,新節點將被添加,而舊的且不匹配的節點將從 Virtual DOM 和真實 DOM 中同時移除。
  3. 另外若是有必要的話,匹配節點的樣式、class、dataset 和事件監聽器也會被更新或刪除。

相同的過程會遞歸式地應用到全部節點上。

此外,我得提醒你一些事情 -- 靜態節點,咱們在優化階段討論過的。靜態節點樹並不會被觸及,並被原樣使用。這意味着 -- 咱們並不須要對這種樹與真實 DOM 交互。

生命週期鉤子

讓咱們來討論一下特定組件的生命跨度,並嘗試把它們帶入本文討論的話題。

組件生命週期可被分爲四個節段 --

  • 建立
  • 加載
  • 更新
  • 銷燬

一旦 Vue 的新實例被執行,建立組件的過程就啓動了。

beforeCreation: 收集組件所需的事件、數據以前。換句話說 -- 在收集 watchers/dependencies 的過程當中。

created: 當 Vue 設置好 data 和 watchers 的時候。

beforeMount: 早於 patch 過程。VNode 正在基於 data 和 watchers 被建立。

mount: patch 過程以後。

beforeUpdate: 若是數據改變,watcher 會更新 VNode 並從新開始一次 patch 過程。

update: patch 過程完成時。

beforeDestroy: 卸載組件以前。此時,組件還是全須全尾的。

destroyed: 銷燬 watchers 並刪除附加其上的事件監聽器或子組件時。



--End--

搜索 fewelife 關注公衆號

轉載請註明出處

相關文章
相關標籤/搜索