Vue2源碼分析-邏輯梳理

好久以前就看完vue1,可是太懶就一直沒寫博客,此次看Vue2打算抽下懶筋先把本身看過了記錄下來,不然等所有看完,估計又沒下文了vue

看源碼總須要抱着一個目的,不然就很難堅持下去,我並沒作過vue的項目,我幾乎不多會依賴大型的框架,一個是跟平臺有關係,另外一方面由於我以爲是對本身能力的束縛,而我更渴望的就是經過閱讀別人的源碼,吸取別人的思路,取之精華去之糟粕,從而改造本身的項目。固然,這是在項目條件容許的狀況下。目前我有個項目持續開發的項目,基本融入了本身這麼多年看到框架思路,這纔是我堅持看源碼的緣由。能夠參考下吧 xut.jsnode

vue2源碼晦澀的程度比vue1高多了,翻開源碼估計90%都會直接關閉吧,主要仍是引入Flow的語法問題,能夠用babel-plugin-transform-flow-strip-types去轉化下便可。看源碼仍是有必定技巧的,這個因每一個人而已。大神嘛,直接掃描下代碼,看看註釋,看看流程,閉目YY一下,就知道大致是怎麼玩的了。我表示作不到,普通人呢,我仍是主張有時間的話本身能動手從零開始實現一遍,這樣你才能真正去理解做者的設計的意圖。一樣的,我也正在從零在實現vue,不過徹底同樣仍是不可能滴,只能是大致上理解做者的設計,可是足夠了 vue-analysis
git

vue2的源碼的前戲太多了,很難進入高潮部分,從頭開始入戲須要有很強的邏輯能力、空間跳躍能力,因此這裏不打算從頭開始疏通,而是採用從後往前推導,先看看要實現其功能,最後須要哪些實現步驟與機制github

 

先摘一段源碼,做爲簡單的分析算法

{3505DBA6-6138-047A-212E-044652E37CE6}

預期的效果:數組

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

 

看看內部是如何處理的:babel

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的結構就是以下:

{246AEF4D-7E82-1EDE-5851-C0ACBE60A1BC}

根據當前這個例子的代碼,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 < queue.length; 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樹的一個結構映射了:

{B60D40D5-7A1D-75D8-40B1-884B212EC8D2}

可是這個結構是可執行的,可編譯的,經過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;
}

經過這個簡單的例子是不可以評價這個Virtual DOM的優劣的,由於改動確實很小,並且都是局部的變化,都是直接更新到頁面中了,真正的代碼部分是作了很是多的優化手段的

 

總結

由於不是具體的算法分析,因此不會一段代碼一段代碼的去句斟字酌了,整段分析都是基於這個簡單的代碼,因此在實現上不少地方是有誤差的,不能以偏概全,可是經過這個文章,想必你對vue2的內部邏輯應該是有一個初步的認識。後續就會有時間就會開始比較細緻的分解咯~~~

相關文章
相關標籤/搜索