React系列
React簡單模擬語法(一)
Jsx, 合成事件與Refs(二)
virtualdom diff算法實(shí)現(xiàn)分析(三)
從Mixin到HOC再到HOOKS(四)
createElement, ReactElement與Component部分源碼解析(五)
完整代碼可查看virtualdom-diff
渲染DOM
經(jīng)歷過PHP模板開發(fā)或者JQuery的洗禮的人都知道,它們實(shí)現(xiàn)重新渲染采用最簡單粗暴的辦法就是重新構(gòu)建DOM替換舊DOM,問題也很明顯
- 性能消耗高
- 無法保存狀態(tài)(聚焦,滾動等)
我們先看看創(chuàng)建一個元素所包含的實(shí)例屬性有多少個
const div = document.createElement('div'); let num = 0; for (let k in div) { num++; } console.log(num); // 241
然后瀏覽器根據(jù)CSS規(guī)則查找匹配節(jié)點(diǎn),計(jì)算合并樣式布局,為了避免重新計(jì)算一般瀏覽器會保存這些數(shù)據(jù).但這是整個過程下來依然會耗費(fèi)大量的內(nèi)存和 CPU 資源.
Virtual DOM
實(shí)際也是操作Dom樹進(jìn)行渲染更新,但是它只是針對修改部分進(jìn)行局部渲染,將影響降到最低,雖然實(shí)現(xiàn)方式各有不同,但是大體步驟如下:
- 用Javascript對象結(jié)構(gòu)描述Dom樹結(jié)構(gòu),然后用它來構(gòu)建真正的Dom樹插入文檔
- 當(dāng)狀態(tài)發(fā)生改變之后,重新構(gòu)造新的Javascript對象結(jié)構(gòu)和舊的作對比得出差異
- 針對差異之處進(jìn)行重新構(gòu)建更新視圖
無非就是利用Js做一層映射比較,操作簡單并且速度遠(yuǎn)遠(yuǎn)高于直接比較Dom樹
基礎(chǔ)工具函數(shù)
無非就是一些類型判斷,循環(huán)遍歷的簡化函數(shù)
function type(obj) { return Object.prototype.toString.call(obj).replace(/\[object\s|\]/g, ""); } function isArray(list) { return type(list) === "Array"; } function isObject(obj) { return type(obj) === "Object"; } function isString(str) { return type(str) === "String"; } function isNotEmptyObj(obj) { return isObject(obj) && JSON.stringify(obj) != "{}"; } function objForEach(obj, fn) { isNotEmptyObj(obj) && Object.keys(obj).forEach(fn); } function aryForEach(ary, fn) { ary.length && ary.forEach(fn); } function setAttr(node, key, value) { switch (key) { case "style": node.style.cssText = value; break; case "value": var tagName = node.tagName || ""; tagName = tagName.toLowerCase(); if (tagName === "input" || tagName === "textarea") { node.value = value; } else { // if it is not a input or textarea, use `setAttribute` to set node.setAttribute(key, value); } break; default: node.setAttribute(key, value); break; } } function toArray(data) { if (!data) { return []; } const ary = []; aryForEach(data, item => { ary.push(item); }); return ary; } export { isArray, isObject, isString, isNotEmptyObj, objForEach, aryForEach, setAttr, toArray };
相關(guān)代碼可以查看util.js
Javascript對象結(jié)構(gòu)描述
我之前講JSX的時候舉過這么個例子,然后我們就以這個來實(shí)現(xiàn)效果吧
123456
"use strict"; React.createElement("div", { className: "num", index: 1 }, React.createElement("span", null, "123456"));
創(chuàng)建一個Element類
負(fù)責(zé)將Javascript對象結(jié)構(gòu)
轉(zhuǎn)換為Dom樹結(jié)構(gòu)
import { isObject, isString, isArray, isNotEmptyObj, objForEach, aryForEach } from "./util"; import { NOKEY } from "./common"; class Element { constructor(tagName, props, children) { // 解析參數(shù) this.tagName = tagName; // 字段處理,可省略參數(shù) this.props = isObject(props) ? props : {}; this.children = children || (!isNotEmptyObj(this.props) && ((isString(props) && [props]) || (isArray(props) && props))) || []; // 無論void后的表達(dá)式是什么,void操作符都會返回undefined this.key = props ? props.key : void NOKEY; // 計(jì)算節(jié)點(diǎn)數(shù) let count = 0; aryForEach(this.children, (item, index) => { if (item instanceof Element) { count += item.count; } else { this.children[index] = "" + item; } count++; }); this.count = count; } render() { // 根據(jù)tagName構(gòu)建 const dom = document.createElement(this.tagName); // 設(shè)置props objForEach(this.props, propName => dom.setAttribute(propName, this.props[propName]) ); // 渲染children aryForEach(this.children, child => { const childDom = child instanceof Element ? child.render() // 如果子節(jié)點(diǎn)也是虛擬DOM,遞歸構(gòu)建DOM節(jié)點(diǎn) : document.createTextNode(child); // 如果字符串,只構(gòu)建文本節(jié)點(diǎn) dom.appendChild(childDom); }); return dom; } } // 改變傳參方式,免去手動實(shí)例化 export default function CreateElement(tagName, props, children) { return new Element( tagName, props, children ); }
新建示例,調(diào)用方式如下
// 1. 構(gòu)建虛擬DOM const tree = createElement("div", { id: "root" }, [ createElement("h1", { style: "color: blue" }, ["Tittle1"]), createElement("p", ["Hello, virtual-dom"]), createElement("ul", [ createElement("li", { key: 1 }, ["li1"]), createElement("li", { key: 2 }, ["li2"]), createElement("li", { key: 3 }, ["li3"]), createElement("li", { key: 4 }, ["li4"]) ]) ]); // 2. 通過虛擬DOM構(gòu)建真正的DOM const root = tree.render(); document.body.appendChild(root);
運(yùn)行之后能正常得出結(jié)果了,那么第一步驟算是完成了,具體還有更多不同類型標(biāo)簽,對應(yīng)事件狀態(tài)先略過.
界面如圖
Javascript結(jié)構(gòu)如圖
結(jié)構(gòu)原型如下
相關(guān)代碼可以查看element.js
diff算法
這是整個實(shí)現(xiàn)里面最關(guān)鍵的一步,因?yàn)檫@決定了計(jì)算的速度和操作Dom的數(shù)量
我們創(chuàng)建新的Dom樹作對比
// 3. 生成新的虛擬DOM const newTree = createElement("div", { id: "container" }, [ createElement("h1", { style: "color: red" }, ["Title2"]), createElement("h3", ["Hello, virtual-dom"]), createElement("ul", [ createElement("li", { key: 3 }, ["li3"]), createElement("li", { key: 1 }, ["li1"]), createElement("li", { key: 2 }, ["li2"]), createElement("li", { key: 5 }, ["li5"]) ]) ]);
Javascript結(jié)構(gòu)如圖
tree diff
傳統(tǒng) diff 算法的復(fù)雜度為 O(n^3),但是一般Dom跨層級的情況是非常少見的。愛掏網(wǎng) - it200.com所以React 只針對同層級Dom節(jié)點(diǎn)做比較,將 O(n^3) 復(fù)雜度的問題轉(zhuǎn)換成 O(n) 復(fù)雜度的問題。愛掏網(wǎng) - it200.com
比較大的問題就是當(dāng)節(jié)點(diǎn)跨層級移動并不會進(jìn)行移動而是直接替換整個節(jié)點(diǎn),所以切記這點(diǎn)性能問題
component diff
- 某個組件發(fā)生變化,會導(dǎo)致自其從上往下整體替換
- 同一類型組件會進(jìn)行Virtual DOM進(jìn)行比較
- React提供了一個
shouldComponentUpdate
決定是否更新
盡可能將動態(tài)組件往底層節(jié)點(diǎn)遷移,有利于提高性能
element diff
元素操作無非就是幾種,我們定義幾個類型做狀態(tài)標(biāo)記
const REPLACE = "replace"; const REORDER = "reorder"; const PROPS = "props"; const TEXT = "text"; const NOKEY = "no_key" export { REPLACE, REORDER, PROPS, TEXT, NOKEY }
其中NOKEY就是專門給那些沒有定義key的組件做默認(rèn),React對同一層級的同組子節(jié)點(diǎn),添加唯一 key 進(jìn)行區(qū)分進(jìn)行位移而不是直接替換,這點(diǎn)對于整體性能尤為關(guān)鍵
我們首先針對不同類型做些區(qū)分處理
import { isString, objForEach, aryForEach, isNotEmptyObj } from "./util"; import { REPLACE, REORDER, PROPS, TEXT } from "./common"; import listDiff from "list-diff2"; /** * * @param {舊Dom樹} oTree * @param {新Dom樹} nTree * 返回差異記錄 */ function diff(oTree, nTree) { // 節(jié)點(diǎn)位置 let index = 0; // 差異記錄 const patches = {}; dfsWalk(oTree, nTree, index, patches); return patches; } function dfsWalk(oNode, nNode, index, patches) { const currentPatch = []; // 首次渲染 if (nNode === null) return; // 都是字符串形式并且不相同直接替換文字 if (isString(oNode) && isString(nNode)) { oNode !== nNode && currentPatch.push({ type: TEXT, content: nNode }); // 同種標(biāo)簽并且key相同 } else if (oNode.tagName === nNode.tagName && oNode.key === nNode.key) { // 至少一方有值 if (isNotEmptyObj(oNode.props) || isNotEmptyObj(nNode.props)) { // 計(jì)算props結(jié)果 const propsPatches = diffProps(oNode, nNode); // 有差異則重新排序 propsPatches && currentPatch.push({ type: PROPS, props: propsPatches }); } // children對比 if ( !(!isNotEmptyObj(nNode.props) && nNode.props.hasOwnProperty("ignore")) ) { (oNode.children.length || nNode.children.length) && diffChildren( oNode.children, nNode.children, index, patches, currentPatch ); } } else { // 都不符合上面情況就直接替換 currentPatch.push({ type: REPLACE, node: nNode }); } // 最終對比結(jié)果 currentPatch.length && (patches[index] = currentPatch); }
新舊節(jié)點(diǎn)的props屬性比較
/** * * @param {舊節(jié)點(diǎn)} oNode * @param {新節(jié)點(diǎn)} nNode */ function diffProps(oNode, nNode) { let isChange = false; const oProps = oNode.props; const nProps = nNode.props; // 節(jié)點(diǎn)屬性記錄 const propsPatched = {}; // 替換/新增屬性 objForEach(oProps, key => { if (nProps[key] !== oProps[key] || !oProps.hasOwnProperty(key)) { !isChange && (isChange = true); propsPatched[key] = nProps[key]; } }); return !isChange "code" >/** * 同級對比 * @param {*} oChildren * @param {*} nChildren * @param {*} index * @param {*} patches * @param {*} currentPatch */ function diffChildren(oChildren, nChildren, index, patches, currentPatch) { // 得出相對簡化移動路徑 const diffs = listDiff(oChildren, nChildren, "key"); // 保留元素 nChildren = diffs.children; // 記錄排序位移 diffs.moves.length && currentPatch.push({ type: REORDER, moves: diffs.moves }); // 深度遍歷 let leftNode = null; let currentNodeIndex = index; aryForEach(oChildren, (_item, _index) => { const nChild = nChildren[_index]; currentNodeIndex = leftNode && leftNode.count ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1; _item !== nChild && dfsWalk(_item, nChild, currentNodeIndex, patches); leftNode = _item; }); }
深度遍歷的原型圖如下
其中的listDiff來自于list-diff,能通過關(guān)鍵屬性獲得最小移動量,moves就是給第三步更新視圖做鋪墊指示,官方介紹如下
Diff two lists in time O(n). I The algorithm finding the minimal amount of moves is Levenshtein distance which is O(n*m). This algorithm is not the best but is enougth for front-end DOM list manipulation.
This project is mostly influenced by virtual-dom algorithm.
調(diào)用對比方式
// 4. 比較兩棵虛擬DOM樹的不同 const patches = diff(tree, newTree);
得出差異如下
相關(guān)代碼可以查看diff.js
更新視圖
進(jìn)行深度遍歷
import { isString, isObject, objForEach, aryForEach, setAttr, toArray } from "./util"; import { REPLACE, REORDER, PROPS, TEXT, NOKEY } from "./common"; function patch(node, patches) { const walker = { index: 0 }; dfsWalk(node, walker, patches); } // 深度遍歷更新 function dfsWalk(node, walker, patches) { const currentPatches = patches[walker.index]; node.childNodes && aryForEach(node.childNodes, item => { walker.index++; dfsWalk(item, walker, patches); }); currentPatches && applyPatches(node, currentPatches); }
針對不同標(biāo)志做對應(yīng)處理
// 更新類型 function applyPatches(node, currentPatches) { aryForEach(currentPatches, item => { switch (item.type) { case REPLACE: const nNode = isString(item.node) "code" >// 修改屬性 function setProps(node, props) { objForEach(props, key => { if (props[key] === void NOKEY) { node.removeAttribute(key); } else { setAttr(node, key, props[key]); } }); }
最后就是列表渲染
// 列表排序渲染 function reorderChildren(node, moves) { const staticNodeList = toArray(node.childNodes); const maps = {}; aryForEach(staticNodeList, node => { // Element if (node.nodeType === 1) { const key = node.getAttribute("key"); key && (maps[key] = node); } }); aryForEach(moves, move => { const index = move.index; // 0:刪除 1:替換 if (move.type === 0) { // 找到對應(yīng)節(jié)點(diǎn)刪除 staticNodeList[index] === node.childNodes[index] && node.removeChild(node.childNodes[index]); staticNodeList.splice(index, 1); } else if (move.type === 1) { let insertNode; if (maps[move.item.key]) { // 刪除并返回節(jié)點(diǎn) insertNode = node.removeChild(maps[move.item.key]); // 獲取刪除節(jié)點(diǎn)位置 staticNodeList.splice(Array.prototype.indexOf.call(node.childNodes, maps[move.item.key]), 1); } else { // 創(chuàng)建節(jié)點(diǎn) insertNode = isObject(move.item) ? move.item.render() : document.createTextNode(move.item); } // 同步staticNodeList信息 staticNodeList.splice(index, 0, insertNode); // 操作Dom node.insertBefore(insertNode, node.childNodes[index] || null); } }); } export default patch;
當(dāng)這一步完成以后我們可以直接應(yīng)用查看效果
// 4. 比較兩棵虛擬DOM樹的不同 const patches = diff(tree, newTree); // 5. 在真正的DOM元素上應(yīng)用變更 patch(root, patches);
結(jié)果如圖
相關(guān)代碼可以查看patch.js
參考
深度剖析:如何實(shí)現(xiàn)一個 Virtual DOM 算法