我们先来构建两棵有差异的Virtual-DOM,模拟虚拟DOM的状态变更:
<!--旧DOM树--> <div> <div> <ul cprop="1"> <li>page1</li> <li>page2</li> <li>page3</li> </ul> </div> <div> <div>header zone</div> <div> <div fx="1">flex1</div> <div fx="2">flex2</div> </div> <div>footer zone</div> </div> </div> <!--新DOM树--> <div> <div> <ul cprop="1" ap='test'> <li bp="test">page4</li> <li>page5</li> <div>FromLiToDiv</div> </ul> </div> <div> <div>header zone</div> <div> <div fx="3">flex1</div> <div fx="2">flex2</div> </div> <div>footer zone</div> </div> </div>如果DOM-Diff算法正常工作,应该会检测出如下的区别:
1.ul标签上增加ap="test"属性 2.li第1个标签修改了文本节点内容并增加了新属性 3.第2个节点修改了内容 4.li第3个元素替换为div元素 5.flex1所在标签的fx属性值发生了变化 /*由于深度优先遍历时会按访问次序对节点增加索引代号,所以上述变化会相应转变为类似于如下标记形式*/ patches = { '2':[{type:'新增属性',propName:'ap',value:'test'}], '3':[{type:'新增属性',propName:'bp',value:'test'},{type:'修改内容',value:'page4'}], '4':[{type:'修改内容',value:'page5'}], '5':[{type:'替换元素',node:{tag:'div',.....}}] '9':[{type:'修改属性',propName:'fx',value:'3'}] } 4.2 DOM-Diff代码代码简化了判断逻辑所以不是很长,就直接写在一起实现了,方便学习,细节部分直接以注释形式写在代码中。
省略的逻辑部分主要是针对例如多个li等列表形式元素的,不仅包含标签本身的增删改,还涉及排序和元素追踪,场景较为复杂,会在后续博文中专门描述。
domdiff.js:
/** * DOM-Diff主框架 */ /** * #define定义补丁的类型 */ let PatchType = { ChangeProps: 'ChangeProps', ChangeInnerText: 'ChangeInnerText', Replace: 'Replace' } function domdiff(oldTree, newTree) { let patches = {}; //用于记录差异的补丁包 let globalIndex = 0; //遍历时为节点添加索引,方便打补丁时找到节点 dfsWalk(oldTree, newTree, globalIndex, patches);//patches会以传址的形式进行递归,所以不需要返回值 console.log(patches); return patches; } //深度优先遍历树 function dfsWalk(oldNode, newNode, index, patches) { let curPatch = []; let nextIndex = index + 1; if (!newNode) { //如果没有传入新节点则什么都不做 }else if (newNode.tag === oldNode.tag && newNode.key === oldNode.key){ //节点相同,开始判断属性(未写key时都是undefined,也是相等的) let props = diffProps(oldNode.props, newNode.props); if (props.length) { curPatch.push({type : PatchType.ChangeProps, props}); } //如果有子树则遍历子树 if (oldNode.children.length>0) { if (oldNode.children[0] instanceof Element) { //如果是子节点就递归处理 nextIndex = diffChildren(oldNode.children, newNode.children, nextIndex, patches); } else{ //否则就当做文本节点对比值 if (newNode.children[0] !== oldNode.children[0]) { curPatch.push({type : PatchType.ChangeInnerText, value:newNode.children[0]}) } } } }else{ //节点tagName或key不同 curPatch.push({type : PatchType.Replace, node: newNode}); } //将收集的变化添加至补丁包 if (curPatch.length) { if (patches[index]) { patches[index] = patches[index].concat(curPatch); }else{ patches[index] = curPatch; } } //为追踪节点索引,需要将索引返回出去 return nextIndex; } //对比节点属性 /** * 1.遍历旧序列,检查是否存在属性删除或修改 * 2.遍历新序列,检查属性新增 * 3.定义:type = DEL 删除 * type = MOD 修改 * type = NEW 新增 */ function diffProps(oldProps, newProps) { let propPatch = []; //遍历旧属性检查删除和修改 for(let prop of Object.keys(oldProps)){ //如果是节点删除 if (newProps[prop] === undefined) { propPatch.push({ type:'DEL', propName:prop }); }else{ //节点存在则判断是否有变更 if (newProps[prop] !== oldProps[prop]) { propPatch.push({ type:'MOD', propName:prop, value:newProps[prop] }); } } } //遍历新属性检查新增属性 for(let prop of Object.keys(newProps)){ if (oldProps[prop] === undefined) { propPatch.push({ type:'NEW', propName:prop, value:newProps[prop] }) } } //返回属性检查的补丁包 return propPatch; } /** * 遍历子节点 */ function diffChildren(oldChildren,newChildren,index,patches) { for(let i = 0; i < oldChildren.length; i++){ index = dfsWalk(oldChildren[i],newChildren[i],index,patches); } return index; }运行domdiff( )来对比两棵树查看结果: