React系列 --- 簡單模擬語法(一)
React系列 --- Jsx, 合成事件與Refs(二)
React系列 --- virtualdom diff算法實現分析(三)
React系列 --- 從Mixin到HOC再到HOOKS(四)
React系列 --- createElement, ReactElement與Component部分源碼解析(五)
React系列 --- 從使用React瞭解Css的各類使用方案(六)
React系列 --- 從零構建狀態管理及Redux源碼解析(七)
React系列 --- 擴展狀態管理功能及Redux源碼解析(八)css
完整代碼可查看virtualdom-diffnode
經歷過PHP模板開發或者JQuery的洗禮的人都知道,它們實現從新渲染採用最簡單粗暴的辦法就是從新構建DOM替換舊DOM,問題也很明顯git
咱們先看看建立一個元素所包含的實例屬性有多少個github
const div = document.createElement('div'); let num = 0; for (let k in div) { num++; } console.log(num); // 241
而後瀏覽器根據CSS規則查找匹配節點,計算合併樣式佈局,爲了不從新計算通常瀏覽器會保存這些數據.但這是整個過程下來依然會耗費大量的內存和 CPU 資源.算法
實際也是操做Dom樹進行渲染更新,可是它只是針對修改部分進行局部渲染,將影響降到最低,雖然實現方式各有不一樣,可是大致步驟以下:segmentfault
無非就是利用Js作一層映射比較,操做簡單而且速度遠遠高於直接比較Dom樹瀏覽器
無非就是一些類型判斷,循環遍歷的簡化函數app
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 };
相關代碼能夠查看util.jsdom
我以前講JSX的時候舉過這麼個例子,而後咱們就以這個來實現效果吧函數
<div className="num" index={1}> <span>123456</span> </div>
"use strict"; React.createElement("div", { className: "num", index: 1 }, React.createElement("span", null, "123456"));
建立一個Element類
負責將Javascript對象結構
轉換爲Dom樹結構
import { isObject, isString, isArray, isNotEmptyObj, objForEach, aryForEach } from "./util"; import { NOKEY } from "./common"; class Element { constructor(tagName, props, children) { // 解析參數 this.tagName = tagName; // 字段處理,可省略參數 this.props = isObject(props) ? props : {}; this.children = children || (!isNotEmptyObj(this.props) && ((isString(props) && [props]) || (isArray(props) && props))) || []; // 不管void後的表達式是什麼,void操做符都會返回undefined this.key = props ? props.key : void NOKEY; // 計算節點數 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() { // 根據tagName構建 const dom = document.createElement(this.tagName); // 設置props objForEach(this.props, propName => dom.setAttribute(propName, this.props[propName]) ); // 渲染children aryForEach(this.children, child => { const childDom = child instanceof Element ? child.render() // 若是子節點也是虛擬DOM,遞歸構建DOM節點 : document.createTextNode(child); // 若是字符串,只構建文本節點 dom.appendChild(childDom); }); return dom; } } // 改變傳參方式,免去手動實例化 export default function CreateElement(tagName, props, children) { return new Element( tagName, props, children ); }
新建示例,調用方式以下
// 1. 構建虛擬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構建真正的DOM const root = tree.render(); document.body.appendChild(root);
運行以後能正常得出結果了,那麼第一步驟算是完成了,具體還有更多不一樣類型標籤,對應事件狀態先略過.
界面如圖
Javascript結構如圖
結構原型以下
相關代碼能夠查看element.js
這是整個實現裏面最關鍵的一步,由於這決定了計算的速度和操做Dom的數量
咱們建立新的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結構如圖
傳統 diff 算法的複雜度爲 O(n^3),可是通常Dom跨層級的狀況是很是少見的。因此React 只針對同層級Dom節點作比較,將 O(n^3) 複雜度的問題轉換成 O(n) 複雜度的問題。
比較大的問題就是當節點跨層級移動並不會進行移動而是直接替換整個節點,因此切記這點性能問題
shouldComponentUpdate
決定是否更新儘量將動態組件往底層節點遷移,有利於提升性能
元素操做無非就是幾種,咱們定義幾個類型作狀態標記
const REPLACE = "replace"; const REORDER = "reorder"; const PROPS = "props"; const TEXT = "text"; const NOKEY = "no_key" export { REPLACE, REORDER, PROPS, TEXT, NOKEY }
其中NOKEY
就是專門給那些沒有定義key的組件作默認,React對同一層級的同組子節點,添加惟一 key 進行區分進行位移而不是直接替換,這點對於總體性能尤其關鍵
咱們首先針對不一樣類型作些區分處理
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) { // 節點位置 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 }); // 同種標籤而且key相同 } else if (oNode.tagName === nNode.tagName && oNode.key === nNode.key) { // 至少一方有值 if (isNotEmptyObj(oNode.props) || isNotEmptyObj(nNode.props)) { // 計算props結果 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 }); } // 最終對比結果 currentPatch.length && (patches[index] = currentPatch); }
新舊節點的props屬性比較
/** * * @param {舊節點} oNode * @param {新節點} nNode */ function diffProps(oNode, nNode) { let isChange = false; const oProps = oNode.props; const nProps = nNode.props; // 節點屬性記錄 const propsPatched = {}; // 替換/新增屬性 objForEach(oProps, key => { if (nProps[key] !== oProps[key] || !oProps.hasOwnProperty(key)) { !isChange && (isChange = true); propsPatched[key] = nProps[key]; } }); return !isChange ? null : propsPatched; }
新舊節點的子元素對比
/** * 同級對比 * @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,能經過關鍵屬性得到最小移動量,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.
調用對比方式
// 4. 比較兩棵虛擬DOM樹的不一樣 const patches = diff(tree, newTree);
得出差別以下
相關代碼能夠查看diff.js
進行深度遍歷
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); }
針對不一樣標誌作對應處理
// 更新類型 function applyPatches(node, currentPatches) { aryForEach(currentPatches, item => { switch (item.type) { case REPLACE: const nNode = isString(item.node) ? document.createTextNode(item.node) : item.node.render(); node.parentNode.replaceChild(nNode, node); break; case REORDER: reorderChildren(node, item.moves); break; case PROPS: setProps(node, item.props); break; case TEXT: if (node.textContent) { // 使用純文本 node.textContent = item.content; } else { // 僅僅對CDATA片斷,註釋comment,Processing Instruction節點或text節點有效 node.nodeValue = item.content; } break; default: throw new Error("Unknown patch type " + item.type); } }); }
先說簡單的屬性替換
// 修改屬性 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) { // 找到對應節點刪除 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]) { // 刪除並返回節點 insertNode = node.removeChild(maps[move.item.key]); // 獲取刪除節點位置 staticNodeList.splice(Array.prototype.indexOf.call(node.childNodes, maps[move.item.key]), 1); } else { // 建立節點 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;
當這一步完成之後咱們能夠直接應用查看效果
// 4. 比較兩棵虛擬DOM樹的不一樣 const patches = diff(tree, newTree); // 5. 在真正的DOM元素上應用變動 patch(root, patches);
結果如圖
相關代碼能夠查看patch.js