jieye の 数字花园

Search

Search IconIcon to open search

虚拟DOM和diff算法

Last updated Nov 1, 2021

# 虚拟DOM和diff算法

流程

# snabbdom安装配置

snabbdom是瑞典语单词,单词原意“速度”,在IT方面是著名的虚拟DOM库,是diff算法的鼻祖,Vue源码就借鉴了snabbdom。官方git:https://github.com/snabbdom/snabbdom。

在git上的snabbdom源码是用TypeScript写的,git上并不提供编译好的JavaScript版本,因此如果要直接使用build出来的JavaScript版的snabbdom库,可以从npm上下载:

npm i -D snabbdom

snabbdom库是DOM库,当然不能在nodejs环境运行,所以我们需要搭建webpack和webpack-dev-server开发环境,好消息是不需要安装任何loader。

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

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

webpack.config.js配置为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 从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'
    }
};

相应的index.html为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button id="btn">按我改变DOM</button>
    <div id="container"></div>
    
    <script src="/xuni/bundle.js"></script>
</body>
</html>

# 虚拟DOM简介

真实DOM和虚拟DOM的区别:

image-20210627135248024

diff自然是发生在虚拟DOM上的:

image-20210627135317534

关于DOM如何变为虚拟DOM,属于模板编译原理范畴,不在本笔记学习范围之内

# 生成虚拟DOM(h函数)

h函数用来产生虚拟节点(vnode),比如这样调用h函数:

h('a',{ props:{ href:'http://www.baidu.com'}},'百度')

将得到这样的虚拟节点:

{"sel":"a","data":{props:{href:'http://www.baidu.com'}},"text":"百度"}

渲染回页面后真实的DOM为:

<ahref="http://www.baidu.com">百度</a>


一个虚拟节点往往有这些属性:

1
2
3
4
5
6
7
8
{
    children:undefined,
    data:{},
	elm:undefined,
	key:undefined,
	sel:"div",
	text:"我是一个盒子"
}
1
2
3
4
5
6
7
// 函数的功能非常简单,就是把传入的5个参数组合成对象返回
export default function(sel, data, children, text, elm) {
    const key = data.key;
    return {
        sel, data, children, text, elm, key
    };
}

h函数可以嵌套使用,从而得到虚拟DOM树:

image-20210627140256839

image-20210627140309003

手写h函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 编写一个低配版本的h函数,这个函数必须接受3个参数,缺一不可
// 相当于它的重载功能较弱。
// 也就是说,调用的时候形态必须是下面的三种之一:
// 形态① h('div', {}, '文字')
// 形态② h('div', {}, [])
// 形态③ h('div', {}, h())
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('传入的第三个参数类型不对');
    }
};