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