栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 前沿技术 > 云计算 > 虚拟化

一文带你彻底搞定Diff算法

虚拟化 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力



一、基础

Diff算法实现的是最小量更新虚拟DOM。这句话虽然简短,但是涉及到了两个核心要素:虚拟DOM、最小量更新。

1.虚拟DOM

虚拟DOM指的就是将真实的DOM树构造为js对象的形式,从而解决浏览器操作真实DOM的性能问题。

例如:如下DOM与虚拟DOM之间的映射关系



2.最小量更新

Diff的用途就是在新老虚拟DOM之间找到最小更新的部分,从而将该部分对应的DOM进行更新。

二、整个流程

Diff算法真的很美,整个流程如下图所示:



    首先比较一下新旧节点是不是同一个节点(可通过比较sel(选择器)和key(唯一标识)值是不是相同),不是同一个节点则进行暴力删除(注:先以旧节点为基准插入新节点,然后再删除旧节点)。 若是同一个节点则需要进一步比较

完全相同,不做处理

新节点内容为文本,直接替换完事

新节点有子节点,这个时候就要仔细考虑一下了:若老节点没有子元素,则直接清空老节点,将新节点的子元素插入即可;若老节点有子元素则就需要按照上述的更新策略老搞定了(记住更新策略,又可以吹好几年了,666666)。

三、实战

光说不练假把式,下面直接输出diff算法的核心内容。

3.1 patch函数

Diff算法的入口函数,主要判断新旧节点是不是同一个节点,然后交由不同的逻辑进行处理。

  1. export default function patch(oldVnode, newVnode) {     // 判断传入的第一个参数,是DOM节点还是虚拟节点 
  2.     if (oldVnode.sel === '' || oldVnode.sel === undefined) {         // 传入的第一个参数是DOM节点,此时要包装成虚拟节点 
  3.         oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);     } 
  4.      // 判断oldVnode和newVnode是不是同一个节点 
  5.     if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {         //是同一个节点,则进行精细化比较 
  6.         patchVnode(oldVnode, newVnode);     } 
  7.     else {         // 不是同一个节点,暴力插入新的,删除旧的 
  8.         let newVnodeElm = createElement(newVnode);  
  9.         // 将新节点插入到老节点之前         if (oldVnode.elm.parentNode && newVnodeElm) { 
  10.             oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);         } 
  11.         // 删除老节点         oldVnode.elm.parentNode.removeChild(oldVnode.elm); 
  12.     } } 
3.2 patchVnode函数

该函数主要负责精细化比较,通过按照上述流程图中的逻辑依次判断属于哪一个分支,从而采取不同的处理逻辑。(思路清晰,算法太牛了)

  1. export default function patchVnode(oldVnode, newVnode) {     // 判断新旧vnode是否是同一个对象 
  2.     if (oldVnode === newVnode) {         return; 
  3.     }     // 判断vnode有没有text属性 
  4.     if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {         console.log('新vnode有text属性'); 
  5.         if (newVnode.text !== oldVnode.text) {             oldVnode.elm.innerText = newVnode.text; 
  6.         }     } 
  7.     else {         // 新vnode没有text属性,有children 
  8.         console.log('新vnode没有text属性');         // 判断老的有没有children 
  9.         if (oldVnode.children !== undefined && oldVnode.children.length > 0) {             // 老的有children,新的也有children 
  10.             updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);         } 
  11.         else {             // 老的没有children,新的有children 
  12.             // 清空老的节点的内容             oldVnode.elm.innerHTML = ''; 
  13.             // 遍历新的vnode的子节点,创建DOM,上树             for (let i = 0; i < newVnode.children.length; i++) { 
  14.                 let dom = createElement(newVnode.children[i]);                 oldVnode.elm.appendChild(dom); 
  15.             }         } 
  16.     } } 
3.3 updateChildren函数

核心函数,主要负责旧虚拟节点和新虚拟节点均存在子元素的情况,按照比较策略依次进行比较,最终找出子元素中变化的部分,实现最小更新。对于该部分,涉及到一些指针,如下所示:



    旧前指的就是更新前虚拟DOM中的头部指针 旧后指的就是更新前虚拟DOM中的尾部指针 新前指的就是更新后虚拟DOM中的头部指针 新后指的就是更新后虚拟DOM中的尾部指针

按照上述的更新策略,上述旧虚拟DOM更新为新虚拟DOM的流程为:

    命中“新后旧前”策略,然后将信后对应的DOM节点(也就是节点1)移动到旧后节点(节点3)后面,然后旧前节点指针下移,新后节点指针上移。 仍然命中“新后旧前”策略,做相同的操作,将节点2移动到旧后节点(节点3)后面,下移旧前节点,上移新后节点。 命中“新前旧前”策略,DOM节点不变,旧前和新前节点均下移。 跳出循环,移动结束。
  1. export default function updateChildren(parentElm, oldCh, newCh) {     // 旧前 
  2.     let oldStartIdx = 0;     // 新前 
  3.     let newStartIdx = 0;     // 旧后 
  4.     let oldEndIdx = oldCh.length - 1;     // 新后 
  5.     let newEndIdx = newCh.length - 1;     // 旧前节点 
  6.     let oldStartVnode = oldCh[0];     // 旧后节点 
  7.     let oldEndVnode = oldCh[oldEndIdx];     // 新前节点 
  8.     let newStartVnode = newCh[0];     // 新后节点 
  9.     let newEndVnode = newCh[newEndIdx];  
  10.     let keyMap = null;  
  11.     while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {         // 略过已经加undefined标记的内容 
  12.         if (oldStartVnode == null || oldCh[oldStartIdx] === undefined) {             oldStartVnode = oldCh[++oldStartIdx]; 
  13.         }         else if (oldEndVnode == null || oldCh[oldEndIdx] === undefined) { 
  14.             oldEndVnode = oldCh[--oldEndIdx];         } 
  15.         else if (newStartVnode == null || newCh[newStartIdx] === undefined) {             newStartVnode = newCh[++newStartIdx]; 
  16.         }         else if (newEndVnode == null || newCh[newEndIdx] === undefined) { 
  17.             newEndVnode = newCh[--newEndIdx];         } 
  18.         else if (checkSameVnode(oldStartVnode, newStartVnode)) {             // 新前与旧前 
  19.             console.log('新前与旧前命中');             patchVnode(oldStartVnode, newStartVnode); 
  20.             oldStartVnode = oldCh[++oldStartIdx];             newStartVnode = newCh[++newStartIdx]; 
  21.         }         else if (checkSameVnode(oldEndVnode, newEndVnode)) { 
  22.             // 新后和旧后             console.log('新后和旧后命中'); 
  23.             patchVnode(oldEndVnode, newEndVnode);             oldEndVnode = oldCh[--oldEndIdx]; 
  24.             newEndVnode = newCh[--newEndVnode];         } 
  25.         else if (checkSameVnode(oldStartVnode, newEndVnode)) {             console.log('新后和旧前命中'); 
  26.             patchVnode(oldStartVnode, newEndVnode);             // 当新后与旧前命中的时候,此时要移动节点,移动新后指向的这个节点到老节点旧后的后面 
  27.             parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);             oldStartVnode = oldCh[++oldStartIdx]; 
  28.             newEndVnode = newCh[--newEndIdx];         } 
  29.         else if (checkSameVnode(oldEndVnode, newStartVnode)) {             // 新前和旧后 
  30.             console.log('新前和旧后命中');             patchVnode(oldEndVnode, newStartVnode); 
  31.             // 当新前和旧后命中的时候,此时要移动节点,移动新前指向的这个节点到老节点旧前的前面             parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm); 
  32.             oldEndVnode = oldCh[--oldEndIdx];             newStartVnode = newCh[++newStartIdx]; 
  33.         }         else { 
  34.             // 四种都没有命中             // 制作keyMap一个映射对象,这样就不用每次都遍历老对象了 
  35.             if (!keyMap) {                 keyMap = {}; 
  36.                 for (let i = oldStartIdx; i <= oldEndIdx; i++) {                     const key = oldCh[i].key; 
  37.                     if (key !== undefined) {                         keyMap[key] = i; 
  38.                     }                 } 
  39.             }             // 寻找当前这项(newStartIdx)在keyMap中的映射的位置序号 
  40.             const idxInOld = keyMap[newStartVnode.key];             if (idxInOld === undefined) { 
  41.                 // 如果idxInOld是undefined表示踏实全新的项,此时会将该项创建为DOM节点并插入到旧前之前                 parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm); 
  42.             }             else { 
  43.                 // 如果不是undefined,则不是全新的项,则需要移动                 const elmToMove = oldCh[idxInOld]; 
  44.                 patchVnode(elmToMove, newStartVnode);                 // 把这项设置为undefined,表示已经处理完这项了 
  45.                 oldCh[idxInOld] = undefined;                 // 移动 
  46.                 parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);             } 
  47.             // 指针下移,只移动新的头             newStartVnode = newCh[++newStartIdx]; 
  48.         }     } 
  49.      // 循环结束后,处理未处理的项 
  50.     if (newStartIdx <= newEndIdx) {         console.log('new还有剩余节点没有处理,要加项,把所有剩余的节点插入到oldStartIdx之前'); 
  51.         // 遍历新的newCh,添加到老的没有处理的之前         for (let i = newStartIdx; i <= newEndIdx; i++) { 
  52.             // insertBefore方法可以自动识别null,如果是null就会自动排到队尾去             // newCh[i]现在还没有真正的DOM,所以要调用createElement函数变为DOM 
  53.             parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);         } 
  54.     }     else if (oldStartIdx <= oldEndIdx) { 
  55.         console.log('old还有剩余节点没有处理,要删除项');         // 批量删除oldStart和oldEnd指针之间的项 
  56.         for (let i = oldStartIdx; i <= oldEndIdx; i++) {             if (oldCh[i]) { 
  57.                 parentElm.removeChild(oldCh[i].elm);             } 
  58.         }     } 

【编辑推荐】

  1. 鸿蒙官方战略合作共建——HarmonyOS技术社区
  2. 为什么说,MQ,是互联网架构的解耦神器?
  3. Prometheus告警规则管理
  4. 最高法、人社部:“996”严重违法!取消“996”,你们公司提上日程了吗?
  5. Python正面硬刚C语言,结果会怎样?
  6. CNNIC:我国已成为6G专利申请的主要来源国

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/796868.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号