摘要:記錄當前節點的標志這是當前節點的差異深度遍歷子節點對當前節點進行操作將差異的部分應用到中這次的粗糙的基本已經實現了,具體的情況更加復雜。
前言
目前廣為人知的React和Vue都采用了virtual-dom,Virtual DOM憑借其高效的diff算法,讓我們不再關心性能問題,可以隨心所欲的修改數據狀態。在實際開發中,我們并不需要關心Virtual DOM是如何實現的,但是理解Virtual DOM的實現原理確實有必要的。本文是參照https://github.com/livoras/si... DOM。
一、前端應用狀態管理在日益復雜的前端應用中,狀態管理是一個經常被提及的話題,從早期的刀耕火種時代到jQuery,再到現在流行的MVVM時代,狀態管理的形式發生了翻天覆地的變化,我們再也不用維護茫茫多的事件回調、監聽來更新視圖,轉而使用使用雙向數據綁定,只需要維護相應的數據狀態,就可以自動更新視圖,極大提高開發效率。
但是,雙向數據綁定也并不是唯一的辦法,還有一個非常粗暴有效的方式:一旦數據發生變化,重新繪制整個視圖,也就是重新設置一下innerHTML。這樣的做法確實簡單、粗暴、有效,但是如果只是因為局部一個小的數據發生變化而更新整個視圖,性價比未免太低了,而且,像事件,獲取焦點的輸入框等,都需要重新處理。所以,對于小的應用或者說局部的小視圖,這樣處理完全是可以的,但是面對復雜的大型應用,這樣的做法不可取。所以我們可以采取用JavaScript的方法來模擬DOM樹,用新渲染的對象樹去和舊的樹進行對比,記錄下變化的變化,然后應用到真實的DOM樹上,這樣我們只需要更改與原來視圖不同的地方,而不需要全部重新渲染一次。這就是virtual-DOM的優勢
二、視圖渲染相對于DOM對象,原生的JavaScript對象處理得更快,而且簡單。DOM樹上的結構,屬性信息我們都能通過JavaScript進行表示出來,例如:
var element = { tagName: "ul", // 節點標簽名 props: { // dom的屬性鍵值對 id: "list" }, children: [ {tagName: "li", props: {class: "item"}, children: ["Item 1"]}, {tagName: "li", props: {class: "item"}, children: ["Item 2"]}, {tagName: "li", props: {class: "item"}, children: ["Item 3"]} ] }
那么在html渲染的結果就是:
既然能夠通過JavaScript表示DOM樹的信息,那么就可以通過使用JavaScript來構建DOM樹。
然而光是構建DOM樹,沒什么卵用,我們需要將JavaScript構建的DOM樹渲染到真實的DOM樹上,用JavaScript表現一個dom一個節點非常簡單,我們只需要記錄他的節點類型,屬性鍵值對,子節點:
function Element(tagName, props, children) { this.tagName = tagName this.props = props this.children = children }
那么ul標簽我們就可以使用這種方式來表示
var ul = new Element("ul", {id: "list"}, [ {tagName: "li", props: {class: "item"}, children: ["Item 1"]}, {tagName: "li", props: {class: "item"}, children: ["Item 2"]}, {tagName: "li", props: {class: "item"}, children: ["Item 3"]} ])
說了這么多,他只是用JavaScript表示的一個結構,那該如何將他渲染到真實的DOM結構中呢:
Element.prototype.render = function() { let el = document.createElement(this.tagName), // 節點名稱 props = this.props // 節點屬性 for (var propName in props) { propValue = props[propName] el.setAttribute(propName, propValue) } this.children.forEach((child) => { var childEl = (child instanceof Element) ? child.render() : document.createTextNode(child) el.appendChild(childEl) }) return el }
如果我們想將ul渲染到DOM結構中,就只需要
ulRoot = ul.render() document.appendChild(ulRoot)
這樣就完成了ul到DOM的渲染,也就有了真正的DOM結構
React的核心算法是diff算法(這里指的是優化后的算法)我們來看看diff算法是如何實現的:
diff只會對相同顏色方框內的DOM節點進行比較,即同一個父節點下的所有子節點。當發現節點不存在,則該節點和子節點會被完全刪除,不會做進一步的比較。
在實際的代碼中,會對新舊兩棵樹進行深度的遍歷,給每一個節點進行標記。然后在新舊兩棵樹的對比中,將不同的地方記錄下來。
// diff 算法,對比兩棵樹 function diff(oldTree, newTree) { var index = 0 // 當前節點的標志 var patches = {} // 記錄每個節點差異的地方 dfsWalk(oldTree, newTree, index, patches) return patches } function dfsWalk(oldNode, newNode, index, patches) { // 對比newNode和oldNode的差異地方進行記錄 patches[index] = [...] diffChildren(oldNode.children, newNode.children, index, patches) } function diffChildren(oldChildren, newChildren, index, patches) { let leftNode = null var currentNodeIndex = index oldChildren.forEach((child, i) => { var newChild = newChildren[i] currentNodeIndex = (leftNode && leftNode.count) // 計算節點的標記 ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1 dfsWalk(child, newChild, currentNodeIndex, patches) // 遍歷子節點 leftNode = child }) }
例如:
在圖中如果div有差異,標記為0,那么:
patches[0] = [{difference}, {difference}]
同理,有p是patches[1], ul是patches[3],以此類推
patches指的是差異變化,這些差異包括:1、節點類型的不同,2、節點類型相同,但是屬性值不同,文本內容不同。所以有這么幾種類型:
var REPLACE = 0, // replace 替換 REORDER = 1, // reorder 父節點中子節點的操作 PROPS = 2, // props 屬性的變化 TEXT = 3 // text 文本內容的變化
如果節點類型不同,就說明是需要替換,例如將div替換成section,就記錄下差異:
patches[0] = [{ type: REPLACE, node: newNode // section },{ type: PROPS, props: { id: "container" } }]四、將差異應用到DOM樹上
在標題二中構建了真正的DOM樹的信息,所以先對那一棵DOM樹進行深度優先的遍歷,遍歷的時候同
patches對象進行對比,找到其中的差異,然后應用到DOM操作中。
function patch(node, patches) { var walker = {index: 0} // 記錄當前節點的標志 dfsWalk(node, walker, patches) } function dfsWalk(node, walker, patches) { var currentPatches = patches[walker.index] // 這是當前節點的差異 var len = node.childNodes ? node.childNodes.length : 0 for (var i = 0; i < len; i++) { // 深度遍歷子節點 var child = node.childNodes[i] walker.index++ dfsWalk(child, walker, patches) } if (currentPatches) { applyPatches(node, currentPatches) // 對當前節點進行DOM操作 } } // 將差異的部分應用到DOM中 function applyPatches(node, currentPatches) { currentPatches.forEach((currentPatch) => { switch (currentPatch.type) { case REPLACE: var newNode = (typeof currentPatch.node === "string") ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break; case REORDER: reorderChldren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: if (node.textContent) { node.textContent = currentPatch.content } else { node.nodeValue = currentPatch.content } break default: throw new Error("Unknown patch type " + currentPatch.type) } }) }
這次的粗糙的virtual-dom基本已經實現了,具體的情況更加復雜。但這已經足夠讓我們理解virtual-dom。
具體的帶解析的代碼已經上傳到github
https://www.cnblogs.com/justa...
https://github.com/livoras/bl...
https://github.com/y8n/blog/i...
https://medium.com/@deathmood...
http://www.infoq.com/cn/artic...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/90120.html
摘要:下面我們從前端基礎和底層原理開始講起。對于和這三個對應于矢量圖位圖和圖的渲染來說,給前端開發帶來了重武器,很多小游戲也因此蓬勃發展。這篇文章受眾之大,后來被人重新整理并發布為,其中還包括中文版。 showImg(https://segmentfault.com/img/bVbjM5r?w=1142&h=640); 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! 這...
摘要:在他的重學前端課程中提到到現在為止,前端工程師已經成為研發體系中的重要崗位之一。大部分前端工程師的知識,其實都是來自于實踐和工作中零散的學習。一基礎前端工程師吃飯的家伙,深度廣度一樣都不能差。 開篇 前端開發是一個非常特殊的行業,它的歷史實際上不是很長,但是知識之繁雜,技術迭代速度之快是其他技術所不能比擬的。 winter在他的《重學前端》課程中提到: 到現在為止,前端工程師已經成為研...
摘要:在他的重學前端課程中提到到現在為止,前端工程師已經成為研發體系中的重要崗位之一。大部分前端工程師的知識,其實都是來自于實踐和工作中零散的學習。一基礎前端工程師吃飯的家伙,深度廣度一樣都不能差。開篇 前端開發是一個非常特殊的行業,它的歷史實際上不是很長,但是知識之繁雜,技術迭代速度之快是其他技術所不能比擬的。 winter在他的《重學前端》課程中提到: 到現在為止,前端工程師已經成為研發體系...
閱讀 2893·2021-11-22 09:34
閱讀 1218·2021-11-19 09:40
閱讀 3338·2021-10-14 09:43
閱讀 3571·2021-09-23 11:22
閱讀 1606·2021-08-31 09:39
閱讀 886·2019-08-30 15:55
閱讀 1417·2019-08-30 15:54
閱讀 862·2019-08-30 15:53