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仓库
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',
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 只是占位符,上树后会消失
const container = document.getElementById('container')
patch(container, myVnode)
完整版
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([classModule, propsModule, styleModule, eventListenersModule])
const myVnode = h('button', {
class: {
"btn": true
},
props: {
id: 'btn',
title: '赤蓝紫'
},
style: {
backgroundColor: 'red',
border: 0,
color: '#fff',
},
on: {
click: function () {
location.assign('https://clz.vercel.app')
}
}
}, '赤蓝紫')
console.log(myVnode)
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', '紫'))
])
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') {
return vnode(sel, data, undefined, content, undefined)
} else if (Array.isArray(content)) {
} else if (typeof c === 'object' && c.hasOwnProperty('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') {
return vnode(sel, data, undefined, content, undefined)
} else if (Array.isArray(content)) {
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])
}
return vnode(sel, data, children, undefined, undefined)
} else if (typeof content === 'object' && content.hasOwnProperty('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
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') {
return vnode(sel, data, undefined, content, undefined)
} else if (Array.isArray(content)) {
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])
}
return vnode(sel, data, children, undefined, undefined)
} else if (typeof content === 'object' && content.hasOwnProperty('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";
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)
})
怎么知道是不是真的是最小量更新呢?
可以用老师用的巧妙法:在 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 相同
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')
])
)
|