虚拟DOM 和 diff 算法
- 研究1:虚拟DOM如何被渲染函数(h函数)产生? — 我们手写h函数
- 研究2:diff 算法原理?— 我们要手写diff 算法
- 研究3:虚拟DOM 如何通过diff 变成真正的DOM的—事实上,虚拟DOM变成真正的,是涵盖在diff算法里面的
项目创建
- 创建文件夹:vnode
- 使用
npm init
管理vnode 文件 - 安装 snabbdom
npm i -S snabbdom
- 安装webpack 相关包
npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
- 新增 webpack.config.js 文件 并设置相关配置
const path = require('path')
module.exports = {
//入口
entry: './src/index.js',
output: {
publicPath: 'xuni',
filename: 'bundle.js'
},
devServer: {
port: '8080',
contentBase: 'www'
}
}
- 新增 src 文件夹和www文件夹
- 修改package.json
修改为
"scripts": {
"dev": "webpack-dev-server"
},
- 在www/index.html 文件中引入bundle.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>你好</div>
<script src="/xuni/bundle.js"></script>
</body>
</html>
snabbdom简介
虚拟DOM
虚拟DOM:用javascript对象描述DOM 的层次结构。DOM 中的一切都在虚拟DOM 中有对应的属性
在这里插入图片描述
diff 算法是发生在虚拟DOM 上的,新虚拟DOM 和老虚拟DOM进行diff(精细化比较),算出应该如何最小量更新,最后反映到真正的DOM 上。
h函数用来产生虚拟节点(vnode)
一个虚拟节点有哪些属性
{
children:undefind, //子元素
data:{},//属性、数据
elm:undefind,//虚拟DOM的真正DOM节点,如果是undefind表示虚拟节点还没有上树
key:undefind,//虚拟节点的唯一标识
sel:'div',//标签名
text:'我是一个盒子',//文本
}
h函数的基本使用
- 如何调用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>
h函数是可以嵌套使用的,从而得到虚拟DOM树
- 比如这样嵌套使用h函数
h('url',{},[
h('li',{},'牛奶'),
h('li',{},'咖啡'),
h('li',{},'可乐'),
])
- 得到这样的虚拟DOM树
{
"sel":"url",
"data":{},
"children":[
{'sel':'li','text':'牛奶'},
{'sel':'li','text':'咖啡'},
{'sel':'li','text':'可乐'},
]
}
手写h函数
创建vNode.js文件
// 函数的功能十分简单,就是把传入的5个参数组合成对象返回
export default function (sel, data, children, text, elm) {
return {
sel, data, children, text, elm
}
}
创建h.js文件
import vNode from './vNode'
/**
1. 编写低配版的h函数,这个函数必须接受3个参数,缺一不可
2.相当于它的重载功能较弱
3.也就是说,调用的形态必须是下面三种之一
情况1: h('div',{},'文字')
情况2: h('div',{},[])
情况3: h('div',{},h())
*/
/**
*
* @param {*} sel 选择器
* @param {*} data 属性
* @param {*} c 不确定
*/
export default function (sel, data, c) {
//首先检查参数个数是否为三个
if (arguments.length !== 3) throw new Error('对不起,h函数必须传3个参数,我们是低配版h函数');
//检查参数c的类型
if (typeof (c) === 'string' || typeof (c) === 'number') {
//1. c的类型为字符串类型或者数字类型
return vNode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
let children = []
//2. c的类型是数组
//遍历c
for (let i = 0; i < c.length; i++) {
// 判断 c数组对象中的每一项是不是一个h函数; 第一:h函数返回的肯定是一个对象,第二:h函数对象中肯定有sel属性
if (!c[i] instanceof Object && c[i].hasOwnProperty('sel')) throw new Error('传入的数组对象中有项不是h函数');
//这里不需要c[i],因为你的测试语句中已经有了执行
//此时只需要收集即可(如果是一个h函数,则向children 添加c[i])
children.push(c[i])
}
return vNode(sel, data, children, undefined, undefined)
} else if (c instanceof Object && c.hasOwnProperty('sel')) {
//2. c的类型是一个h函数 第一:h函数返回的肯定是一个对象,第二:h函数对象中肯定有sel属性
return vNode(sel, data, [c], undefined, undefined)
} else {
throw new Error('对不起,h函数传入的第三个参数类型不对');
}
}
diff 算法的心得
-
最小量更新太厉害啦!真的是最小量更新!当然,key很重要。key是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点
-
只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。
延伸问题:如何定义是同一个虚拟节点?
答:选择器相同且key相同
-
只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是跨层了,对不起,精细化比较不diff你。而是暴力删除旧的、然后插入新的。
diff 并不是那么的‘无微不至’啊!真的影响效率吗?
答:上面的操作,在实际vue开发中,基本不会遇见,所以这是合理的优化机制
diff 算法流程图
-
如何定义新老虚拟DOM是 ‘同一个节点’?
新老节点的key相同并且,sel(选择器)相同
- 创建节点时,所有子节点需要递归创建的
diff算法的子节点更新策略
四种命中查找
- 新前与旧前
- 新后与旧后
- 新后与旧前(此种情况发生,要移动节点,移动新前指向的这个节点到老节点的旧后的后面)
- 新前与旧后(此种情况发生,要移动节点,移动新前指向的这个节点到老节点的旧前的前面)
命中一种就不再进行命中判断
如果老节点比新节点先遍历完,说明是老节点后面需要增加新节点里没有被遍历的子节点
如果新节点比老节点先遍历完,说明是老节点需要删除新节点里没有被遍历的子节点
面试题: 如何理解 虚拟DOM 和 diff 算法
虚拟DOM 其实就是 一个JS 对象,我们用它来描述真实的DOM。虚拟DOM 和diff 算法配合使用,可以优化浏览器渲染的性能。但是更重要的一点虚拟DOM可以用来做跨端应用。
diff 算法 底层其实是调用了patch 方法,patch 方法需要传递两个参数,第一个参数是老节点,第二个参数是新节点。当调用patch方法是,我们会首先判断老节点是虚拟DOM还是真实DOM。如果是真实DOM,我会会把这个真实DOM 转换成虚拟DOM。 因为diff算法比较本来就是比较新的虚拟DOM和老的虚拟DOM的差别。然后我会再比较老的虚拟DOM 和 新的虚拟DOM是不是同一个节点的,也就是比较它们的sel(选择器)和key 是否相同。如果不相同,我们则暴力删除旧的,添加新的。如果相同,我们就要进行精细化比较。diff算法的子节点更新策略就是判断四个命中。分别是新前与后前,新旧与后旧,新旧与旧前,新前与后旧。