

Vue源码之虚拟DOM和diff算法(一) 使用snabbdom
source link: https://www.clzczh.top/2022/04/04/vue-virtualDOM-diff-1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Vue源码之虚拟DOM和diff算法(一) 使用snabbdom
什么是虚拟DOM和diff算法
diff算法简介

要把左图装修成右图的样子。(哪里不同?仔细找)
有两种方案。
方案一:拆掉重建(效率低,代价大)
方案二:diff(精细化比对,最小量更新)
怎么看都应该会选择方案二。
那么在Vue中使用 diff
的情景呢?

上图就是在Vue中使用 diff
的情景(比如左图中,有一些元素的 v-if
为false
,所以不显示,而右图中, v-if
为 true
)
虚拟DOM简介
虚拟DOM:用来描述DOM的层次结构的js对象。真实DOM中的一切属性在虚拟DOM中都存在。

diff是发生在虚拟DOM上的

优点:
- 减少对真实DOM的操作
- 虚拟 DOM 本质上是 JavaScript 对象,可以跨平台,比如服务器渲染等
snabbdom
snabbdom是著名的虚拟DOM库,是diff
算法的鼻祖。(Vue源码也借鉴了 snabbdom
)
npm install snabbdom
webpack配置
上一篇中,有 webpack
配置可查看Vue源码系列的上一篇文章。
webpack.config.js
const path = require('path');
module.exports = {
entry: path.join(__dirname, 'src', 'index.js'),
mode: 'development',
output: {
filename: 'bundle.js',
// 虚拟打包路径,bundle.js文件没有真正的生成
publicPath: "/virtual/"
},
devServer: {
// 静态文件根目录
static: path.join(__dirname, 'www'),
// 不压缩
compress: false,
port: 8080,
}
}
h函数使用
h函数用来创建虚拟节点(vnode)

参数介绍:
- 第一个参数:是生成的虚拟节点对应DOM节点的标签名
- 第二个参数:一个对象,虚拟节点的属性(可选)
- 第三个参数:标签中的内容
h函数体验
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const myVnode = h('a', {
props: {
href: 'https://clz.vercel.app'
}
}, '赤蓝紫')
console.log(myVnode)

虚拟DOM节点属性介绍:
- children: 子元素,没有则为
undefined
- data(对象形式): 类名、属性、样式、事件(对象形式)
- elm: 对应的真实DOM节点(如果没有对应的,则为
undefined
) - key:唯一标识
- sel:选择器
- text:文字
搭配 patch
函数生成真实DOM节点
通过引入的 init
函数把所有的模块(类名模块、属性模块、样式模块、事件监听模块)作为参数(少的话,则上树后也会少,比如少事件监听模块,上树后,事件将不再生效)
const patch = init([classModule, propsModule, styleModule, eventListenersModule])
container
只是占位符,上树后会消失
// container只是占位符,上树后会消失
const container = document.getElementById('container')
patch(container, myVnode) // 上树(第一个参数如果不是虚拟节点,则是执行上树操作,否则是diff算法,第一个参数是旧虚拟节点,第二个参数是新虚拟节点)
完整版
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 加载模块,,创建出patch函数。没有对应模块的话,上树后,也对应没有。比如少事件监听模块,上树后,事件将不再生效
// 类名模块、属性模块、样式模块、事件监听模块
const patch = init([classModule, propsModule, styleModule, eventListenersModule])
const myVnode = h('button', {
class: { // 类名
"btn": true
},
props: { // 属性, id也在这里面
id: 'btn',
title: '赤蓝紫'
},
style: { // 样式
backgroundColor: 'red',
border: 0,
color: '#fff',
},
on: { // 事件监听
click: function () {
location.assign('https://clz.vercel.app')
}
}
}, '赤蓝紫')
console.log(myVnode)
// container只是占位符,上树后会消失
const container = document.getElementById('container')
patch(container, myVnode) // 上树
h函数嵌套使用
h函数可以嵌套使用,从而得到虚拟DOM树

动手实践
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([classModule, propsModule, styleModule, eventListenersModule])
const myVnode = h('ul', [ // 没有数据参数
h('li', '赤'), // 可以没有数据参数
h('li', {}, '蓝'), // 数据参数可为空
h('li', h('span', '紫')) // 内容参数为调用h函数且只有一个时,可以不是数组形式
])
console.log(myVnode)
const container = document.getElementById('container')
patch(container, myVnode)

手写h函数
编写vnode函数
vnode
功能:把传入的参数组合成对象后返回
src \ mysnabbdom \ vnode.js
export default function (sel, data, children, text, elm) {
return {
sel,
data,
children,
text,
elm
}
}
本次手写的是低配版本的h函数,必须接收3个参数
调用可以有以下三种形式
h(‘div’, {}, ‘文字’)
h(‘div’, {}, [])
h(‘div’, {}, h())
实现第一种形式
直接调用 vnode
函数,返回即可
src \ mysnabbdom \ h.js
import vnode from './vnode.js'
export default function (sel, data, content) {
if (arguments.length !== 3) {
throw new Error('这是低配版h函数, 必须接收3个参数')
} else if (typeof content === 'string' || typeof content === 'number') {
// 形式1
return vnode(sel, data, undefined, content, undefined)
} else if (Array.isArray(content)) {
// 形式2
} else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
// 形式3:因为h函数最终会返回一个一定会有`sel`属性的对象
} else {
throw new Error('传入的第三个参数类型不对')
}
}
src \ index.js
import h from './mysnabbdom/h.js'
console.log(h('div', {}, '文字'))

实现第二种形式
src \ mysnabbdom \ h.js
import vnode from './vnode.js'
export default function (sel, data, content) {
if (arguments.length !== 3) {
throw new Error('这是低配版h函数, 必须接收3个参数')
} else if (typeof content === 'string' || typeof content === 'number') {
// 形式1
return vnode(sel, data, undefined, content, undefined)
} else if (Array.isArray(content)) {
// 形式2
const children = []
for (let i = 0; i < content.length; i++) {
if (!(typeof content[i] === 'object' && content[i].hasOwnProperty('sel'))) {
throw new Error('传入的数组中又有不是调用h函数的')
}
children.push(content[i]) // 因为传的数组里的元素就是调用h函数的,即得到的已经是处理过后返回的对象了
}
return vnode(sel, data, children, undefined, undefined)
} else if (typeof content === 'object' && content.hasOwnProperty('sel')) {
// 形式3:因为h函数最终会返回一个一定会有`sel`属性的对象
} else {
throw new Error('传入的第三个参数类型不对')
}
}
src \ index.js
import h from './mysnabbdom/h.js'
console.log(h('div', {}, [
h('p', {}, '赤'),
h('p', {}, '蓝'),
h('p', {}, '紫'),
h('p', {}, [
h('span', {}, '黑'),
h('span', {}, '白')
])
]))

第三种形式
src \ mysnabbdom \ h.js
/*
* 低配版本的h函数,必须接收3个参数
* 调用可以有以下三种形式
* 1. h('div', {}, '文字')
* 2. h('div', {}, [])
* 3. h('div', {}, h())
*/
import { h } from 'snabbdom'
import vnode from './vnode.js'
export default function (sel, data, content) {
if (arguments.length !== 3) {
throw new Error('这是低配版h函数, 必须接收3个参数')
} else if (typeof content === 'string' || typeof content === 'number') {
// 形式1
return vnode(sel, data, undefined, content, undefined)
} else if (Array.isArray(content)) {
// 形式2
const children = []
for (let i = 0; i < content.length; i++) {
if (!(typeof content[i] === 'object' && content[i].hasOwnProperty('sel'))) {
throw new Error('传入的数组中又有不是调用h函数的')
}
children.push(content[i]) // 因为传的数组里的元素就是调用h函数的,即得到的已经是处理过后返回的对象了
}
return vnode(sel, data, children, undefined, undefined)
} else if (typeof content === 'object' && content.hasOwnProperty('sel')) {
// 形式3:因为h函数最终会返回一个一定会有`sel`属性的对象
return vnode(sel, data, [content], undefined, undefined) //把它当成只有一个元素的数组来处理
} else {
throw new Error('传入的第三个参数类型不对')
}
}
src \ index.js
import h from './mysnabbdom/h.js'
console.log(h('div', {}, h('span', {}, '赤蓝紫')))

diff算法初体验
在后面插入
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 加载模块,创建出patch函数。没有对应模块的话,上树后,也对应没有。比如少事件监听模块,上树后,事件将不再生效
// 类名模块、属性模块、样式模块、事件监听模块
const patch = init([classModule, propsModule, styleModule, eventListenersModule])
const myVnode1 = h('ul', {}, [
h('li', {}, 'a'),
h('li', {}, 'b'),
h('li', {}, 'c'),
h('li', {}, 'd'),
])
const myVnode2 = h('ul', {}, [
h('li', {}, 'a'),
h('li', {}, 'b'),
h('li', {}, 'c'),
h('li', {}, 'd'),
h('li', {}, 'e')
])
const container = document.getElementById('container')
patch(container, myVnode1) // 上树
const btn = document.getElementById('btn')
btn.addEventListener('click', function () {
patch(myVnode1, myVnode2) // 点击后,从myVnode1变为myVnode2
})

怎么知道是不是真的是最小量更新呢?
可以用老师用的巧妙法:在 DevTools
的 Elements
面板修改内容,查看有没有变化

可以发现,确确实实是最小量更新。仔细看上面的图,发现不需要修改 Elements
面板,有更新的话,会变紫,闪烁一下
在前面插入
那么,接下来就试一下在开头加入新节点的情况咯
const myVnode2 = h('ul', {}, [
h('li', {}, 'e'),
h('li', {}, 'a'),
h('li', {}, 'b'),
h('li', {}, 'c'),
h('li', {}, 'd')
])

可以说是,上面的情况压根就不是最小量更新。
这是为什么呢?这时候就需要 key
的闪亮登场了
没有key的时候:会先把节点插到最后,再把插入的节点移动到要去的位置,其他节点也需要移动到要去的位置
在中间插入
可以再来测试一下
const myVnode2 = h('ul', {}, [
h('li', {}, 'a'),
h('li', {}, 'e'),
h('li', {}, 'b'),
h('li', {}, 'c'),
h('li', {}, 'd')
])
这时候,只有a不会变化,因为e插入的位置不会影响到a
使用key,真正实现最小量更新
有 key
的时候,就不一样了,每一个虚拟节点都有一个唯一标识,所以能够精准定位,真正实现最小化更新
const myVnode1 = h('ul', {}, [
h('li', { key: 'a' }, 'a'),
h('li', { key: 'b' }, 'b'),
h('li', { key: 'c' }, 'c'),
h('li', { key: 'd' }, 'd'),
])
const myVnode2 = 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,并完全调换位置
const myVnode1 = h('ul', {}, [
h('li', { key: 'a' }, 'a'),
h('li', { key: 'b' }, 'b'),
h('li', { key: 'c' }, 'c'),
h('li', { key: 'd' }, 'd'),
])
const myVnode2 = h('ul', {}, [
h('li', { key: 'd' }, 'd'),
h('li', { key: 'a' }, 'a'),
h('li', { key: 'e' }, 'e'),
h('li', { key: 'b' }, 'b'),
h('li', { key: 'c' }, 'c'),
])

还是最小量更新。另外,闪烁法还是不太可靠,建议还是修改Element法
最小量更新:需要
key
,key
是节点的唯一标识,用于告诉diff
算法,在更改前后是同一个DOM节点只有是同一个虚拟节点,才会进行精细化比较,否则就是暴力删除旧的、插入新的。如上面的例子中,从
ul
变为ol
同一个虚拟节点:选择器相同且
key
相同// 供测试用:可以使用回上面说的修改Elemens面板法(不过,下面的例子实际开发遇到的可能性很小) const myVnode1 = h('ul', { key: 'ul1' }, [ h('li', { key: 'a' }, 'a'), h('li', { key: 'b' }, 'b'), h('li', { key: 'c' }, 'c'), h('li', { key: 'd' }, 'd'), ]) const myVnode2 = h('ul', { key: 'ul2' }, [ 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') ])
只进行同层比较,不进行跨层比较。如果跨层了,则依然是暴力删除旧的,然后插入新的
// 下面的例子实际开发遇到的可能性很小 const myVnode1 = h('div', { key: 'box' }, [ h('p', { key: 'a' }, 'a'), h('p', { key: 'b' }, 'b'), h('p', { key: 'c' }, 'c'), h('p', { key: 'd' }, 'd'), ]) const myVnode2 = h('div', { key: 'box' }, h('section', { key: 'section' }, [ h('p', { key: 'e' }, 'e'), h('p', { key: 'a' }, 'a'), h('p', { key: 'b' }, 'b'), h('p', { key: 'c' }, 'c'), h('p', { key: 'd' }, 'd') ]) )
diff
Recommend
-
37
作者: steins from 迅雷前端 原文地址:github.com/linrui1994/… 随着 React Vue 等框架的流行,Virtual DOM 也越来越火,snabbdom 是其中一种实现,而且 Vue 2.x 版本的 Virtual D
-
43
什么是虚拟Dom 我们知道我们平时的页面都是有很多Dom组成,那虚拟Dom(virtual dom)到底是什么,简单来讲,就是将真实的dom节点用JavaScript来模拟出来,而Dom变化的对比,放到 Js 层来做。 下面是一个传统的dom节点,大家肯定都不
-
34
原文链接 原文写于 2015-07-31,虽然时间比较久远,但是对于我们理解虚拟 DOM 和 view 层之间的关系还...
-
34
1. 前言 diff 算法是一种通过同层的树节点进行比较的高效算法,避免了对树进行逐层搜索遍历,所以时间复杂度只有 O(n)。diff 算法的在很多场景下都有应用,例如在 vue 虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较更新时,就...
-
15
DOM Diff Vue 创建视图分为俩种情况: 首次渲染,会用组件 template 转换成的真实 DOM 来替换应用中的根元素 当数据更新后,视图重新渲染...
-
5
snabbdom 本文的snabbdom源码分析采用的是0.54版本(即未用ts重写前的最后一版) snabbdom被用作vue的虚拟dom。本文的一个目的就是对于进入vue源码预备。 本文大致讲解,而不会完全细化至代码行数讲解 文件(以下只指出需要阅读...
-
8
各位小伙伴新年好啊~新的一年又要开始了,继续努力加油… ~ 求关注,求收藏,求点赞,如果发现博主有写的不合理的地方请及时告知,谢谢~
-
5
文章目录 一、一些必要的概念解释1、什么是虚拟 DOM2、什么是 diff 算法3、snabbdom 是什么二、snabbdom 中 diff 算法...
-
5
Vue源码之虚拟DOM和diff算法(二) 手写diff算法个人练习结果仓库(持续更新):Vue源码解析 patch函数简要流程
-
8
vue 和 react 都是基于 vdom 的
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK