vue源码学习笔记二-diff算法

139 阅读10分钟

前言

在面试的时候经常被问到关于vue2源码的知识点,其中diff算法频率相对较高,本文较长,请慢慢看

diff算法

diff的原理就是当前的真实的dom生成一颗virtual DOM也就是虚拟DOM,当虚拟DOM的某个节点的数据发生改变会生成一个新的Vnode, 通过newVnode和oldVnode对比,发现有不同,直接修改在真实DOM上

snabbdom

一个虚拟的DOM库专注于简化,模块化拥有强大的功能和性能

上手

import {
  init,
  classModule, // 类模块
  propsModule, // 属性模块
  styleModule, // 样式模块
  eventListenersModule, // 事件模块
  h, // 虚拟节点函数
  toVNode,
} from "snabbdom";

// 创建patch函数,初始化模块
let patch = init([classModule, propsModule, styleModule, eventListenersModule])
let vnode1 = h('a', {
  props: {
    href: 'http://www.baidu.com',
    target: '_banlk'
  }
}, '百度');
/*
<div id="box">
        <h3>小米的爱好</h3>
        <ul>
            <li>篮球</li>
            <li>足球</li>
            <li>乒乓球</li>
        </ul>
    </div>
*/ 
let vnode2 = h(
  'div', {
    props: {
      id: "box",
    }
  },
  [
    h("h3",{}, "小米的爱好"),
    h('ul',{},[
      h("li",{},'篮球'),
      h("li",{},'足球'),
      h("li",{},'乒乓球'),
    ])
  ]
)
console.log(vnode2)
let container = document.getElementById("container");
// 让虚拟节点上树
patch(container, vnode2)

5.png

真实dom - 虚拟dom

// 真实dom
<div class="holidays">
      <h3>今天是周末</h3>
      <ul>
        <li>吃饭</li>
        <li>睡觉</li>
        <li>看电影</li>
      </ul>
    </div>

// 虚拟dom
{
    "sel": "div",
    "data": {
      "class": { "holidays": true },
    },
    "children": [
      {
        "sel": "h3",
        "data": {},
        "text": "今天是周末",
      },
      {
        "sel": "ul",
        "data": {},
        "children": [
          { "sel": "li", "data": {}, "text": "吃饭" },
          { "sel": "li", "data": {}, "text": "睡觉" },
          { "sel": "li", "data": {}, "text": "看电影" },
        ],
      },
    ],
};

新虚拟的dom和老虚拟的dom进行diff,算出应该如何最小量更新,最后反映到真正的dom上

9.png

h函数

h函数用来产生虚拟节点(vnode)

// 调用h函数
h('a',{props:{href:'http://www.baidu.com'}},'百度');

// 得到的虚拟节点
 {"sel":"a","data":{props:{href:'http://www.baidu.com'}},"text":"百度"}

// 真实的dom节点
<a href="http://www.baidu.com">百度</a>

虚拟节点的属性

{
  children: undefined, // 是否有子元素  
 data: { // 属性,如class  href   id   等 
    props:{
      ...
    }
  },   
  elm: undefined, // elm为虚拟节点所对应的真实节点,若为undefined则是没有上DOM树
  key: undefined, // 节点唯一标识
  sel: "a", // 选择器 ,标签
  text: "百度", // 文字
}

let vnode1 = h('a', {
  props: {
    href: 'http://www.baidu.com'
  }
}, '百度');
console.log(vnode1)

4.png

手写h函数

10.png vnode.js

// 参数以对象返回
export default function (sel, data, children, text, elm) {
    let key = data.key;
    return {
        sel,   // 选择器 ,标签
        data,  // 属性
        children, // 子节点
        text, // 文字
        elm,  // 节点 父节点,最外层节点
        key,
    }
}

h.js h函数

import vnode from './vnode.js'
/*
h('div','文字');
h('div',{},[]);
h('div',[]);
h('div',{},h())
h('div');
h('div',{},'文字');
h('div',h())
*/
// 低配版h函数,这个函数必须接受3个参数 ,重载功能较弱
// 也就是说,调用的时候必须是下面的三种之一:
// 1, h('div', {}, '文字')
// 2, h('div', {}, [])
// 3, h('div', {}, h())
export default function (sel, data, c) {
    // 检查参数的个数
    if (arguments.length != 3)
        throw new Error('h函数必须传入3个参数');
    // 检查参数c的类型
    if (typeof c == 'string' || typeof c == 'number') {
        // 第一种情况 h('div', {}, '文字')
        return vnode(sel, data, undefined, c, undefined);
    } else if (Array.isArray(c)) {
        //  第二种情况 h('div', {}, [])
        let children = [];
        for (let i = 0; i < c.length; i++) {
            // 检查c[i]必须是一个对象
            if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel'))) {
                throw new Error('传入的数组参数中有项不是h函数');
            }
            // 这里不用执行c[i],h函数会调用,返回对象
            children.push(c[i]);
        }
        return vnode(sel, data, children, undefined, undefined); // 返回虚拟节点,它有children属性
    } else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
        // 第三种情况 h('div', {}, h())
        let children = [c];  // 转成数组传入
        return vnode(sel, data, children, undefined, undefined);
    } else {
        throw new Error('传入的第三个参数类型不对');
    }
};
测试
import h from './snabbdom-handle/h.js'
let vnode1 = h(
    'div', {},
    [
        h('div', {}, [
            h('span', {}, '111'),
            h('span', {}, '444')  
        ]),
        h('span', {}, '2'),
        h('span', {}, h('span', {}, '555')),
     ]
)
console.log(vnode1)

8.png

diff处理新节点

创建新节点,删除旧节点

1.png

patch.js

import vnode from './vnode.js';
import createElement from './createElement.js';
import patchVnode from './patchVnode.js'

export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数,是DOM节点还是虚拟节点?
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 传入的第一个参数是DOM节点,此时要包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
    }
    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
        console.log('是同一个节点');
        patchVnode(oldVnode, newVnode);
    } else {
        console.log('不是同一个节点,暴力插入新的,删除旧的');
        let newVnodeElm = createElement(newVnode);
        // 插入到老节点之前
        if (oldVnode.elm.parentNode && newVnodeElm) {
            // oldVnode.elm  旧节点  如<ui><li></li></ul>
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm);
    }
};

createElement.js

//  创建节点
export default function createElement(vnode) {
    // 创建节点
    let domNode = document.createElement(vnode.sel);
    // 如果是文本
    if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
        // 文本
        domNode.innerText = vnode.text;
        
    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) { // 有子节点
        // 递归创建节点
        for (let i = 0; i < vnode.children.length; i++) {
            let ch = vnode.children[i];
            // 创建子节点的dom 添加到domNode,只有最外层domNode才添加上树
            let newChildrem = createElement(ch);
            // 添加到domNode
            domNode.appendChild(newChildrem)
        }

    }
    // 补充elm属性,并返回这个值,被引用的时候可以根据 vnode.elm的父节点姐插入
    vnode.elm = domNode;

    return vnode.elm;
}
测试创建新节点
import h from './snabbdom-handle/h.js'
import patch from './snabbdom-handle/patch.js'
let vnode1 = h(
    'ul',{},[
        h('li',{},'A'),
        h('li',{},'B'),
        h('li',{},'C'),
        h('li',{},'D'),
    ]
)
let vnode2 = h(
    'ul',{},'1111111'
)
let container = document.getElementById("container");
patch(container,vnode1)

2.png 从图上我们可以看到,当id为container的div和ul相比较时,不是同一个节点比较,会创建新节点,删除老节点

diff处理新旧节点text不同情况

思路

5.png

1 判断oldVnode和newVnode是不是同一个节点 --- 是同一个节点

2 判断newVnode和oldVnode是不是同一个对象 2.1 是同一对象 不需要做什么 2.2 不是同一对象

3 判断新节点有没有text属性
3.1 新节点有text属性
3.11 新节点的text替换老节点的text,如果老节点有children也会替换

3.2 新节点没有text属性 有children属性

3.2.1 判断老的节点是否有children,即新老节点都有children,复杂情况 后面没继续讨论

3.2.2 老节点没有children,新节点有children
删除旧节点 新节点添加上树

patch.js

// 判断oldVnode和newVnode是不是同一个节点
    if(oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key) {
      //  是同一个节点
      // 判断newVnode和oldVnode是不是同一个对象
      if(newVnode === oldVnode) return;  // 不需要做什么
      // 判断新节点有没有text属性   
      if(newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
        // 新节点有text属性
        // 判断新旧节点是否相等
        if(newVnode.text != oldVnode.text) {
            oldVnode.elm.innerText = newVnode.text;  // 新节点的text替换老节点的text,如果老节点有children也会替换
        }
      } else {
        // 新节点没有text属性,有children属性
        // 判断老的节点是否有children,即新老节点都有children,复杂情况
        if(oldVnode.children != undefined && oldVnode.children.length > 0) {

        } else {
            // 删除旧节点
            oldVnode.elm.innerHTML = '';
            // 老节点没有children,新节点有children
            for (let i = 0; i < newVnode.children.length; i++) {
                let dom = createElement(newVnode.children[i]);
                oldVnode.elm.appendChild(dom) // 添加上树
            }
        }
      }
    }
测试
import h from './snabbdom-handle/h.js'
import patch from './snabbdom-handle/patch.js'

let vnode1 = h('section',{} ,"我是文本")

let container = document.getElementById("container");

patch(container,vnode1)

// 新节点 ,有children
let vnode2 = h('section',{}, [
    h('p',{},'x'),
    h('p',{},'y'),
    h('p',{},'z')
])

let btn = document.getElementById('btn');
btn.onclick = function() {
    patch(vnode1,vnode2)
}

点击diff按钮后,如下面所示

3.png

4.png

diff算法的子节点的更新策略

复杂情况 newVnode和oldVnode都有children

  • 1 新前与旧前
  • 2 新后与旧后
  • 3 新后与旧前(此策略发生,新前指向的节点,移动到旧后的后面)
  • 4 新前与旧后(此策略发生,新前指向的节点,移动到旧前的前面)

依次查找,先策略1去查找,策略1找不到是采用策略2,策略2找不到是采用策略3,策略3找不到是采用策略4,如果策略没有找到,则需要用循坏来寻找

6.png

图解几种情况

7.png

8.png

9.png

10.png 优化patch.js的代码

import vnode from './vnode.js';
import createElement from './createElement.js';
import patchVnode from './patchVnode.js'

export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数,是DOM节点还是虚拟节点?
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 传入的第一个参数是DOM节点,此时要包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
    }
    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
        console.log('是同一个节点');
        patchVnode(oldVnode, newVnode);
    } else {
        console.log('不是同一个节点,暴力插入新的,删除旧的');
        let newVnodeElm = createElement(newVnode);
        // 插入到老节点之前
        if (oldVnode.elm.parentNode && newVnodeElm) {
            // oldVnode.elm  旧节点  如<ui><li></li></ul>
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm);
    }
};

patchVnode.js

import createElement from "./createElement";
import updateChildren from './updateChildren.js';

// 对比同一个虚拟节点
export default function patchVnode(oldVnode, newVnode) {
    // 判断新旧vnode是否是同一个对象
    if (oldVnode === newVnode) return;
    // 判断新vnode有没有text属性
    if (newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
        // 新vnode有text属性
        console.log('新vnode有text属性');
        if (newVnode.text != oldVnode.text) {
            // 如果新虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可。如果老的elm中是children,那么也会立即消失掉。
            oldVnode.elm.innerText = newVnode.text;
        }
    } else {
        // 新vnode没有text属性,有children
        console.log('新vnode没有text属性');
        // 判断老节点有没有children
        if (oldVnode.children != undefined && oldVnode.children.length > 0) {
            // 老节点有children,新节点也有children,最复杂的情况。
            updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);
        } else {
            // 老节点没有children,新节点有children
            oldVnode.elm.innerHTML = ''; // 清空老的节点的内容
            // 遍历新的vnode的子节点,
            for (let i = 0; i < newVnode.children.length; i++) {
                let dom = createElement(newVnode.children[i]);
                oldVnode.elm.appendChild(dom); // 创建DOM,上树
            }
        }
    }
}

updateChildren.js -核心代码

import patchVnode from './patchVnode.js';
import createElement from './createElement.js';

// 判断是否是同一个虚拟节点
function checkSameVnode(a, b) {
    return a.sel == b.sel && a.key == b.key;
};

export default function updateChildren(parentElm, oldCh, newCh) {
    console.log('我是updateChildren');
    console.log(oldCh, newCh);

    // 旧前
    let oldStartIdx = 0;
    // 新前
    let newStartIdx = 0;
    // 旧后
    let oldEndIdx = oldCh.length - 1;
    // 新后
    let newEndIdx = newCh.length - 1;
    // 旧前节点
    let oldStartVnode = oldCh[0];
    // 旧后节点
    let oldEndVnode = oldCh[oldEndIdx];
    // 新前节点
    let newStartVnode = newCh[0];
    // 新后节点
    let newEndVnode = newCh[newEndIdx];

    let keyMap = null;

    // 开始大while了
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        // 首先不是判断4策略命中,而是要略过已经加undefined标记的东西
        if (oldCh[oldStartIdx] == undefined) {
            oldStartVnode = oldCh[++oldStartIdx];
        } else if (oldCh[oldEndIdx] == undefined) {
            oldEndVnode = oldCh[--oldEndIdx];
        } else if (newCh[newStartIdx] == undefined) {
            newStartVnode = newCh[++newStartIdx];
        } else if (newCh[newEndIdx] == undefined) {
            newEndVnode = newCh[--newEndIdx];
        }
        // 新前和旧前
        else if (checkSameVnode(oldStartVnode, newStartVnode)) {
            console.log('----------1新前和旧前------------');
            patchVnode(oldStartVnode, newStartVnode);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        }
        // 新后和旧后 
        else if (checkSameVnode(oldEndVnode, newEndVnode)) {
            console.log('----------2新后和旧后------------');
            patchVnode(oldEndVnode, newEndVnode);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        }
        // 新后和旧前 
        else if (checkSameVnode(oldStartVnode, newEndVnode)) {
            console.log('----------3新后和旧前------------');
            patchVnode(oldStartVnode, newEndVnode);
            // 当3新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
            // 如何移动节点? 只要你插入一个已经在DOM树上的节点,它就会被移动
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        }
        // 新前和旧后
        else if (checkSameVnode(oldEndVnode, newStartVnode)) {
            console.log('----------4新前和旧后------------');
            patchVnode(oldEndVnode, newStartVnode);
            // 当4新前和旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        } else {
            // 四种策略都没有匹配到
            // keyMap一个映射对象
            if (!keyMap) {
                keyMap = {};
                // 从oldStartIdx开始,到oldEndIdx结束,创建keyMap映射对象
                for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                    const key = oldCh[i].key;
                    if (key != undefined) {
                        keyMap[key] = i;
                    }
                }
            }
            // 寻找当前这项(newStartIdx)这项在keyMap中的映射的位置序号
            const idxInOld = keyMap[newStartVnode.key];
            if (idxInOld == undefined) {
                // 判断,如果idxInOld是undefined表示它是全新的项
                // 被加入的项(就是newStartVnode这项)现不是真正的DOM节点
                parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
            } else {
                // 如果不是undefined,不是全新的项,而是要移动
                const elmToMove = oldCh[idxInOld];
                patchVnode(elmToMove, newStartVnode);
                // 把这项设置为undefined,表示我已经处理完这项了
                oldCh[idxInOld] = undefined;
                // 移动,调用insertBefore也可以实现移动。
                parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
            }
            // 指针下移,只移动新的头
            newStartVnode = newCh[++newStartIdx];
        }
    }

    //查看剩余的。循环结束了start还是比old小
    if (newStartIdx <= newEndIdx) {
        // '新节点还有剩余节点没有处理,要加项。要把所有剩余的节点,都要插入到oldStartIdx之前'
        // 遍历新的newCh,添加到老的没有处理的之前
        for (let i = newStartIdx; i <= newEndIdx; i++) {
            // insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了。
            // newCh[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM
            parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
        }
    } else if (oldStartIdx <= oldEndIdx) {
        // 老节点还有剩余节点没有处理,要删除项;
        // 批量删除oldStart和oldEnd指针之间的项
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
            if (oldCh[i]) {
                parentElm.removeChild(oldCh[i].elm);
            }
        }
    }
};
部分测试
新前与旧前测试代码
import h from './snabbdom-handle/h.js'
import patch from './snabbdom-handle/patch.js'

let vnode1 = h('ul',{}, [
    h('li',{ key: 'A'},'A'),
    h('li',{key: 'B'},'B'),
    h('li',{key: 'C'},'C'),
    h('li',{key: 'D'},'D'),
])


patch(container,vnode1)

// 新节点 ,有children
let vnode2 = h('ul',{}, [
    h('li',{key: 'A'},'AAA'),
    h('li',{key: 'B'},'B'),
    h('li',{key: 'C'},'C'),
    h('li',{key: 'D'},'D'),
])

let btn = document.getElementById('btn');
btn.onclick = function() {
    patch(vnode1,vnode2)
}

新前与旧前1.png

新前与旧前2.png

新后与旧后 + 循环对比删除,新增测试代码
import h from './snabbdom-handle/h.js'
import patch from './snabbdom-handle/patch.js'

let vnode1 = h('ul',{}, [
    h('li',{ key: 'A'},'A1'),
    h('li',{ key: 'B'},'B2'),
    h('li',{ key: 'C'},'C3'),
    h('li',{ key: 'D'},'D4')
])

patch(container,vnode1)

// 新节点 ,有children
let vnode2 = h('ul',{}, [
    h('li',{ key: 'Q'},'Q'),
    h('li',{ key: 'M'},'M'),
    h('li',{ key: 'N'},'N'),
    h('li',{ key: 'D'},'D4')
    
])
let btn = document.getElementById('btn');
btn.onclick = function() {
    patch(vnode1,vnode2)
}

1.png

2.png

先新前与新后查找,Q与A节点不同,此时新后与旧后查找,此时指针往上移,继续查找,找不到,新后与旧前查找,找不到,继续新前与旧后查找,发现四种策略都找不到,此时开始循环查找;新节点循环有剩余节点Q,M,N,需要新增,旧节点循环有剩余节点A1,B2,C3,说明要删除

新后与旧前测试代码
import h from './snabbdom-handle/h.js'
import patch from './snabbdom-handle/patch.js'

let vnode1 = h('ul',{}, [
    h('li',{ key: 'A'},'A1'),
    h('li',{ key: 'B'},'B2'),
    h('li',{ key: 'C'},'C3'),
    h('li',{ key: 'D'},'D4'),
    h('li',{ key: 'E'},'E5'),
])

patch(container,vnode1)

// 新节点 ,有children
let vnode2 = h('ul',{}, [
    h('li',{ key: 'E'},'E50'),
    h('li',{ key: 'D'},'D40'),
    h('li',{ key: 'C'},'C30'),
    h('li',{ key: 'B'},'B20'),
    h('li',{ key: 'A'},'A10'),
    
])
let btn = document.getElementById('btn');
btn.onclick = function() {
    patch(vnode1,vnode2)
}

1.png

2.png

1 新前和新后比较 E和A不相同 则

2 新后和旧后开始比较 A和E不相同 ,继续

3 新后与旧前比较 找到A 移动新前指向的节点(A)到老节点的旧后的后面

此时 新后的指针往上移 旧前的指针往下移 新一轮比较

新前和旧前比较 E和B不相同

新后与旧后比较 B和E不相同

新后与旧前比较 B找到

移动新前指向的节点(B)到老节点的旧后的后面 即A的前面

如此反复

总结

1.png

diff算法是虚拟DOM的核心一部分,同层比较,通过新老节点的对比,将改动的地方更新到真实DOM上。

具体实现的方法是patch, patchVnode以及updateChildren

1 patch函数被调用

2 判断oldVnode是虚拟节点还是dom节点?如果是dom节点转化为虚拟节点

3 判断oldVnode和newVnode是不是sel和key都相同? 如果不是,删除旧的节点,插入新的节点

4 oldVnode和newVnode是内存中的同一个对象?

  • 4.1 如果是,什么都不需要处理

  • 4.2 如果不是

    4.2.1 newVnode有text属性,newVnode的text和oldVnode的text是不是相同 如果相同,什么都不需要处理,如果不相同,把oldVnode中的elm的innerText改为newVnode的text

    4.2.2 newVnode有children属性,oldVnode有text属性,清空oldVnode的text,将newVnode的children添加到dom中;newVnode和oldVnode都有children,此情况最复杂

5 diff算法的子节点的更新策略

  1. 新前与旧前
  2. 新后与旧后
  3. 新后与旧前(此策略发生,新前指向的节点,移动到旧后的后面)
  4. 新前与旧后(此策略发生,新前指向的节点,移动到旧前的前面)

查找过程 新前与旧前比较,如果找到,新前指针和旧前指针下移(++),如果找不到,

新后与旧后开始比较,如果找到,新后指针和旧后指针上移(--);如果找不到,

新后与旧前开始比较,如果找到,新后指针上移(--),旧前指针下移(++);且新后对应的节点移到旧后的后面,如果找不到,

新前与旧后开始对比,如果找到,新前指针下移(++),旧后指针上移(--),新前指向的节点移到旧前的前面,如果找不到,

判断是否oldCh中有和newStartVnode的具有相同的key的Vnode,如果没有找到,说明是新的节点,创建一个新的节点,插入即可

如果找到了和newStartVnode具有相同的key的Vnode,命名为elmToMove,下标idxInOld,如果idxInOld是undefined 说明是新的节点,此时创建新的虚拟节点,并插入到oldStartVnode.elm前面,如果idxInOld不是undefined,那就两者再去patchVnode,把这项设置为undefined,表示我已经处理完这项了 然后插到oldStartVnode.elm前面

在经过了While循环之后,如果发现新节点数组或者旧节点数组里面还有剩余的节点,根据具体情况来进行删除或者新增的操作

资源来源

B站视频地址