摘要:但是實際開發(fā)中,整個文檔樹中和標簽基本不會有太大的改動。在測試中,不難發(fā)現(xiàn)其實已經(jīng)很快了,但是速度會比較慢,所以這里留下了一個待優(yōu)化的點就是。
如何實現(xiàn) virtual-dom 0. 什么是 vnode
相信大部分前端同學之前早已無數(shù)次聽過或了解過 vnode(虛擬節(jié)點),那么什么是 vnode? vnode 應(yīng)該是什么樣的?
如果不使用前端框架,我們可能會寫出這樣的頁面:
不難發(fā)現(xiàn),整個文檔樹的根節(jié)點只有一個 html,然后嵌套各種子標簽,如果使用某種數(shù)據(jù)結(jié)構(gòu)來表示這棵樹,那么它可能是這樣。
{ tagName: "html", children: [ { tagName: "head", children: [ { tagName: "title" } ] }, { tagName: "body", children: [ { tagName: "div" }, { tagName: "script" } ] } ] }
但是實際開發(fā)中,整個文檔樹中head 和 script 標簽基本不會有太大的改動。頻繁交互可能改動的應(yīng)當是 body 里面的除 script 的部分,所以構(gòu)建 虛擬節(jié)點樹 應(yīng)當是整個 HTML 文檔樹的一個子樹,而這個子樹應(yīng)當保持和 HTML 文檔樹一致的數(shù)據(jù)結(jié)構(gòu)。它可能是這樣。
這里應(yīng)當構(gòu)建的 虛擬節(jié)點樹 應(yīng)當是 div#root 這棵子樹:
{ tagName: "div", children: [ { tagName: "div", }, { tagName: "div", }, { tagName: "div", }, ] }
到這里,vnode 的概念應(yīng)當很清晰了,vnode 是用來表示實際 dom 節(jié)點的一種數(shù)據(jù)結(jié)構(gòu),其結(jié)構(gòu)大概長這樣。
{ tagName: "div", attrs: { class: "header" }, children: [] }
一般,我們可能會這樣定義 vnode。
// vnode.js export const vnode = function vnode() {}1. 從 JSX 到 vnode
使用 React 會經(jīng)常寫 JSX,那么如何將 JSX 表示成 vnode?這里可以借助 @babel/plugin-transform-react-jsx 這個插件來自定義轉(zhuǎn)換函數(shù),
只需要在 .babelrc 中配置:
{ "plugins": [ [ "@babel/plugin-transform-react-jsx", { "pragma": "window.h" } ] ] }
然后在 window 對象上掛載一個 h 函數(shù):
// h.js const flattern = arr => [].concat.apply([], arr) window.h = function h(tagName, attrs, ...children) { const node = new vnode() node.tagName = tagName node.attrs = attrs || {} node.children = flattern(children) return node }
測試一下:
2. 渲染 vnode現(xiàn)在我們已經(jīng)知道了如何構(gòu)建 vnode,接下來就是將其渲染成真正的 dom 節(jié)點并掛載。
// 將 vnode 創(chuàng)建為真正的 dom 節(jié)點 export function createElement(vnode) { if (typeof vnode !== "object") { // 文本節(jié)點 return document.createTextNode(vnode) } const el = document.createElement(vnode.tagName) setAttributes(el, vnode.attrs) vnode.children.map(createElement).forEach(el.appendChild.bind(el)) return el } // render.js export default function render(vnode, parent) { parent = typeof parent === "string" ? document.querySelector(parent) : parent return parent.appendChild(createElement(vnode)) }
這里的邏輯主要為:
根據(jù) vnode.tagName 創(chuàng)建元素
根據(jù) vnode.attrs 設(shè)置元素的 attributes
遍歷 vnode.children 并將其創(chuàng)建為真正的元素,然后將真實子元素節(jié)點 append 到第 1 步創(chuàng)建的元素
3. diff vnode第 2 步已經(jīng)實現(xiàn)了 vnode 到 dom 節(jié)點的轉(zhuǎn)換與掛載,那么接下來某一個時刻 dom 節(jié)點發(fā)生了變化,如何更新 dom樹?顯然不能無腦卸載整棵樹,然后掛載新的樹,最好的辦法還是找出兩棵樹之間的差異,然后應(yīng)用這些差異。
在寫 diff 之前,首先要定義好,要 diff 什么,明確 diff 的返回值。比較上圖兩個 vnode,可以得出:
更換第 1、2、3 個 li 的內(nèi)容
在 ul 下創(chuàng)建兩個 li,這兩個 li 為 第 4 個和 第 5 個子節(jié)點
那么可能得返回值為:
{ "type": "UPDATE", "children": [ { "type": "UPDATE", "children": [ { "type": "REPLACE", "newVNode": 0 } ], "attrs": [] }, { "type": "UPDATE", "children": [ { "type": "REPLACE", "newVNode": 1 } ], "attrs": [] }, { "type": "UPDATE", "children": [ { "type": "REPLACE", "newVNode": 2 } ], "attrs": [] }, { "type": "CREATE", "newVNode": { "tagName": "li", "attrs": {}, "children": [ 3 ] } }, { "type": "CREATE", "newVNode": { "tagName": "li", "attrs": {}, "children": [ 4 ] } } ], "attrs": [] }
diff 的過程中,要保證節(jié)點的父節(jié)點正確,并要保證該節(jié)點在父節(jié)點 的子節(jié)點中的索引正確(保證節(jié)點內(nèi)容正確,位置正確)。diff 的核心流程:
case CREATE: 舊節(jié)點不存在,則應(yīng)當新建新節(jié)點
case REMOVE: 新節(jié)點不存在,則移出舊節(jié)點
case REPLACE: 只比較新舊節(jié)點,不比較其子元素,新舊節(jié)點標簽名或文本內(nèi)容不一致,則應(yīng)當替換舊節(jié)點
case UPDATE: 到這里,新舊節(jié)點可能只剩下 attrs 和 子節(jié)點未進行 diff,所以直接循環(huán) diffAttrs 和 diffChildren 即可
/** * diff 新舊節(jié)點差異 * @param {*} oldVNode * @param {*} newVNode */ export default function diff(oldVNode, newVNode) { if (isNull(oldVNode)) { return { type: CREATE, newVNode } } if (isNull(newVNode)) { return { type: REMOVE } } if (isDiffrentVNode(oldVNode, newVNode)) { return { type: REPLACE, newVNode } } if (newVNode.tagName) { return { type: UPDATE, children: diffVNodeChildren(oldVNode, newVNode), attrs: diffVNodeAttrs(oldVNode, newVNode) } } }4. patch 應(yīng)用更新
知道了兩棵樹之前的差異,接下來如何應(yīng)用這些更新?在文章開頭部分我們提到 dom 節(jié)點樹應(yīng)當只有一個根節(jié)點,同時 diff 算法是保證了虛擬節(jié)點的位置和父節(jié)點是與 dom 樹保持一致的,那么 patch 的入口也就很簡單了,從 虛擬節(jié)點的掛載點開始遞歸應(yīng)用更新即可。
/** * 根據(jù) diff 結(jié)果更新 dom 樹 * 這里為什么從 index = 0 開始? * 因為我們是使用樹去表示整個 dom 樹的,傳入的 parent 即為 dom 掛載點 * 從根節(jié)點的第一個節(jié)點開始應(yīng)用更新,這是與整個dom樹的結(jié)構(gòu)保持一致的 * @param {*} parent * @param {*} patches * @param {*} index */ export default function patch(parent, patches, index = 0) { if (!patches) { return } parent = typeof parent === "string" ? document.querySelector(parent) : parent const el = parent.childNodes[index] /* eslint-disable indent */ switch (patches.type) { case CREATE: { const { newVNode } = patches const newEl = createElement(newVNode) parent.appendChild(newEl) break } case REPLACE: { const { newVNode } = patches const newEl = createElement(newVNode) parent.replaceChild(newEl, el) break } case REMOVE: { parent.removeChild(el) break } case UPDATE: { const { attrs, children } = patches patchAttrs(el, attrs) for (let i = 0, len = children.length; i < len; i++) { patch(el, children[i], i) } break } } }總結(jié)
至此,vdom 的核心 diff 與 patch 都已基本實現(xiàn)。在測試 demo 中,不難發(fā)現(xiàn) diff 其實已經(jīng)很快了,但是 patch 速度會比較慢,所以這里留下了一個待優(yōu)化的點就是 patch。
本文完整代碼均在這個倉庫。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/100075.html
摘要:目錄前言問題的提出模板引擎和結(jié)合的實現(xiàn)編譯原理相關(guān)模版引擎的詞法分析語法分析與抽象語法樹代碼生成完整的結(jié)語前言本文嘗試構(gòu)建一個前端模板引擎,并且把這個引擎和進行結(jié)合。于是就構(gòu)思了一個方案,在前端模板引擎上做手腳。 作者:戴嘉華 轉(zhuǎn)載請注明出處并保留原文鏈接( https://github.com/livoras/blog/issues/14 )和作者信息。 目錄 前言 問題的提出...
摘要:具體代碼如下,,下面,我們來簡單介紹下這個排序算法檢查和中的是否擁有字段,如果沒有,直接返回的數(shù)組。通過上面這個排序算法,我們可以得到一個新的的數(shù)組。 概述 本文通過對virtual-dom的源碼進行閱讀和分析,針對Virtual DOM的結(jié)構(gòu)和相關(guān)的Diff算法進行講解,讓讀者能夠?qū)φ麄€數(shù)據(jù)結(jié)構(gòu)以及相關(guān)的Diff算法有一定的了解。 Virtual DOM中Diff算法得到的結(jié)果如何映...
摘要:記錄當前節(jié)點的標志這是當前節(jié)點的差異深度遍歷子節(jié)點對當前節(jié)點進行操作將差異的部分應(yīng)用到中這次的粗糙的基本已經(jīng)實現(xiàn)了,具體的情況更加復雜。 前言 目前廣為人知的React和Vue都采用了virtual-dom,Virtual DOM憑借其高效的diff算法,讓我們不再關(guān)心性能問題,可以隨心所欲的修改數(shù)據(jù)狀態(tài)。在實際開發(fā)中,我們并不需要關(guān)心Virtual DOM是如何實現(xiàn)的,但是理解Vir...
閱讀 3668·2021-09-22 15:15
閱讀 3569·2021-08-12 13:24
閱讀 1316·2019-08-30 15:53
閱讀 1827·2019-08-30 15:43
閱讀 1190·2019-08-29 17:04
閱讀 2801·2019-08-29 15:08
閱讀 1587·2019-08-29 13:13
閱讀 3093·2019-08-29 11:06