手动实现一个虚拟DOM算法
发现一个好文:《深度剖析:如何实现一个 Virtual DOM 算法》 源码
文章写得非常详细,仔细看了一遍代码,加了一些注释。其实还有有一些地方看的不是很懂(毕竟我菜qaq 先码 有时间研究下diff算法
util.js
/** * 工具..类? */ var _ = exports /** * 获取一个对象的类型 * 匹配 '[object\s' (\s 是空白字符) 或 ']' 并替换为空 * 也就是可以将 [object Array] 变为 Array * @param {Object} obj */ _.type = function(obj) { return Object.prototype.toString.call(obj).replace(/\[object\s|\]/g, '') } /** * 判断一个对象是否是数组 * @param {Object} list */ _.isArray = function isArray(list) { return _.type(list) === 'Array' } /** * 判断一个对象是否为 String */ _.isString = function isString(list) { return _.type(list) === 'String' } /** * 用于将 类数组对象 变为数组 比如 nodeList, argument 等带有 length 属性的对象 * @param {*} arrayLike * @param {int} index 从第几个元素开始 */ _.slice = function slice(arrayLike, index) { return Array.prototype.slice.call(arrayLike, index) } /** * 获取 value 表达式的布尔值 * @param {*} value */ _.truthy = function truthy(value) { return !!value } /** * 对数组中每一个元素执行 fn (相当于map? * @param {*} array * @param {*} fn */ _.each = function each(array, fn) { for (var i = 0, len = array.length; i < len; i++) { fn(array[i], i) } } /** * 为 DOM 节点设置属性 */ _.setAttr = function(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 { node.setAttribute(key, value) } break default: node.setAttribute(key, value) break } } /** * 将类数组类型转化为数组类型 * @param {Object} listLike * ( 和 slice 有什么区别呢????? */ _.toArray = function toArray(listLike) { if (!listLike) return [] var list = [] for (var i = 0, len = listLike.length; i < len; i++) { list.push(listLike[i]) } return list }
element.js
var _ = require('./util') /** * 用来表示虚拟 DOM 节点的数据结构 * @param {String} tagName 节点类型 * @param {Object} props 节点属性 键值对形式 可以选填 * @param {Array<Element|String>} children 节点的子元素 或者文本 * @example Element('div', {'id': 'container'}, [Element('p', ['the count is :' + count])]) */ function Element(tagName, props, children) { // var e = Element(tagName, props, children) // 并不会让 e instanceof Element 为 true 要加 new 关键字才可以哦 if (!(this instanceof Element)) { // 如果 children 不是数组且不为空 就把第三个参数以及后面的参数都作为 children if (!_.isArray(children) && children != null) { // children 去掉非空子元素 children = _.slice(arguments, 2).filter(_.truthy) } return new Element(tagName, props, children) } // 如果属性是数组类型 证明没有传属性 第二个参数就是 children if (_.isArray(props)) { children = props props = {} } this.tagName = tagName this.props = props || {} this.children = children || [] // void后面跟一个表达式 void操作符会立即执行后面的表达式 并且统一返回undefined // 可以为节点添加一个属性 key 以便重新排序的时候 判断节点位置的变化 this.key = props ? props.key : void 0 // count 统计不包含文本元素 一共有多少子元素 var count = 0 _.each(this.children, function(child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) this.count = count } /** * 将虚拟DOM 渲染成真实的DOM元素 */ Element.prototype.render = function() { // 根据 tag 创建元素 var el = document.createElement(this.tagName) var props = this.props // 为元素添加属性 for (var propName in props) { var propValue = props[propName] _.setAttr(el, propName, propValue) } // 先渲染子节点 然后添加到当前节点 _.each(this.children, function(child) { var childEl = (child instanceof Element) ? child.render() : document.createTextNode(child) el.appendChild(childEl) }) return el } module.exports = Element
diff.js
var _ = require('./util') var patch = require('./patch.js') var listDiff = require('list-diff2') /** * 统计更新前后 DOM 树的改变 * @param {Element} oldTree 更新前 DOM 树 * @param {Element} newTree 更新后 DOM 树 */ function diff(oldTree, newTree) { var index = 0 var patches = {} dfsWalk(oldTree, newTree, index, patches) return patches } /** * dfs 遍历新旧 DOM 树 * patches 记录差异 */ function dfsWalk(oldNode, newNode, index, patches) { var currentPatch = [] if (newNode === null) { // 如果该节点被删除 不需要做任何事情 } else if (_.isString(oldNode) && _.isString(newNode)) { // 如果改变前后该节点都是文本类型 if (newNode !== oldNode) { currentPatch.push({ type: patch.TEXT, content: newNode }) } } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) { // 当节点的类型以及key都相同的时候 判断两个节点的属性是否有变化 var propsPatches = diffProps(oldNode, newNode) if (propsPatches) { currentPatch.push({ type: patch.PROPS, props: propsPatches }) } // 当新节点包含ignore属性的时候 不比较其子节点 // (也就是说 如果子节树不会有变化的话 手动添加 ignore 属性来防止比较子节点降低效率??? if (!isIgnoreChildren(newNode)) { diffChildren(oldNode.children, newNode.children, index, patches, currentPatch) } } else { // 节点的类型不同 直接替换 currentPatch.push({ type: patch.REPLACE, node: newNode }) } if (currentPatch.length) { patches[index] = currentPatch } } /** * 比较两个元素的子节点列表 * @param {Array<Element|String>} oldChildren * @param {Array<Element|String>} newChildren */ function diffChildren(oldChildren, newChildren, index, patches, currentPatch) { // 此处未实现 diff 算法 直接引用 list-diff2 的 listDiff 函数 var diffs = listDiff(oldChildren, newChildren, 'key') newChildren = diffs.children // 如果有移动 就为当前节点标记改变 if (diffs.moves.length) { // diffs.moves 记录节点的移动顺序 var reorderPatch = { type: patch.RECORDER, moves: diffs.moves } currentPatch.push(recorderPatch) } // leftNode 记录的是前一个子节点 根据dfs遍历的顺序为每个节点标号(index var leftNode = null var currentNodeIndex = index _.each(oldChildren, function(child, i) { // 对于每一个子节点 进行比较 var newChild = newChildren[i] currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1 dfsWalk(child, newChild, currentNodeIndex, patches) leftNode = child }) } /** * 比较新旧节点的属性变化 */ function diffProps(oldNode, newNode) { var count = 0 var oldProps = oldNode.props var newProps = newNode.props var key, value var propsPatches = {} // 记录写与原节点相比 值改变的属性 for (key in oldProps) { value = oldProps[key] if (newProps[key] !== value) { count++ propsPatches[key] = newProps[key] } } // 记录之前不存在的属性 for (key in newProps) { value = newProps[key] if (!oldProps.hasOwnProperty(key)) { count++ propsPatches[key] = newProps[key] } } // 改变前后节点属性完全相同 返回 null if (count === 0) return null return propsPatches } function isIgnoreChildren(node) { return (node.props && node.props.hasOwnProperty('ignore')) } module.exports = diff
patch.js
/** * 根据改变前后节点的差异 渲染页面 */ var _ = require('./util') var REPLACE = 0 // 替换元素 var REORDER = 1 // 移动 删除 新增 子节点 var PROPS = 2 // 修改节点属性 var TEXT = 3 // 修改文本内容 /** * * @param {element} node 改变之前的渲染结果 * @param {Object} patches 通过 diff 计算出的差异集合 */ function patch(node, patches) { var walker = { index: 0 } dfsWalk(node, walker, patches) } /** * dfs 遍历dom树 根据旧节点和patches渲染新节点 * @param {element} node 更改之前的 dom 元素 * @param {*} walker 记录走到第几个节点(so...为什么不直接传index... * @param {Object} 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) } } function applyPatches(node, currentPatches) { // 根据差异类型的不同 进行不同的渲染 _.each(currentPatches, function(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: // 子节点重新排序 reorderChildren(node, currentPatch.moves) break case PROPS: // 重新设置属性 setProps(node, currentPatch.props) break case TEXT: // 改变文本值 if (node.textContent) { node.textContent = currentPatch.content } else { // IE node.nodeValue = currentPatch.content } break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) } /** * 为节点重新设置属性 属性值为undefined表示该属性被删除了 * @param {element} node * @param {Object} props */ function setProps(node, props) { for (var key in props) { // 所以到底为什么不使用 undefined // undefined 并不是保留词(reserved word),它只是全局对象的一个属性,在低版本 IE 中能被重写 if (props[key] === void 0) { node.removeAttribute(key) } else { var value = props[key] _.setAttr(node, key, value) } } } /** * 将节点根据moves重新排序 * @param {element} node DOM元素 * @param {Obejct} moves diff算法根据新旧子树以及key算出的移动顺序 */ function reorderChildren(node, moves) { var staticNodeList = _.toArray(node.childNodes) var maps = {} _.each(staticNodeList, function(node) { // nodeType 属性返回以数字值返回指定节点的节点类型。 // nodeType === 1 表示 元素element if (node.nodeType === 1) { var key = node.getAttribute('key') if (key) { maps[key] = node } } }) _.each(moves, function(move) { var index = move.index if (move.type === 0) { // 删除节点 if (staticNodeList[index] === node.childNodes[index]) { node.removeChild(node.childNodes[index]) } // splice() 方法可删除从 index 处开始的零个或多个元素,并且用参数列表中声明的一个或多个值来替换那些被删除的元素。 // arrayObject.splice(index,howmany,item1,.....,itemX) staticNodeList.splice(index, 1) } else if (move.type === 1) { // 新增节点 如果之前就存在相同的key 就将之前的拷贝 否则创建新节点 // cloneNode() 创建节点的拷贝 并返回该副本 参数为true表示深拷贝 var insertNode = maps[move.item.key] ? maps[move.item.key].cloneNode(true) : ( (typeof move.item === 'object') ? move.item.render() : document.createTextNode(move.item)) staticNodeList.splice(index, 0, insertNode) node.insertBefore(insertNode, node.childNodes[index] || null) } }) } patch.REPLACE = REPLACE patch.REORDER = REORDER patch.PROPS = PROPS patch.TEXT = TEXT module.exports = patch