Vue源码-虚拟DOM与diff算法

609 阅读18分钟

本文研究目标

  • 虚拟DOM如何被渲染函数(h函数)产生,需要手写h函数
  • diff算法原理并手写
  • 虚拟DOM如何通过diff变为真正的DOM,实际上虚拟DOM变回真正的DOM,是涵盖在diff算法里面的
  • 本文不研究如何把真实DOM变为虚拟DOM(模板编译范畴),只研究虚拟DOM如何变回真实DOM

snabbdom

snabbdom(瑞典语,“速度”)是著名的虚拟DOM库,是diff算法的鼻祖。

搭建初始环境

1.安装snabbdom npm install -S snabbdom

2.安装webpack5并配置 cnpm i -D webpack@5 webpack-cli@3 webpack-dev-server@3

3.跑通snabbdom官方demo

/src/index.js

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";

const patch = init([
    // Init patch function with chosen modules
    classModule, // makes it easy to toggle classes
    propsModule, // for setting properties on DOM elements
    styleModule, // handles styling on elements with support for animations
    eventListenersModule, // attaches event listeners
]);

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

const vnode = h("div#container.two.classes", { on: { click: function () { } } }, [
    h("span", { style: { fontWeight: "bold" } }, "This is bold"),
    " and this is just normal text",
    h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);

const newVnode = h(
    "div#container.two.classes",
    { on: { click: function () { } } },
    [
        h(
            "span",
            { style: { fontWeight: "normal", fontStyle: "italic" } },
            "This is now italic type"
        ),
        " and this is still just normal text",
        h("a", { props: { href: "/bar" } }, "I'll take you places!"),
    ]
);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state

www/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>虚拟DOM与diff算法</title>
</head>

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

</html>

package.json

{
  "name": "vdom_diff_study",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "dev": "webpack-dev-server --open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "snabbdom": "^3.6.2",
    "webpack": "^5.92.0",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.3"
  }
}

webpack.config.js

const path = require('path')
module.exports = {
    // webpack5 不用配置mode
    // 入口
    entry: "./src/index.js",
    // 出口
    output: {
        // 虚拟打包路径,文件夹不会真正生成,而是在8080端口虚拟生成
        publicPath: "xuni",
        // 打包出来的文件名
        filename: "bundle.js",
    },
    // 配置webpack-dev-server
    devServer: {
        // 静态根目录
        contentBase: 'www',
        // 端口号
        port: 8080,
        open: true, // 自动打开浏览器
        hot: true, // 启用热模块替换
    },
};

最后npm run dev启动项目

image.png

虚拟DOM和h函数

什么是虚拟DOM

虚拟DOM:用JS对象描述DOM的层次结构。DOM中的一切属性都在虚拟DOM中有对应的属性

image.png

diff是发生在虚拟DOM上的

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

DOM如何变为虚拟DOM,本文章不研究

DOM如何变为虚拟DOM,属于模板编译原理范畴,本节不研究

h函数

h函数用来产生虚拟节点

image.png

虚拟节点有哪些属性

{
    children:undefined,
    data:{},
    elm:undefined,//表示对应的真实DOM节点,如果为undefined表示还没有上树
    key:undefined,//唯一标识
    sel:'div'//选择器,
    text:'文字'
}

创建第一个虚拟节点

index.js文件

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";

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

打印结果: image.png

但是发现虽然创建了虚拟节点,但是页面看不到这个虚拟节点,也就是并没有上树

需要使用patch方法让虚拟节点上树(patch函数后续再讲解,非常重要是diff算法的核心):

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";

// 创建出patch函数
const patch = init([
    // init 模块用来创建具有初始化功能的模块
    classModule,
    propsModule,
    styleModule,
    eventListenersModule
]);


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


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

h函数可以嵌套使用,从而得到虚拟DOM树(重要)

const myVnode3 = h('ul', [
    h('li', '语文'),
    h('li', '数学'),
    h('li', [
        h('div', [
            h('span', '英语'),
            h('span', '物理')
        ])
    ])
])

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

手写h函数

只关注核心代码

vnode函数

vnode函数功能就是把传入的参数组成对象返回 vnode.js

// vnode函数功能就是把传入的参数组成对象返回
export default function (sel, data, children, text, elm) {
    return { sel, data, children, text, elm }
}

h.js

import vnode from './vnode'

// 编写一个低配版本的h函数,这个函数必须接收3个参数,缺一不可
// 相当于它的重载能力较弱
// 也就是说,调用的时候形态必须是下面的三种之一
// 形态一:h('div', {}, '文字')
// 形态二:h('div', {}, [])
// 形态三:h('div', {}, h())
export default function h(sel, data, c) {
    // 检查参数的个数
    if (arguments.length !== 3) {
        throw new Error('对不起,h函数必须传3个参数,我们这是低配版')
    }
    // 检查参数c的类型
    if (typeof c === 'string' || typeof c === 'number') {
        // 说明现在调用的h函数是形态一
        return vnode(sel, data, undefined, c, undefined)
    } else if (Array.isArray(c)) {
        //说明现在是形态二
        let children = []
        // 遍历c
        for (let i = 0; i < c.length; i++) {
            // 检查c[i]必须是一个对象
            if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) {
                throw new Error('形态二的c数组里面的元素必须是对象,并且有sel属性')
            }
            // 这里不用执行c[i],因为你的测试语句中已经有了执行
            // 此时只需要收集好就行了
            children.push(c[i])
        }
        // 循环结束,children收集完毕
        // 此时可以返回children属性
        return vnode(sel, data, children, undefined, undefined)
    } else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
        // 说明现在是形态三
        // 即传入的c是唯一的children
        let children = [c]
        return vnode(sel, data, children, undefined, undefined)
    } else {
        throw new Error('对不起,h函数的第三个参数应该是字符串、数组、对象')
    }
}

index.js使用自己写的h函数与snabbdom的patch函数结合使用

import h from './mysnabbdom/h'

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
} from "snabbdom";

// 创建出patch函数
const patch = init([
    // init 模块用来创建具有初始化功能的模块
    classModule,
    propsModule,
    styleModule,
    eventListenersModule
]);

// 比较神奇,由h函数组成的参数已经形成树结构,所有h函数内部无需递归
var myVnode = h('ul', {}, [
    h('li', {}, '语文'),
    h('li', {}, '数学'),
    h('li', {}, [
        h('div', {}, [
            h('span', {}, '英语'),
            h('span', {}, '物理')
        ])
    ])
])
console.log('myVnode', myVnode);

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

感受diff算法

key的作用

key是节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点

代码说明: index.js

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";

// 创建出patch函数
const patch = init([
    // init 模块用来创建具有初始化功能的模块
    classModule,
    propsModule,
    styleModule,
    eventListenersModule
]);

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

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

patch(container, vnode1)

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

])

// 点击按钮时,将vnode1替换成vnode2
btn.onclick = function () {
    patch(vnode1, vnode2)
}

点击按钮前: image.png 点击按钮后: image.png

为了证明实现最小量更新,利用浏览器直接修改li中的文字

image.png

但是有一些问题:

修改vnode2

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

点击按钮前

image.png

点击按钮后,发现修改的结果被重置了

image.png

那是因为DOM的更新顺序是这样的,新的节点在尾部插入: image.png

把虚拟DOM的key属性加上就可以解决这个问题,这样就会直接在头部创建一个DOM,key服务于最小量更新,可以极大提升DOM更新的效率:

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


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

])

只是同一个虚拟节点,才进行精细化比较

只有同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。

如何定义同一个虚拟节点:选择器key相同

案例演示:

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";

// 创建出patch函数
const patch = init([
    // init 模块用来创建具有初始化功能的模块
    classModule,
    propsModule,
    styleModule,
    eventListenersModule
]);

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

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('ol', {}, [
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'D' }, 'D'),

])

// 点击按钮时,将vnode1替换成vnode2
btn.onclick = function () {
    patch(vnode1, vnode2)
}

点击前: image.png

点击后,修改的内容重新更新了。说明是暴力重新创建 image.png

只进行同层比较不会进行跨层比较

diff函数只进行同层比较不会进行跨层比较,即使是同一片虚拟节点,但是跨层了,对不起,精细化比较不diff你,而是暴力删除旧的,然后插入新的 index.js

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";

// 创建出patch函数
const patch = init([
    // init 模块用来创建具有初始化功能的模块
    classModule,
    propsModule,
    styleModule,
    eventListenersModule
]);

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

const vnode1 = h('div', {}, [
    h('p', { key: 'A' }, 'A'),
    h('p', { key: 'B' }, 'B'),
    h('p', { key: 'C' }, 'C'),
    h('p', { key: 'D' }, 'D'),
])

patch(container, vnode1)

const vnode2 = h('div', {}, h('section', {}, [
    h('p', { key: 'A' }, 'A'),
    h('p', { key: 'B' }, 'B'),
    h('p', { key: 'C' }, 'C'),
    h('p', { key: 'D' }, 'D'),
]))

// 点击按钮时,将vnode1替换成vnode2
btn.onclick = function () {
    patch(vnode1, vnode2)
}

模仿上述的调试过程,发现跨层变化也会直接暴力删除再暴力创建

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

  • patch函数被调用时首先会判断oldVnode是虚拟节点还是真实DOM(真实DOM也有可能的,比如第一次上树1时patch(container, vnode1),这个container盒子就是一个真实DOM)
  • 如果是真实节点就将oldVnode包装为虚拟节点
  • 如果是虚拟节点执行下一步判断oldVnode和newVnode是不是同一个节点(key相同且sel相同)
  • 如果是则精细化diff比较
  • 不是则暴力删除旧的、插入新的

patch函数流程图: image.png

如何定义是否同一个节点的大致代码:

image.png

创建节点时,所有子节点需要递归创建的大致代码: image.png

手写第一次上树

什么是第一次上树,就是第一次调用patch方法,且patch方法的第一个参数一定是一个真实DOM

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

patch(container, myVnode1) 

代码: index.js

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

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

patch(container, myVnode1) 

patch.js

import vnode from './vnode'
import createElement from './createElement';
export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数是DOM节点还是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 第一个参数是DOM节点,需要将oldVnode转换成虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
        console.log('oldVnode', oldVnode);
    }
    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
        // 是同一个节点,精细化比较
    } else {
        // 不是同一个节点,暴力插入新的,删除旧的
        createElement(newVnode, oldVnode.elm)
    }
}

createElement.js

// 真正创建节点,将vnode创建为dom,插入到pivot这个元素之前
export default function (vnode, pivot) {
    console.log('目的是把虚拟节点', vnode, '插入到标杆', pivot, '前');
    // 以vnode的sel属性作为标签名创建一个dom节点,当前这个节点还是孤儿节点
    let domNode = document.createElement(vnode.sel)
    // 有子节点还是有文本(本课程为了方便教学只教文本与子元素互斥的情况,其实文本和子元素是可以共存的)
    if (vnode.text !== '' && vnode.children === undefined || vnode.children.length === 0) {
        // 它内部是文字
        domNode.innerText = vnode.text
        // 将孤儿节点上树,让标杆节点的父元素调用insertBefore方法,将新的孤儿节点插入到标杆节点之前
        // 由于第一次上树时,pivot一定是一个真实DOM,所以pivot.parentNode一个是存在的
        pivot.parentNode.insertBefore(domNode, pivot)
    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
        // 递归处理子节点
    }
}

手写递归创建子节点

当patch函数的vnode参数有children属性时,代表其有子节点,这时需要递归创建。

什么时机停止递归:当h函数第三个参数是文本时

此时上一节的createElement函数无法实现递归创建子节点,因为缺乏标杆pivot参数

看这块h函数创建虚拟DOM的代码,发现创建出来的虚拟节点的children是一个数组,无法拿到标杆pivot

// 使用h函数创建虚拟DOM
const myVnode1 = h('h1', {}, [
    h('h2', {}, 1111),
    h('h2', {}, 2222),
    h('h2', {}, 3333),
    h('h2', {}, 4444),
])

综上所述,我们需要对createElement函数进行改造,去除createElement插入新的dom节点的操作,将这个插入新节点操作交给patch函数完成

调整的代码如下: index.js

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

// 使用h函数创建虚拟DOM
const myVnode1 = h('h1', {}, [
    h('h2', {}, 'A'),
    h('h2', {}, 'B'),
    h('h2', {}, 'C'),
    h('div', {}, h('ol', {}, [
        h('li', {}, '语文'),
        h('li', {}, '数学'),
        h('li', {}, '英语'),
    ])),
])

const myVnode2 = h('h2', {}, [
    h('h3', {}, 'A'),
    h('h3', {}, 'B'),
    h('h3', {}, 'C'),
    h('div', {}, h('ol', {}, [
        h('li', {}, '苹果'),
        h('li', {}, '葡萄'),
        h('li', {}, '梨子'),
    ])),
])
const container = document.getElementById('container')

// 使用patch函数渲染虚拟DOM(上树)
patch(container, myVnode1)

const btn = document.getElementById('btn')
btn.onclick = function () {
    console.log('点击按钮');
    patch(myVnode1, myVnode2)
}

createElement.js

// 真正创建节点,将vnode创建为dom,是孤儿节点,不进行插入
export default function createElement(vnode) {
    console.log('目的是把虚拟节点', vnode, '真正变为dom');
    // 以vnode的sel属性作为标签名创建一个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(真实DOM)
    return domNode
}

patch.js

import vnode from './vnode'
import createElement from './createElement';
export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数是DOM节点还是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 第一个参数是DOM节点,需要将oldVnode转换成虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
    }
    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
        // 是同一个节点,精细化比较
        console.log('是同一个节点,精细化比较');
    } else {
        // 不是同一个节点,暴力插入新的,删除旧的
        let newVnodeElm = createElement(newVnode, oldVnode.elm)
        // 插入到老节点之前
        if (oldVnode.elm !== undefined && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}

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

diff处理新旧节点是同一个节点时的流程图: image.png

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

patch.js

import vnode from './vnode'
import createElement from './createElement';
export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数是DOM节点还是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 第一个参数是DOM节点,需要将oldVnode转换成虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
    }
    // 判断oldVnode和newVnode是不是同一个节点
    if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
        // 是同一个节点,精细化比较
        console.log('是同一个节点,精细化比较');
        // 判断新旧vnode是否是同一个对象
        if (oldVnode === newVnode) return
        // 判断newVnode有没有text属性
        if (newVnode.text !== undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
            // 新节点有text属性
            console.log('newVnode有text属性');
            if (oldVnode.text !== newVnode.text) {
                // 新老节点的text不相等,直接替换
                oldVnode.elm.innerText = newVnode.text
            }
        } else {
            // 新节点没有text属性,意味着有children
            console.log('newVnode没有text属性');
            // 判断oldVnode有没有children
            if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
                // 此时就是最复杂的情况,新老就有children
            } else {
                console.log('老的没有,新的有children');
                // 老的没有,新的有children
                // 清空老的节点的内容
                oldVnode.elm.innerText = ''
                for (let i = 0; i < newVnode.children.length; i++) {
                    let dom = createElement(newVnode.children[i])
                    oldVnode.elm.appendChild(dom)
                }
            }
        }
    } else {
        // 不是同一个节点,暴力插入新的,删除旧的
        let newVnodeElm = createElement(newVnode)
        // 插入到老节点之前
        if (oldVnode.elm !== undefined && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}

diff算法的子节点更新策略(重点)

经典的diff算法优化策略

四种命中查找(有顺序关系,都没有命中则循环,命中一种就不再进行命中判断了。所谓的命中就是判断两个指针指向的节点是否是同一个节点,如果未命中则执行下一个命中条件,如此循环往复。并且如果命中的化,前指针往后移动,后指针往前移动

并且这个循环条件是while(新前<=新后&&旧前<=旧后)

  1. 新前与旧前
  2. 新后与旧后
  3. 新后于旧前
  4. 新前与旧后

image.png

什么意思呢?我们现在需要准备四个指针,分别指向新子节点的头部、新子节点的尾部、旧子节点的头部、旧子节点的尾部(面试常考一定要会) image.png

这一小节课程讲得很烂,看这篇文章吧

juejin.cn/post/695471…

手写子节点更新策略

第一部分

1.将patch函数中处理相同节点的部分抽离为一个函数patchVnode patch.js

import vnode from './vnode'
import createElement from './createElement';
import patchVnode from './patchVNode.js'
export default function patch(oldVnode, newVnode) {
    // 判断传入的第一个参数是DOM节点还是虚拟节点
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 第一个参数是DOM节点,需要将oldVnode转换成虚拟节点
        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 !== undefined && newVnodeElm) {
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}

patchVNode.js 专门用来处理diff更新时新旧节点为相同节点时的情况

import createElement from './createElement'
import updateChildren from './updateChildren'

// patchVNode.js 专门用来处理diff更新时新旧节点为相同节点时的情况
export default function patchVNode(oldVnode, newVnode) {
    // 判断新旧vnode是否是同一个对象
    if (oldVnode === newVnode) return
    // 判断newVnode有没有text属性
    if (newVnode.text !== undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
        // 新节点有text属性
        console.log('newVnode有text属性');
        if (oldVnode.text !== newVnode.text) {
            // 新老节点的text不相等,直接替换
            oldVnode.elm.innerText = newVnode.text
        }
    } else {
        // 新节点没有text属性,意味着有children
        console.log('newVnode没有text属性');
        // 判断oldVnode有没有children
        if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
            // 此时就是最复杂的情况,新老都有children
            console.log('最复杂的情况来了');
            updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
        } else {
            console.log('老的没有,新的有children');
            // 老的没有,新的有children
            // 清空老的节点的内容        
            oldVnode.elm.innerText = ''
            for (let i = 0; i < newVnode.children.length; i++) {
                let dom = createElement(newVnode.children[i])
                oldVnode.elm.appendChild(dom)
            }
        }
    }
}

updateChildren.js

import patchVNode from "./patchVNode";
//updateChildren 实现子节点更新 parentElm 父节点 oldCh 老节点的子节点 newCh 新节点的子节点
export default function updateChildren(parentElm, 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];

    // 开始大while了
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        console.log('循环中-------');
        // 四种命中条件
        if (checkSameVnode(oldStartVnode, newStartVnode)) {
            console.log('命中1');
            // 新前与旧前
            // 递归 使用patchVNode
            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)
            // 将旧前插入到旧后之后
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (checkSameVnode(oldEndVnode, newStartVnode)) {
            console.log('命中4');
            // 新前与旧后 
            patchVNode(oldEndVnode, newStartVnode)
            // 将旧后插入到旧前之前
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[--newStartIdx];
        }
    }
}

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

第二部分 循环结束后对剩余节点做新增删除处理

import patchVNode from "./patchVNode";
import createElement from "./createElement";
//updateChildren 实现子节点更新 parentElm 父节点 oldCh 老节点的子节点 newCh 新节点的子节点
export default function updateChildren(parentElm, 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];

    // 开始大while了
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        console.log('循环中-------');
        // 四种命中条件
        if (checkSameVnode(oldStartVnode, newStartVnode)) {
            console.log('命中1');
            // 新前与旧前
            // 递归 使用patchVNode
            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)
            // 将旧前插入到旧后之后
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (checkSameVnode(oldEndVnode, newStartVnode)) {
            console.log('命中4');
            // 新前与旧后 
            patchVNode(oldEndVnode, newStartVnode)
            // 将旧后插入到旧前之前
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[--newStartIdx];
        } else {
            console.log('没命中');
        }
    }
    //处理while循环结束后的情况 
    if (newStartIdx <= newEndIdx) {
        // 如果旧节点先循环完毕,新节点还有剩余,需要新增
        console.log('新节点有剩余没有处理');
        // 插入的标杆
        const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
        console.log(before);
        for (let i = newStartIdx; i <= newEndIdx; i++) {
            // insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild方法一致
            // newCh[i]现在还没有真正的DOM,所以要调用createElement() 函数变为DOM
            parentElm.insertBefore(createElement(newCh[i]), before)
        };
    } else if (oldStartIdx <= oldEndIdx) {
        // 如果新节点先循环完毕,老节点还有剩余,需要删除
        console.log('old还有剩余节点没有处理')
        // 删除多余的DOM
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
            parentElm.removeChild(oldCh[i].elm)
        }
    }
}

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

第三部分 四种命中条件都没有命中的处理

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('updateCh')
    console.log(oldCh, newCh);

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

    let keyMap = null;

    //开始while
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        console.log('⭐')
        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);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        } else {
            //四种都没有找到
            //制作key的map一个映射对象,这样就不用每次都遍历老对象
            if (!keyMap) {
                keyMap = {};
                // 从 oldStartIdx 开始,到oldEndIdx结束,创建keyMap映射对象
                for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                    const key = oldCh[i].key;
                    if (key != undefined) {
                        keyMap[key] = i;
                    }
                }
            }
            //寻找当前这项newStarIdx在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]
        }
    }
    //继续看看有没有剩余的
    if (newStartIdx <= newEndIdx) {
        console.log('new还有剩余节点没处理,要加项,要把所有剩余的节点都插入到oldStartIdx之前');
        //遍历新的newCh,添加到老的没有处理之前
        for (let i = newStartIdx; i <= newEndIdx; i++) {
            //insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了
            //newCh[i]现在还没有成为真正的DOM,要调用createElment()函数变为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)
            }
        }
    }
}