虚拟DOM与DOMDIFF的原理(二)

关于虚拟DOM请看文章:点击进入任意门

什么是DOMDIFF?

DOMDIFF是用于比较两个虚拟DOM的区别,本质其实就是比较两个对象的区别(虚拟DOM本质是一个对象).

DOMDIFF的作用

根据两个虚拟对象创建补丁,这个补丁可以描述改变的内容,也就是可以描述两个虚拟DOM的差异,然后将这个补丁用来更新DOM

DOMDIFF的优化策略

  • 规则:
    • 当节点类型相同时候,去看一下属性是否相同,产生一个属性的补丁包,比如{type:‘ATTRS’,attrs:{class:‘list-group’}},这就代表变化的类型是属性,并且属性是class,变成了’list-group’。
    • 当新的DOM节点不存在时,产生一个这样的补丁包{type:‘REMOVE’,index:xxx}.这里的index是删除的节点的索引
    • 节点类型不相同,直接采用替换模式{type:‘REPLACE’,newNode:newNode}
    • 文本的变化,就用{type:‘TEXT’,text:新的文本内容}
  • 思路:

用一个{}来存放所有的patch,以下对此{}称为patch存储对象,然后根据“先序深度优先遍历”的原则,对所有节点自父向下标号,从0依次递增。这些标号作为patch存储对象的key,对应节点的patch的数组集合作为属性。然后对oldNode和newNode进行对比,有以下几种情况需要考虑:

  • 如果newNode不存在,那么就是移除了,就按照上述规则之二来处理,也即是patch为{type:‘REMOVE’,index:xxx}
  • 如果新旧节点是文本,并且不相等,那么就按照上述规则之四来处理,也即是patch为{type:‘TEXT’,text:新的文本内容}
  • 如果节点类型相同,那么就需要比较属性props是否一样,如果不一样,使用新的属性,也就是按照上述规则之一来处理,也即是patch为{type:‘ATTRS’,attrs:{class:‘list-group’}}
  • 如果都不是上述情况,则说明节点被替换了,使用上述规则之三,也即是patch为{type:‘REPLACE’,newNode:newNode}

这样进行一次“先序深度优先遍历”,会得到一个patch存储对象,然后根据这个patch存储对象,给这个真是的DOM元素打补丁,重新渲染。打补丁的方法就是:

  • 根据patch存储对象的key值,也就是那些索引,找到对应的node节点,然后根据对应key的value中的type做相应的处理:
  • 如果是ATTRS,则就设置相应的属性,当然,如果属性不存在,则移出这个属性
  • 如果是TEXT,则就对节点设置相应的文本。
  • 如果是REPLACE,则就替换这个DOM节点
  • 如果是REMOVE,则就移出这个额DOM节点

这就是DOMDIFF的原理。
具体代码如下:

  • index.js
 import {createElement,render,renderDom} from './element';

 import diff from './diff';
 import patch from './patch';
 let vertualDom1 = createElement('ul',{class:'list'},[
  createElement('li',{class:'item'},['a']),
  createElement('li',{class:'item'},['b']),
  createElement('li',{class:'item'},['c'])
 ])

 let vertualDom2 = createElement('ul',{class:'yuhua'},[
  createElement('li',{class:'item'},['1']),
  createElement('li',{class:'item'},['b']),
  createElement('div',{class:'item'},['3'])
 ])

 let el = render(vertualDom1);//这里的el就是我们的真实DOM
 renderDom(el,window.root)

 //根据这两个虚拟DOM,来创建出一个补丁对象patches
 let patches = diff(vertualDom1,vertualDom2);
 console.log(patches)

 //给元素打补丁,重新更新视图
 patch(el,patches);


 /*该方法仍然存在以下问题:
  1、如果平级元素有互换,那会导致重复渲染,其实只要互换就可以了
  2、新增节点,也不会被更新,可以通过index来解决
 */


  • element.js
 class Element{
  constructor(type,props,children){
   this.type = type;
   this.props = props;
   this.children = children;
  }
 }

 //设置属性
 function setAttr(node,key,value){//node给谁设,key设的属性,value设的值
  switch(key){//设置值的规则,如果需要添加,就在这里加就可以了
   case 'value'://node是一个input或者textarea
    if(node.tagName.toUpperCase() === 'INPUT' || node.tagName.toUpperCase() === 'TEXTAREA'){
     node.value = value;
    }else{
     node.setAttribute(key,value);
    }
   break;
   case 'style':
    node.style.cssText = value;
   default:
    node.setAttribute(key,value);
   break;
  }
  //其他还有很多情况,加对应的case就可以了
 }
 function createElement(type,props,children){
  return new Element(type,props,children);
 }
 //render方法可以将虚拟DOM转化成真实的DOM
 function render(eleObj){
  //这个eleObj就是虚拟DOM,也即是一开始打印的vertualDom结构
  let el = document.createElement(eleObj.type);
  for(let key in eleObj.props){
   //设置属性的方法
   setAttr(el,key,eleObj.props[key])
  }
  eleObj.children.forEach(child =>{
   //判断child是不是一个element
   child = (child instanceof Element) ? render(child) : document.createTextNode(child);
   el.appendChild(child);
  })
  return el;
 }

 //将DOM渲染到页面上
 function renderDom(el,target){
  target.appendChild(el);
 }
 export {createElement,render,Element,renderDom}; 
  • diff.js
 function diff(oldTree,newTree){
  //需要返回一个补丁包patches,其实pathcs就是一个对象
  let patches = {};
  let index = 0;//给树的节点加索引
  //递归树,比较后的结果放到补丁包
  walk(oldTree,newTree,index,patches)//树的遍历

  return patches;
 }
 function diffAttr(oldAttrs,newAttrs){//老的跟新的比较是否一样,不一样就放到patch里面去
  let patch = {};
  //判断老的属性和新的属性是否一样,不一样,就取新的属性值
  for(let key in oldAttrs){
   if(oldAttrs[key] !== newAttrs[key]){
    patch[key] = newAttrs[key];//有可能是undefined,比如新的已经没有这个属性了
   }
  }
  //新增属性,那么老的就没有
  for(let key in newAttrs){
   //没有的话,说明是新增的,新增的,就取新的属性跟值
   if(!oldAttrs.hasOwnProperty(key)){
    patch[key] = newAttrs[key];
   }
  }
  return patch;
 }

 const ATTRS = 'ATTRS';
 const TEXT = 'TEXT';
 const REMOVE = 'REMOVE';
 const REPLACE = 'REPLACE';
 let Index = 0;//全局的Index
 //比较子节点
 function diffChildren(oldChildren,newChildren,index,patches){
  //比较老的第一个和新的第一个
  oldChildren.forEach((child,idx) => {
   //索引不应该是index了-------
   //index 每次传递给walk时,index是递增的,所有的节点应该都基于一个Index,所以应该全局设立一个Index,在这个Index上++
   walk(child,newChildren[idx],++Index,patches)
  })
 }

 function isString(node){
  return Object.prototype.toString.call(node) === '[object String]';
  //当然这里还有数字等情况
 }

 function walk(oldNode,newNode,index,patches){
  let currentPatch = [];//每个元素都有一个补丁对象
  //如果没有新节点的情况下,也就是新节点被删除的情况下
  if(!newNode){
   currentPatch.push({
    type:REMOVE,
    index
   })
  }else if(isString(oldNode)&&isString(newNode)){//判断下是否为文本
   if(oldNode !== newNode){//如果这两个文本不一样,就改成newNode
    currentPatch.push({
     type:TEXT,
     text:newNode
    })
   }
  }else if(oldNode.type === newNode.type){
   //attrs表示哪些属性变了
   //比较属性是否有更改
   let attrs = diffAttr(oldNode.props,newNode.props);
   //如果没有更改,则attrs为{},那么我们需要考虑到这个没有更改的情况,如果有更改,才往currentPatch里放
   if(Object.keys(attrs).length){
    currentPatch.push({
     type:ATTRS,
     attrs
    })
   }
   //如果有子节点,应该遍历子节点
   diffChildren(oldNode.children,newNode.children,index,patches);
  }else{
   //说明节点被替换了
   currentPatch.push({
    type:REPLACE,
    newNode
   })
  }
  if(currentPatch.length){//当前元素确实有补丁
   patches[index] = currentPatch;
   console.log(patches)
  }
  
 }
 export default diff;
  • patch.js
 import {Element,render} from './element';
 let allPatches;
 let index = 0;//默认哪个需要打补丁
 function patch(node,patches){//node是一个真实的DOM
  allPatches = patches;
  
  walk(node);

  //给某个元素打补丁

 }

 function walk(node){
  let currentPatch = allPatches[index++];
  let childNodes = node.childNodes;
  childNodes.forEach(child => {
   walk(child);
  })
  if(currentPatch){
   //说明有这样一个补丁,有就打上补丁,打补丁的方向是倒着来的,先打2,再打1
   doPatch(node,currentPatch);
  }
 }

 //设置属性
 function setAttr(node,key,value){//node给谁设,key设的属性,value设的值
  switch(key){//设置值的规则,如果需要添加,就在这里加就可以了
   case 'value'://node是一个input或者textarea
    if(node.tagName.toUpperCase() === 'INPUT' || node.tagName.toUpperCase() === 'TEXTAREA'){
     node.value = value;
    }else{
     node.setAttribute(key,value);
    }
   break;
   case 'style':
    node.style.cssText = value;
   default:
    node.setAttribute(key,value);
   break;
  }
  //其他还有很多情况,加对应的case就可以了
 }

 function doPatch(node,patches){
  patches.forEach(patch => {
   switch(patch.type){
    case 'ATTRS':
     for(let key in patch.attrs){
      let value = patch.attrs[key];
      if(value){
       setAttr(node,key,value);
      }else{
       node.removeAttribute(key);
      }
      
     }
     break;
    case 'TEXT':
     node.textContent = patch.text;
     break;
    case 'REPLACE':
     let newNode = (patch.newNode instanceof Element) ? render(patch.newNode) : document.createTextNode(patches.newNode);
     node.parentNode.replaceChild(newNode,node);
     break;
    case 'REMOVE':
     node.parentNode.removeChild(node);
     break;
    default:
     break;
   }
  })
 }

 export default patch;

关于 先序深度优先遍历 和 先序广度优先遍历 简述一二

       A
       |
 ---------------
 |             |
 B             C
 |             |
---------     ---------
|       |     |       |
D       E     F       G

先序深度优先遍历

沿着树的深度遍历树的节点,尽可能深的搜索树的分支。例如以上树节点,遍历的顺序是ABDECFG;
深度优先是先访问根结点,然后遍历左子树接着是遍历右子树,因此我们可以利用堆栈的先进后出的特点,现将右子树压栈,再将左子树压栈,这样左子树就位于栈顶,可以保证结点的左子树先与右子树被遍历。

先序广度优先遍历

从根结点开始沿着树的宽度搜索遍历。例如以上树节点,遍历的顺序是ABCDEFG;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
虚拟DOM(Virtual DOM)是一种在前端开发中优化渲染性能的技术。它是通过在内存中构建一个轻量级的DOM树来代替真实的DOM树,然后通过比较新旧两个虚拟DOM树的差异(Diff算法),仅对差异部分进行实际的DOM操作,最终减少了浏览器重绘和回流的次数,提高了页面渲染的性能。 虚拟DOM的工作原理可以简单概括为以下几个步骤: 1. 初始化阶段:将真实的DOM树构建成一个初始的虚拟DOM树。 2. 更新阶段:当应用状态发生变化时,生成新的虚拟DOM树,并与旧的虚拟DOM树进行比较。 3. Diff算法:Diff算法是虚拟DOM的核心部分,它通过逐个节点比较新旧虚拟DOM树的差异,找出需要更新的部分。这个过程会尽量减少DOM操作的次数和范围,提高性能。 4. 执行更新:根据差异信息,对真实的DOM进行更新操作。 5. 渲染阶段:将更新后的虚拟DOM树渲染到真实的DOM中。 Diff算法是虚拟DOM性能优化的关键,常见的Diff算法有两种:深度优先遍历和双端比较深度优先遍历算法是最简单的一种Diff算法,它会递归地比较新旧虚拟DOM的节点,找出差异并更新DOM。但是这种算法在处理列表类型的节点时,性能不佳。 双端比较算法是一种更高效的Diff算法,它将新旧虚拟DOM的节点按照顺序进行比较,并将差异信息记录下来。在比较过程中,如果发现节点类型相同,则进行属性比较;如果节点类型不同,则直接替换节点。这种算法在处理列表类型的节点时,可以减少很多不必要的比较和更新操作,提高性能。 总结起来,虚拟DOMDiff算法的深入解析可以帮助我们理解前端性能优化的原理和方法,通过最小化DOM操作的次数和范围,提高页面渲染的效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值