1. 程式人生 > >前端Vue 原始碼分析-邏輯層

前端Vue 原始碼分析-邏輯層

Vue 原始碼分析-邏輯層

預期的效果:

先摘一段原始碼,作為簡單的分析

預期的效果:

監聽input的輸入,input在輸入的時候,會觸發 watch與computed函式,並且會更新原始的input的數值。所以直接跟input相關的處理就有3處,但實際上會有連帶性的觸發,觸發watch的input函式的時候,還會觸發this.answer對應的依賴處理

看看內部是如何處理的:

Vue在初始化data的時候,會通過Object.defineProperty重新定義input的set與get訪問介面,同時會建立一個記錄並且保持其資料對應的依賴watcher物件的Dep物件,這個Dep物件是通過閉包的方式儲存在每個獨立的data中,而Dep就是用於收集當前data所依賴的Watcher物件

簡單來說

  1. 在data中定義了input,那麼意味著需要對這個變數進行defineProperty的處理,並建立Dep物件

  2. watch中的input函式會變成一個Watcher物件,因為它與input有關係,所以需要在data的input的Dep中儲存一份引用

  3. computed中的compiledMarkdown函式會變成一個Watcher物件,,因為它與input有關係,所以需要在data的input的Dep中儲存一份引用

input資料的監控內部建立的Dep的結構就是如下:

根據當前這個例子的程式碼,watch與computed明明只有2個對應的Watcher物件,為什麼subs會有3個呢?多增加的一個是幹什麼的?這個多出的Watcher就是vue2中的虛擬dom的處理,後面會提到

這裡最終可以簡單的梳理下更新的流程:當input資料發生變化的時候,只需要呼叫響應依賴的Watcher物件,Watcher物件就會負責各自的更新處理。這裡面向物件的設計優勢就體現出來了,將行為分佈在各個物件中,並讓這些物件負責自己的行為,所以每個不同Watcher物件更新各自的特點,處理各自的邏輯

更新

更新邏輯:

vue1的 dom更新方式採用佇列+直接更新的處理,這種簡單粗暴。vue2在vue1的設計上,繼續保留了佇列的處理方式,同時結合了時下最流行的 virtual dom

記得在Vue1中,每個Watcher物件都會儲存各自的dom節點的處理方式,通過對Watcher的的處理達到直接更新DOM的目的。Vue2因為引入的Virtual Dom的機制,所以Watcher的工作就需要變化了,大多數的Watcher不再直接負責DOM的更新操作,而只是更新資料。這裡用了大多數,因為還有一個Watcher是跟Virtual Dom相關的。所以這就是在上文提到的Dep中會多一個Watcher的原因了

Virtual DOM

虛擬DOM的文章現在已經很多了,但是如何緊密結合vue中,到實際的運用是我們分析的重點,這裡只是粗略下,我還要抽時間把演算法看完先

原理:

簡單的說,直接通過JS操作瀏覽器API去繪製DOM節點是很慢的,大量的頁面處理中,開發者不經意就會呼叫更多多餘或者重複的操作,這種是有效能開銷的。那麼有什麼辦法減少這種是誤操作呢?就是通過一種方式能算出來最小的更新量,從而提高效率。既然要計算出對小的更新量,那麼就會有對比,需要通過對新舊兩個節點的對比從而計算出。DOM的操作很慢,但是JS確很快的,DOM 樹上的結構、屬性資訊我們都可以很容易地用 JavaScript 物件表示出來,既然我們可以用JS物件表示DOM結構,那麼當資料狀態發生變化而需要改變DOM結構時,我們先通過JS物件表示的虛擬DOM計算出實際DOM需要做的最小變動,反過來,就可以根據這個用 JavaScript 物件表示的樹結構來構建一棵真正的DOM樹,操作實際DOM更新了, 從而避免了粗放式的DOM操作帶來的效能問題。

根據上面的原理,Virtual DOM在實現上首先就必須先建立可以對比的JS物件,這個叫做vnode,也就是虛擬DOM了,這個物件是真實DOM結構的一個對映,通過對比更新前後vnode的變化差異diff,記錄下來的不同就是我們需要對頁面真正的 DOM 操作。

Virtual DOM演算法,簡單總結下包括幾個步驟:

  1. 用JS物件描述出DOM樹的結構,然後在初始化構建中,用這個描述樹去構建真正的DOM,並實際展現到頁面中

  2. 當有資料狀態變更時,重新構建一個新的JS的DOM樹,通過新舊對比DOM數的變化diff,並記錄兩棵樹差異

  3. 把步驟2中對應的差異通過步驟1重新構建真正的DOM,並重新渲染到頁面中,這樣整個虛擬DOM的操作就完成了,檢視也就更新了

看到這裡可以簡單總結下,Vue中Watcher與Virtual DOM的關係:

  1. Watcher 是來決定你要不要更新這個dom

  2. 虛擬DOM是用來找出怎麼以最小的代價來更新

Vue2中對應的邏輯

這裡不會涉及演算法,並非這章的重點,主要看下整個更新過程中,虛擬DOM邏輯是怎麼配合工作的。

繼續input的資料流向,之前講到了input中的Dep是儲存了3個Watcher物件的引用,其中會有一個Watcher是跟整個頁面的渲染有關係的,這個就是用來封裝vnode的處理。

當遍歷Dep這個儲存Watcher陣列的時候,會把Watcher加入到一個非同步的佇列中進行處理

程式碼進行了簡化

function queueWatcher(watcher) {

  var id = watcher.id;

  if (has[id] == null) {

    has[id] = true;

    queue.push(watcher);

    nextTick(flushSchedulerQueue);

  }

}

function flushSchedulerQueue() {

    queue.sort(function(a, b) { return a.id - b.id; });

    for (index = 0; index ) {

      watcher = queue[index];

      id = watcher.id;

      has[id] = null;

      watcher.run();

    }

}

這裡很關鍵的一個點就是針對queue進行了排序,原因就是其中有一個Wacher是儲存了vnode了,因為最後一步才是vnode的對比更新。必須讓前面的Watcher更新資料完畢後,最後vnode才能做真正的對比,不過computed的Wacher不會加入到這個佇列中,它會再編譯樹中動態的執行。

當前面的Watcher執行完畢後,調到最後一個Watcher,可以看到對應的程式碼

vm._update(vm._render(), hydrating);

  1. 通過vm._render方法構建vnode

  2. 通過vm._update 對比vnode,並渲染到頁面中

vm._render

初始化的時,會通過構建出來的JS描述樹,生成初始vnode,去繪製初始頁面。每次DOM變化的時候,我們還是需要重新構建這個描述樹,通過這個描述樹去構建新的vnode

這個描述樹生成相當複雜,vue2內部專門會有一個AST是幹這個事的

對應的結構是這樣的,這個可以其實就是真實DOM樹的一個結構映射了:

但是這個結構是可執行的,可編譯的,通過with的方式改變this的上下文,動態執行每個可執行的程式碼部分,並把每個節點部分都編譯成vnode,組成一個有對應層次結構的vnode物件

舉例來說

div是最外層的vnode

div有子節點=> p,生成對應vnode

p有子節點=>文字節點answer,生成對應vnode

每個vnode會儲存每個對應節點一些計算資訊,比如tag、data、 children、text這些都是用於後面的比對計算的

vm._update

通過render拿到了vnode,然後通過update對比vnode繪製到頁面

update這個方法內部有段程式碼

vm.$el = vm.__patch__(prevVnode, vnode);

從這個字面意思就明顯知道,更新補丁,用於對比新舊2個vnode,

vue2有個專門的patch檔案用於vnode的對比策略,patch內部會細分很多策略出來

  1. 如果vnode不存在但是oldVnode存在,就意味著要銷燬

  2. 如果oldVnode不存在但是vnode存在,說明意圖是要建立新節點

  3. 當vnode和oldVnode都存在時,就需要更新了

每一種策略都對應的不同的處理方式,更新才意味著需要對比新舊的vnode,首先是需要判斷下兩個節點是否值得比較,在這個例子裡面只改變了屬性input與answer的值,所以,這裡是屬於同節點內的屬性變更的,所以檢測vnode的變化也是相對最簡單,遞迴子節點,通過patchVnode檢測每個節點屬性的變化

if(sameVnode(oldStartVnode, newStartVnode)) {

  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);

  oldStartVnode = oldCh[++oldStartIdx];

  newStartVnode = newCh[++newStartIdx];

}

當對比到差異時,例如文字answer被改變,那麼對應的vnode在對比的時候,就能找到差異,然後重新設定值,此刻的node就是真實的DOM引用的,如果改變了textContent就意味著頁面上呈現的資料就直接被改變了

if (oldVnode.text !== vnode.text) {

   nodeOps.setTextContent(elm, vnode.text);

}

function setTextContent (node, text) {

  node.textContent = text;

}