Vue Diff算法

1.前言

Vue和React中都有Vnode(虚拟节点),它的作用是便于更新Dom节点,如果没有Vnode的话每更新一次数据都会做一次dom操作,极其的影响执行效率浪费资源。而用了Vnode之后,每次数据更新之后会首先在Vnode虚拟节点上进行更新,然后在通过VnodeList对dom进行操作,将dom更新集中在一起进行,也避免的很多冗余的操作,比如一个节点先更新了内容再进行了卸载,用了Vnode后就之后执行卸载这一步。

2.什么是Diff算法? 第一节我们说了Vue中会通过Vnode更新dom节点,更新的过程包括:`判断节点是否可以复用、如何复用节点、如何卸载节点、如何新增节点`。而Diff算法就是用来优化这个过程的,不同的优化过程就是不同的算法,和它的名字一样`diff(different)`找到不同并进行修改。 在了解Diff算法前,我们得来了解一下一些必须的前置变量: - `newVnodeList:`新的虚拟节点列表 - `oldVnodeList:`旧的虚拟节点列表 - `newStart/oldStart:`一个变量,值为newVnodeList/oldVnodeList当前开始的index - `newEnd/oldEnd:` 一个变量,值为newVnodeList/oldVnodeList当前结尾的index

Diff的基本步骤:

  1. 找到可以复用的节点(一般都是通过比较newVnodeList、oldVoneList中各项的key值,若key值相同则视为可复用)
  2. 更新(patch)并移动可以复用的节点
  3. 卸载(unmount)多余的节点(一般是当newVnodeList为空时,oldVnodeList多余的节点进行卸载)
  4. 新增(insert)全新的节点(一般是newVnodeList的一个节点item,item在oldVnodeList中都找不到key值相同的节点时,则视为该节点全新节点)
    Tip: 如何更新、卸载、新增、移动节点这部分知识是渲染器那部分的,和Diff算法关系不大。后面会有空更新渲染器的渲染原理。

3.Vue中的Diff算法有哪些?

Vue中共有3中Diff算法,简单Diff算法、双端Diff算法、快速Diff算法,其中快速Diff算法的性能最优,也是现在Vue3使用的Diff算法。双端Diff算法是Vue2中使用的,总的来说 快速Diff算法优于双端Diff算法,双端Diff算法优于简单Diff算法。

简单 Diff算法

找到可复用的节点

如何判断一个节点是否可以复用,简单来说就是从newVnodeList中取出一个虚拟节点V1,并遍历oldVnodeList去找和V1的key值相同的节点,若找到了则标记V1可复用,若找不到,则标记V1不可复用,进行新增操作。

在简单Diff算法中,通过使用2个for循环,来判断哪些节点可以复用

js 复制代码
    for(let i = 0; i < newVnodeList.length; i++) {
        for(let j = 0; j < oldVnodeList.length; j++) {
            if(newVnodeList[i].key == oldVnodeList[i].key){
                // 可复用
                patch(oldVnodeList[i], newVnodeList[i], container); // 执行更新操作,为后面的复用做准备
                break;
            } 
        }
    }

移动可复用的节点

拿newVnodeList中的每一项item去oldVnodeList中进行对比,找到了相同key的节点,就记录一下当前item在oldVnodeList中的index,并与maxIndex进行比较,若大于maxIndex则不需要移动,若小于则需要移动。 很好理解,就是比较2个节点的先后顺序,如果在新旧虚拟节点List中先后顺序都相同的话,就不需要移动,如果不同的话则一定需要移动。

js 复制代码
    let maxIndex = -1;
    for(let i = 0; i < newVnodeList.length; i++) {
        for(let j = 0; j < oldVnodeList.length; j++) {
            if(newVnodeList[i].key == oldVnodeList[i].key){
                // 可复用
                patch(oldVnodeList[i], newVnodeList[i], container);
                if(j > maxIndex) {
                    // 不移动节点
                    maxIndex = j;
                } else {
                    // 移动节点
                }
                break;
            } 
        }
    }

卸载多余的节点

拿oldVnodeList中的每一项item去newVnodeList中寻找,找不到就说明需要卸载。

js 复制代码
    for(let i = 0; i < oldVnodeList.length; i++) {
        if(!newVnodeList.find((item) => {
            return item.key == oldVnodeList[i].key
        })) {
            unmount(oldVnodeList[i])
        }
    }

新增全新的节点

拿newVnodeList中的每一项item去oldVnodeList中寻找,找不到就说明需要新增。

js 复制代码
    for(let i = 0; i < newVnodeList.length; i++) {
        let find = false;
        for(let j = 0; j < oldVnodeList.length; j++) {
            if(newVnodeList[i].key == oldVnodeList[i].key){
                // 可复用
                patch(oldVnodeList[i], newVnodeList[i], container); // 执行更新操作,为后面的复用做准备
                ...
                find = true;
                break;
            } 
        }
        
        if(!find) {
            // 执行新增操作
        }
    }

执行完这四项,dom节点也被更新完毕了。

双端 Diff算法

js 复制代码
    while(newStart < newEnd && oldStart < oldEnd) {
        if(nd[newStart].key == od[oldStart].key) {
            // 更新操作
           newStart++;
           oldStart++;
        } else if (nd[newEnd].key == od[oldEnd].key) {
            // 更新操作
            newEnd--;
            oldEnd--
        } else if (nd[newEnd].key == od[oldStart].key) {
            // 更新操作
            newEnd--;
            oldStart++;
        } else if (nd[newStart].key == od[oldEnd].key) {
            // 更新操作
            newStart++;
            oldEnd--;
        } else {
            // 双端都没有,就去内部找
            let find = flase;
            for(let i = oldStart + 1; i <= oldEnd; i++) {
                if(nd[newStart].key == od[i].key) { // 找到了对应节点
                    newStart--;
                    patch // 更新操作
                    insert // 进行移动
                    find = true;
                    od[i] = undefined; // 已经被移动了,此处设undefined
                }
            }
            
            if(!find) {
                patch // 新增节点,执行新增操作
            }
        }
    }
    
    if(oldEnd >= oldStart) {
        for(let i = oldStart; i <= oldEnd; i++) {
            unmount(od[i]) // 卸载对应的节点
        }
    }

快速 Diff算法

js 复制代码
 // 预处理
 while(nStart < nEnd && oStart < oEnd) {
     if(nd[nStart].key == od[oStart].key) {
         patch // 更新
         nStart++;
         oStart++;
     } else if(nd[nEnd].key == od[oEnd].key) {
         patch // 更新
         nEnd--;
         oEnd--;
     } else {
         break;
     }
 }
 
 // 开始Diff算法
 let keyObj = {}
 nd.forEach(item,index => {
     keyObj[item.key] = index;
 })
 
 
 let indexArr = new Array(nStart - nEnd + 1).fill(-1)
 
 od.forEach(item,index => {
     if(keyObj[item.key] == undefined) 
         unmount(item) // 执行卸载操作
     indexArr[keyObj[item.key] - nStart] = index;
 })
 
 let seq = findAdd(indexArr) // 找到最大递增子序列
 
 let s = seq.length - 1;
 let i = indexArr.length - 1;
 
 for(let j = i; j > 0; j--) {
     if(indexArr[j] == '-1') {
         // 新增节点
     } else if(indexArr[j] == seq[s]){
         // 不移动;
         patch;
         s--;
     ] else {
         // 该节点需要移动
         ptach;
         insert;
     }
 }
 
 

4.总结

功力不够,无法用自己的语言简单的表达处理,只能通过代码的形式输出出来,省略了很多优化的步骤,只将最核心的功能写了出来。

相关推荐
程序员爱技术1 小时前
Vue 2 + JavaScript + vue-count-to 集成案例
前端·javascript·vue.js
并不会2 小时前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
悦涵仙子2 小时前
CSS中的变量应用——:root,Sass变量,JavaScript中使用Sass变量
javascript·css·sass
衣乌安、2 小时前
【CSS】居中样式
前端·css·css3
兔老大的胡萝卜2 小时前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
低代码布道师2 小时前
CSS的三个重点
前端·css
耶啵奶膘4 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^5 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie6 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic6 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js