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.总结

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

相关推荐
一路向前的月光40 分钟前
react(9)-redux
前端·javascript·react.js
大数据追光猿1 小时前
Python中的Flask深入认知&搭建前端页面?
前端·css·python·前端框架·flask·html5
莫忘初心丶1 小时前
python flask 使用教程 快速搭建一个 Web 应用
前端·python·flask
xw52 小时前
Trae初体验
前端·trae
横冲直撞de2 小时前
前端接收后端19位数字参数,精度丢失的问题
前端
我是哈哈hh2 小时前
【JavaScript进阶】作用域&解构&箭头函数
开发语言·前端·javascript·html
摸鱼大侠想挣钱2 小时前
ActiveX控件
前端
谢尔登2 小时前
Vue 和 React 响应式的区别
前端·vue.js·react.js
后端小肥肠2 小时前
【AI编程】Java程序员如何用Cursor 3小时搞定CAS单点登录前端集成
前端·后端·cursor
酷酷的阿云2 小时前
Vue3性能优化必杀技:useDebounce+useThrottle+useLazyLoad深度剖析
前端·javascript·vue.js