Virtual dom至關於框架API與運行環境之間的中間層,將框架渲染與DOM API進行解耦,增長了跨平臺能力。(能夠將virtual dom映射爲DOM的步驟,改成映射到其餘的執行環境,好比安卓、IOS)javascript
設計js數據結構來表示DOM節點:html
function VNode(tagName, props, children, key) { this.tagName = tagName this.props = props this.children = children this.key = key }
實現一個方法,可以從VNode生成真正的DOM tree(VNode對應根節點):vue
function render({tag, props, children, key}) { // 經過 tag 建立節點 let el = document.createElement(tag) // 設置節點屬性 for (const key in props) { if (props.hasOwnProperty(key)) { const value = props[key] el.setAttribute(key, value) } } if (key) { el.setAttribute('key', key) } // 遞歸建立子節點 if (children) { children.forEach(element => { let child if (element instanceof VNode) { child = this._createElement( element.tag, element.props, element.children, element.key ) } else { child = document.createTextNode(element) } el.appendChild(child) }) } return el }
這個映射是由用戶來定義的(通常經過template),因此這個方法通常是經過編譯template來獲得。java
第4步和第5步的2個函數能夠合併。找出新vdom和DOM tree之間的差別,同時更新DOM tree來消除差別。git
首次渲染:github
狀態更新:根據model渲染新的vdom tree,與舊的vdom tree對比,計算出須要的更新。算法
若是選擇合併diff和patch算法,渲染出新vdom之後,將新的vdom tree與真實的DOM tree對比,同時更新DOM tree,使view(DOM)與新 vdom(data model)保持一致。segmentfault
diff的算法有不少種實現(見下面的參考資料),目的都是計算出須要的更新步驟,以便應用到真實的DOM上。
各類vdom tree diff算法之間的主要差別在於diff子節點列表的算法(也就是下面的listDiff)。把握各類listDiff算法的關鍵在於,數組
開始對2棵樹同時進行深度優先遍歷。這是特殊的深度優先遍歷,每次同時訪問2個節點用於對比:舊vdom的節點(如下稱爲oldNode)和新vdom的對應節點(如下稱爲newNode)。緩存
若是newNode.tag === oldNode.tag && newNode.key === oldNode.key(key能夠都爲undefined),將它們視爲同一個元素在不一樣時刻的狀態。要分別diff它們的屬性和子節點。
diffChildren(oldNode.children, newNode.children),檢測子節點(數組)是否發生了變化,這些修改應該記錄爲patch。此外,diffChildren將會遞歸調用diffTree,來檢測子樹的變化。
要檢測子節點數組的變化,即須要一個算法來找出:oldNode.children數組如何經過 增長、刪除、移動 節點,變成newNode.children。咱們把這種算法稱爲listDiff:
檢測刪除的節點:遍歷oldNode.children,對於每一個child,查找newNode.children中是否有相同key的節點(用map數據結構,查找的時間爲log(n))。若是不存在,說明這個是被刪除的節點,要輸出刪除操做,並記錄它在中間狀態數組中的下標。
檢測增長和移動的節點:遍歷newNode.children,對於每一個child,查找oldNode.children中是否存在相同key的節點。
若是存在,可是在newNode.children中的下標不等於在中間狀態數組中的下標,說明這個是被移動的節點,要輸出這個節點移動前和移動後的中間狀態數組中的位置。
找到2個對應的子節點(一個在oldNode.children中,一個在newNode.children中,兩個節點是同一個元素在不一樣時刻的狀態)來調用diffTree:
遍歷oldNode.children,對於每個child,找出在newNode.children中有相同key的節點,這兩個節點就是相互對應的節點。以這兩個節點爲參數調用diffTree。
diffTree的基本代碼以下(先不考慮patch):
function diffTree(oldTreeRootNode, newTreeRootNode) { diffProps(oldTreeRootNode, newTreeRootNode); // 舊樹和新樹中對應的節點結對返回 const pairs = listDiff(oldTreeRootNode.children, newTreeRootNode.children); for (const [oldChild, newChild] of pairs) { diffTree(oldChild, newChild); } }
上面的討論沒有考慮沒有key的節點。能夠將【舊list的無key節點】與【新list的無key節點】按出現順序一一對應,視爲同一個節點。這個算法的實現能夠參考參考資料1。這個實現僅用於理解中間狀態diff算法的思想。
這個算法並非vue所使用的(見參考資料2)。這個算法僅用於理解diff的思想。
patch的意思是「如何修改舊的vdom,將它變爲新的vdom」。它是diff vdom最重要的輸出,畢竟咱們diff vdom的目的就是要知道如何修改DOM。
patch的操做包括:
增長、刪除、移動某個節點的子節點。
可見,任何patch的操做都和某個節點相關,而且這個節點一定在舊vdom和新vdom中都存在的。
反證法:假設某個patch的操做(設爲patchA)做用的節點不存在於舊vdom中,說明這個節點或它的某個祖先節點是新增的節點,也就是說,一定有一個「增長某個節點的子節點」的patch操做(設爲patchB)做用於一個祖先節點。既然patchB意味着增長整個子樹,那麼patchA根本就沒有存在的必要,由於它所在的整個子樹在patchB的時候就被已經正確建立了。
由於每個patch操做都關聯於一個已存在節點,因此咱們存儲patch的方式是:爲每一箇舊vdom中的節點分配一個數組,這個數組包括了全部和這個節點有關的patch操做。所以最終的patches是一個二維數組。在第一個維度上,節點按照深度優先遍歷的順序排列,也就是第1行是根節點的patch操做,第2行是左子節點的patch操做,第3行是【左子節點的左子節點】的patch操做,以此類推。
獲得了patches之後,經過將patches中的操做應用於對應的DOM節點,咱們就能夠更新DOM樹,使得DOM樹等價於新的vdom樹。
更新的方法就是,對dom進行深度優先遍歷,當前元素深度優先遍歷的序號是n,那麼patches[n]就是這個元素關聯的patch操做,而後將這些操做依次應用於這個元素。
若是有一個子節點是被增長的,那麼這個子節點下面的子樹就能夠被跳過了,由於這是按照新vdom建立的子樹,不須要更新。
對patch的處理上, 參考資料5實現得比較清晰。