Vue源码之虚拟DOM和diff算法

前言

  突然发现草稿箱里还躺着这篇早在暑假就完成的文章,抓紧发了,内容是之前编辑的,如有错误,麻烦告知。😁

一、snabbdom简介

  • snabbdom是瑞典语单词,单词原意“速度”;
    在这里插入图片描述

  • snabbdom是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码借鉴了snabbdom;

  • 官方git:https://github.com/snabbdom/snabbdom

  • 在git上的snabbdom源码是用TypeScript写的,git上并不提供编译好的 JavaScript版本;

  • 如果要直接使用build出来的JavaScript版的snabbdom库,可以从npm上下载:

npm i snabbdom

  • 视频里的老师说:学习库底层时,建议大家阅读原汁原味的TS代码,最好带有库作者原注释, 这样对你的源码阅读能力会有很大的提升。

二、snabbdom测试环境搭建

  1. 新建文件夹,创建package.json

npm init

  1. 安装 snabbdom:

npm i snabbdom

在这里插入图片描述
  可以看到build文件夹下有 js 和 ts 文件,而src文件夹中只有 ts 文件:
在这里插入图片描述
在这里插入图片描述

  1. 搭建webpack 和webpack-dev-server开发环境:

  注意:必须安装最新版webpack@5,不能安装webpack@4,因为 webpack4没有读取身份证中exports的能力。

npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3

在这里插入图片描述
4. 新建 src 文件夹,在该文件夹创建 index.js 文件,比如输入 alert(‘111’) :

alert('111');

在这里插入图片描述
在这里插入图片描述

  1. 新建webpack.config.js文件,参考webpack官网进行配置:
// 从https://www.webpackjs.com/官网照着配置
const path = require('path');

module.exports = {
    // 入口
    entry: './src/index.js',
    // 出口
    output: {
        // 虚拟打包路径,就是说文件夹不会真正生成,而是在8080端口虚拟生成
        publicPath: 'xuni',
        // 打包出来的文件名,不会真正的物理生成
        filename: 'bundle.js'
    },
    devServer: {
        // 端口号
        port: 8080,
        // 静态资源文件夹
        contentBase: 'www'
    }
};
  1. 新建 www 文件夹,在该文件夹中创建 index.html 文件,并引入 bundle.js 文件:
    在这里插入图片描述

  2. 将 package.json 文件中的 scripts 改成:

"dev": "webpack-dev-server"

在这里插入图片描述

  1. 运行,并打开 http://localhost:8080/ 网址:

npm run dev

在这里插入图片描述
  而虚拟打包的文件在 http://localhost:8080/xuni/bundle.js网址,没有被真正的生成:
在这里插入图片描述

  1. 跑通snabbdom官方git首页的demo程序,即可证明调试环境已经搭建成功。步骤如下:

  官方git网址
  将该官方git首页的下述代码复制粘贴到src文件夹的index.js文件中:
在这里插入图片描述
将下图所示框内代码进行更改:
在这里插入图片描述
  更改为:
在这里插入图片描述

  在 www 文件夹中的 index.html文件中添加下述代码,如下图所示:

<div id="container"></div>

在这里插入图片描述
  最后刷新网页,得如下所示结果:
在这里插入图片描述
  说明跑通了snabbdom官方git首页的demo程序,即可证明调试环境已经搭建成功。

三、虚拟DOM和h函数

1. 什么是虚拟 DOM

  用JavaScript对象描述DOM的层次结构,DOM中的一切属性都在虚拟DOM中有对应的属性。
在这里插入图片描述
在这里插入图片描述

2. diff是发生在虚拟DOM上的

  新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出应该如何最小量更新,最后反映到真正的DOM上。diff是发生在虚拟DOM上的diff。

3. DOM如何变为虚拟DOM,属于模板编译原理范畴。
4. h函数用来产生虚拟节点(vnode)

在这里插入图片描述

5. 一个虚拟节点有哪些属性

在这里插入图片描述

6. 创建虚拟节点
import { h } from 'snabbdom/h' 
//创建虚拟节点
const myVnode1 = h('a',{props: { href:'https://www.baidu.com/' } },'百度');
console.log(myVnode1);

在这里插入图片描述

7. h函数可以嵌套使用,从而得到虚拟DOM树
import { h } from 'snabbdom/h' 
const myVnode2 = h('ul',{},[
    h('li',{},'牛奶'),
    h('li',{},'咖啡'),
    h('li',{},'可乐')
]);
console.log(myVnode2);

在这里插入图片描述

8. 虚拟节点上树
import { init } from 'snabbdom/init'
import { classModule } from 'snabbdom/modules/class'
import { propsModule } from 'snabbdom/modules/props'
import { styleModule } from 'snabbdom/modules/style'
import { eventListenersModule } from 'snabbdom/modules/eventlisteners'
import { h } from 'snabbdom/h' 


//创建出patch函数
const patch = init([classModule,propsModule,styleModule,eventListenersModule]);

//创建虚拟节点
const myVnode1 = h('a',{
    props: { 
        href:'https://www.baidu.com/',
        target:'_blank' 
    }
},'百度');
console.log(myVnode1);

//让虚拟节点上树
const container = document.getElementById('container');
patch(container,myVnode1);

  点击链接,即可打开新标签页。
在这里插入图片描述

四、手写h函数

  例如,编写一个低配版本的h函数,这个函数必须接受3个参数,缺一不可,相当于它的重载功能较弱。也就是说,调用的时候形态必须是下面的三种之一:

形态① h(‘div’, {}, ‘文字’)
形态② h(‘div’, {}, [])
形态③ h(‘div’, {}, h())

  新建 mysnabbdom 文件夹,在该文件夹创建 h.js 文件和 vnode.js 文件:
在这里插入图片描述
  vnode.js文件代码:

//把传入的5个参数组合成对象返回
export default function(sel,data,children,text,elm){
    const key = data.key;
    return {
        sel,data,children,text,elm,key
    }
}

  h.js文件代码:

import vnode from './vnode.js'

export default function(sel,data,c){
    //检查参数的个数
    if(arguments.length != 3)
        throw new Error('对不起,h函数必须传入3个参数,我们是低配版h函数');

     // 检查参数c的类型
     if(typeof c == 'string' || typeof c == 'number'){
        // 说明现在调用h函数是形态①
        return vnode(sel,data,undefined,c,undefined);
     }else if(Array.isArray(c)){
          // 说明现在调用h函数是形态②
          let children = [];
          // 遍历c,收集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],因为你的测试语句中已经有了执行
                // 此时只需要收集好就可以了
                children.push(c[i]);
          }
          // 循环结束了,就说明children收集完毕了,此时可以返回虚拟节点了,它有children属性
          return vnode(sel, data, children, undefined, undefined);
     }else if(typeof c == 'object' && c.hasOwnProperty('sel')){
        // 说明现在调用h函数是形态③
        // 即,传入的c是唯一的children。不用执行c,因为测试语句中已经执行了c。
        let children = [c];
        return vnode(sel, data, children, undefined, undefined);
     }else{
        throw new Error('传入的第三个参数类型不对');
     }
}

  index.js文件代码:

import h from './mysnabbdom/h.js';

//创建虚拟节点
const myVnode1 = h('a',{
    props: { 
        href:'https://www.baidu.com/',
        target:'_blank' 
    }
},'百度');
console.log(myVnode1);

const myVnode2 = h('ul',{},[
    h('li',{},'牛奶'),
    h('li',{},'咖啡'),
    h('li',{},'可乐')
]);
console.log(myVnode2);

在这里插入图片描述

五、感受diff算法

  举例:
  index.js文件代码:

import { init } from 'snabbdom/init';
import { classModule } from 'snabbdom/modules/class';
import { propsModule } from 'snabbdom/modules/props';
import { styleModule } from 'snabbdom/modules/style';
import { eventListenersModule } from 'snabbdom/modules/eventlisteners';
import { h } from 'snabbdom/h';

// 得到盒子和按钮
const container = document.getElementById('container');
const btn = document.getElementById('btn');

// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);

const 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);

const vnode2 = h('ul', {}, [
    h('li', { key: 'D' }, 'D'),
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'B' }, 'B')
]);

// 点击按钮时,将vnode1变为vnode2
btn.onclick = function () {
    patch(vnode1, vnode2);
};

  index.html文件代码:

<button id="btn">按我改变DOM</button>
<div id="container"></div>
<script src="/xuni/bundle.js"></script>

刷新后运行结果如下:
在这里插入图片描述
例如我在浏览器 中将 li 标签中的 A 改成 111:
在这里插入图片描述
  然后点击按钮,可以看到不是暴力删除旧的、插入新的而是在原来的基础上进行微改。这是因为前后均是同一虚拟节点(选择器相同且key相同),会进行精细化比较。
在这里插入图片描述
结论:

  1. key很重要,key是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点。
  2. 只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。 如何定义是同一个虚拟节点:选择器相同且key相同
  3. 只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是跨层了,不会进行精细化比较,而是暴力删除旧的、然后插入新的。在实际Vue开发中,基本不会遇见跨层比较,所以这是合理的优化机制。同层比较示意图如下:
    在这里插入图片描述

六、如何定义是否是“同一个节点”

  旧节点的key要和新节点的key相同,且,旧节点的选择器要和新节点的选择器相同。

// snabbdom 源码
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}

七、创建节点时,所有子节点需要递归创建

// snabbdom 源码
function createElm() {
  // ...
  if (is.array(children)) {
    for (i = 0; i < children.length; ++i) {
      const ch = children[i]
      if (ch != null) {
        api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
      }
    }
  } else if (is.primitive(vnode.text)) {
    api.appendChild(elm, api.createTextNode(vnode.text))
  }
  // ...
}

八、diff 处理新旧节点不是同一个节点时

在这里插入图片描述

1、手写第一次上树时(新节点内部没有子节点,只有文本)

  这里是简单的一次上树,考虑的是: 在新旧节点不是同一个节点时,新节点内部没有子节点,只有文本的情况

  www文件夹下的index.html代码:

    <div id="container"></div> 
    <script src="/xuni/bundle.js"></script>

  src文件夹的index.js文件代码:

import h from './mysnabbdom/h.js';
import patch from './mysnabbdom/patch.js';

const myVnode1 = h('h1', {}, '你好');
const container = document.getElementById('container');

// 第一次上树
patch(container, myVnode1);

  mysnabbdom文件夹下的 h.js 和 vnode.js文件同上述,新建 createElement.js 文件,代码如下:

// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入,插入在patch进行
export default function createElement(vnode) {
    console.log('目的是把虚拟节点', vnode,'真正变成DOM,并添加elm属性');
    // 创建一个DOM节点,这个节点现在还是孤儿节点
    let domNode = document.createElement(vnode.sel);
    // 有子节点还是有文本?
    if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
        // 它内部是文字
        domNode.innerText = vnode.text;
        // 补充elm属性
        vnode.elm = domNode;
    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
        // 它内部是子节点,就要递归创建节点
    }

    // 返回elm,elm属性是一个纯DOM对象
    return vnode.elm;
};

  在该文件夹下继续新建 patch.js 文件,代码如下:

import vnode from './vnode.js';
import createElement from './createElement.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('是同一个节点');
        //
        //
        //
    } else {
        console.log('不是同一个节点,暴力插入新的,删除旧的');
        let newVnodeElm = createElement(newVnode);
        // 插入到老节点之前
        oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm); 
    }
};

  运行结果:
在这里插入图片描述

2、手写递归创建子节点(新节点内部有子节点)

  这里考虑了: 在新旧节点不是同一个节点时,新节点内部有子节点的情况

  mysnabbdom文件夹下:

  createElement.js 文件代码:

// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入,插入在patch进行
export default function createElement(vnode) {
    // console.log('目的是把虚拟节点', vnode,'真正变成DOM,并添加elm属性');
    // 创建一个DOM节点,这个节点现在还是孤儿节点
    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++) {
            // 得到当前这个children
            let ch = vnode.children[i];
            // 创建出它的DOM,一旦调用createElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,但是还没有上树,是一个孤儿节点。
            let chDOM = createElement(ch);
            // 上树
            domNode.appendChild(chDOM);
        }
    }
    // 补充elm属性
    vnode.elm = domNode;
   
    // 返回elm,elm属性是一个纯DOM对象
    return vnode.elm;
};

  patch.js 文件代码:

import vnode from './vnode.js';
import createElement from './createElement.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('是同一个节点');
        //
        //
        //
    } else {
        console.log('不是同一个节点,暴力插入新的,删除旧的');
        let newVnodeElm = createElement(newVnode);
        
        // 插入到老节点之前
        if (oldVnode.elm.parentNode && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm);
    }
};

  www文件夹下的index.html代码:

	<button id="btn">按我改变DOM</button>
    <div id="container"></div> 
    <script src="/xuni/bundle.js"></script>

src文件夹的index.js文件代码:

import h from './mysnabbdom/h.js';
import patch from './mysnabbdom/patch.js';

const myVnode1 = h('ul', {}, [
    h('li', {}, 'A'),
    h('li', {}, 'B'),
    h('li', {}, 'C'),
    h('li', {}, 'D'),
    h('li', {}, 'E')
]);

// 得到盒子和按钮
const container = document.getElementById('container');
const btn = document.getElementById('btn');

// 第一次上树
patch(container, myVnode1);

// 新节点
const myVnode2 = h('ul', {}, [
    h('h1', {}, '我是新的h1'),
    h('h1', {}, '我是新的h2'),
]);

btn.onclick = function () {
    patch(myVnode1, myVnode2);
}

  运行结果:
  按钮点击前:
在这里插入图片描述
按钮点击后:
在这里插入图片描述

九、手写diff处理新旧节点是同一个节点时

在这里插入图片描述

1、手写新旧节点 text 的不同情况

  在 mysnabbdom 文件夹新建 patchVnode.js 文件,代码如下:

import createElement from "./createElement";

// 对比同一个虚拟节点
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,此时就是最复杂的情况。
            //
            //
            //
        } else {
            // 老的没有children,新的有children
            // 清空老的节点的内容
            oldVnode.elm.innerHTML = '';
            // 遍历新的vnode的子节点,创建DOM,上树
            for (let i = 0; i < newVnode.children.length; i++) {
                let dom = createElement(newVnode.children[i]);
                oldVnode.elm.appendChild(dom);
            }
        }
    }
}

  patch.js 文件改变位置如下:
在这里插入图片描述

2、手写diff更新子节点

(1)经典的diff算法优化策略

在这里插入图片描述
何为新前 新后 旧前 旧后:

  • 新前:所有新的虚拟节点中 子节点当中 所有未处理的节点中 开头的节点
  • 新后:所有新的虚拟节点当中 子节点当中没有处理的节点中 最后一个节点
  • 旧前 旧后 同理。
  • 为四个指针,分别指向新前 新后 旧前 旧后

四种命中查找:

  ①新前与旧前

  新前与旧前进行对比,对比新前与旧前是不是同一个节点,如果是同一个节点 说明不是新增 不是删除 是更新,执行更新操作。如果完全相同,新前 与 旧前 指针下移,继续比较,如果不命中①,判断是否命中②,依次类推。

  ②新后与旧后

  新前 与 旧前 在指针下移到某位置时对比发现 不再是同一节点,新后 与 旧后 两指针 比较是否是同一节点,如果是同一节点, 指针上移,继续比较,如果不命中②,判断是否命中③,依次类推。
在这里插入图片描述

  ③新后与旧前

  此种发生涉及移动节点,将新前指向的节点,移动到旧后之后。
  如果命中③,移动之后,新后指针上移,旧前指针下移。
在这里插入图片描述  由上图可知,新后 与 旧前 命中,此时在虚拟节点中该节点被标注为 undefined,在真实DOM中被插入在 旧后 的 后面。然后 新后 指针上移,旧前 指针下移。
在这里插入图片描述

  ④新前与旧后

  此种发生涉及移动节点,将新前指向的节点,移动到旧前之前
  如果命中④,移动之后,新前指针上下移,旧后指针上移。
在这里插入图片描述
  由上图可知,新前 与 旧后 命中,此时在虚拟节点中该节点被标注为 undefined,在真实DOM中被插入在旧前的前面。然后 新前指针上下移,旧后指针上移。
在这里插入图片描述

  • 经典的diff算法优化策略 命中一种 就不再进行命中判断。
  • 如果都没有命中,就需要用循环来寻找,循环旧子节点。
  • 如果是旧节点先循环完毕,说明新节点中有要插入的节点。
  • 如果是新节点先循环完毕,如果老节点中还有剩余节点(旧前和新后指针中间的节点),说明他们是要被删除的节点。h(‘li’,{key:‘D’},‘D’)
(2)手写实现

  在 mysnabbdom 文件夹下新增 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) {
        console.log('★');
        // 首先不是判断①②③④命中,而是要略过已经加undefined标记的东西
        if (oldStartVnode == null || oldCh[oldStartIdx] == undefined) {
            oldStartVnode = oldCh[++oldStartIdx];
        } else if (oldEndVnode == null || oldCh[oldEndIdx] == undefined) {
            oldEndVnode = oldCh[--oldEndIdx];
        } else if (newStartVnode == null || newCh[newStartIdx] == undefined) {
            newStartVnode = newCh[++newStartIdx];
        } else if (newEndVnode == null || newCh[newEndIdx] == undefined) {
            newEndVnode = newCh[--newEndIdx];
        } else if (checkSameVnode(oldStartVnode, newStartVnode)) {
            // 新前和旧前
            console.log('①新前和旧前命中');
            patchVnode(oldStartVnode, newStartVnode);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        } else if (checkSameVnode(oldEndVnode, newEndVnode)) {
            // 新后和旧后
            console.log('②新后和旧后命中');
            patchVnode(oldEndVnode, newEndVnode);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (checkSameVnode(oldStartVnode, newEndVnode)) {
            // 新后和旧前
            console.log('③新后和旧前命中');
            patchVnode(oldStartVnode, newEndVnode);
            // 当③新后与旧前命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧后的后面
            // 如何移动节点?只要你插入一个已经在DOM树上的节点,它就会被移动
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (checkSameVnode(oldEndVnode, newStartVnode)) {
            // 新前和旧后
            console.log('④新前和旧后命中');
            patchVnode(oldEndVnode, newStartVnode);
            // 当④新前和旧后命中的时候,此时要移动节点。移动新前指向的这个节点到老节点的旧前的前面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            // 如何移动节点?只要你插入一个已经在DOM树上的节点,它就会被移动
            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;
                    }
                }
            }
            console.log(keyMap);
            // 寻找当前这项(newStartIdx)这项在keyMap中的映射的位置序号
            const idxInOld = keyMap[newStartVnode.key];
            console.log(idxInOld);
            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) {
        console.log('new还有剩余节点没有处理,要加项。要把所有剩余的节点,都要插入到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) {
        console.log('old还有剩余节点没有处理,要删除项');
        // 批量删除oldStart和oldEnd指针之间的项
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
            if (oldCh[i]) {
                parentElm.removeChild(oldCh[i].elm);
            }
        }
    }
};

在 patchVnode.js 文件的红框位置写入以下代码:
在这里插入图片描述

  到此,该部分内容就学完了,以上是根据 Vue源码探秘之虚拟DOM和diff算法 视频内容整理的,希望帮到自己的同时能帮到大家😁。

  这部分内容之前寒假有看过,也整理了这篇博客的一部分,但是不太懂,所以一直在草稿箱躺着。最近突然想整理一下,但是,现在整理好发出来了并不代表是学会了,而是学废了😵脑袋晕晕的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值