JS引擎执行JS代码是很快的,比直接操作真实DOM要快的多。数据改变 --> 虚拟DOM(计算变更)—> 操作真实DOM —> 视图更新
在jquery时代,数据改变 —> 操作真实DOM —> 视图更新,是直接操作真实的DOM。vue框架引入了虚拟DOM
什么是虚拟DOM?
为什么要有虚拟DOM
diff算法是发生在虚拟DOM上的,新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出应该如何最小量更新,最后反映到真正的DOM上。
从真实DOM变成虚拟DOM属于模板编译的内容。diff算法研究最小量更新,并将虚拟DOM变成真实的DOM
npm i snabbdom
h函数用来产生虚拟节点(vnode),由vnode组成的树就是虚拟DOM树
Vnode的优点
VNode
因为是JS
对象,不管Node
还是浏览器,都可以统一操作,从而获得了服务端渲染、原生渲染、手写渲染函数等能力。DOM
,任何页面的变化,都只使用VNode
进行操作对比,只需要在最后一步挂载更新DOM
,不需要频繁操作DOM
,从而提高页面性能。虚拟节点有哪些属性
children
: 值可能是undefined(是undefined表示没有子元素),也可能使数组data
: {}elm
: undefined。elm是undefined说明这个节点还没有在DOM树上key
: undefinedsel
: “div”。选择器text
:文字描述h函数可以产生虚拟节点,h函数可以嵌套使用,从而得到虚拟DOM树。第二个参数可以省略。第三个参数可以是文字、数组、或者h函数
patch函数,让虚拟结点上树
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 创建patch函数
let patch = init([classModule, propsModule, styleModule, eventListenersModule]);
// 创建一个虚拟节点,但是它还没有在DOM树上。要想把它放在DOM树上,需要patch函数
let vnode1 = h('a', {
props: {
href: 'http://www.baidu.com',target:'_blank'}}, '百度一下')
let container = document.getElementById('container');
// 让虚拟结点上树,patch函数只能让一个虚拟结点上树。如果vnode2和vnode3要上树,需要把这个注释掉
patch(container, vnode1);
let vnode2 = h('div',{
class:{
"box":true}},'我是一个盒子');
let vnode3 = h('ul',{
},[
h('li','苹果'), // 第二个参数可以没有
h('li','香蕉'), // 这里已经调用了h函数
h('li',[
h('p',{
},'桔子'),
h('p',{
},'哈哈')
]),
h('li','西瓜'),
h('li',h('span','火龙果')) // 如果children只有一个子元素,第三个参数可以不要数组
]);
h函数有很多种用法,第二个和第三个参数都可以省略。h函数源码中对参数是否存在做了很多的判断,在这里 我们只实现有三个参数的h函数。
// vnode.js
export default function vnode(sel, data, children, text, elm) {
return {
sel, data, children, text, elm};
}
import vnode from './vnode';
// h函数---------------------------------------------------------------
// 编写一个低配版本的h函数,这个函数必须接受3个参数,缺一不可。相当于它的重载功能较弱。
// 形态1 h('div', {},‘文字')
// 形态2 h('div', {},[h(),h()])
// 形态3 h('div', {},h())
export default function (sel, data, c) {
if (arguments.length !== 3) {
throw new Error('h函数必须传入三个参数')
}
// 检查第三个参数c的类型
if (typeof c === 'string' || typeof c === 'number') {
// 形态1
return vnode(sel, data, undefined, c, undefined);
} else if (Array.isArray(c)) {
// 形态2
let children = [];
for (let i = 0; i < c.length; i++) {
if (!(typeof c[i] === 'object' && c[i].hasOwnProperty('sel'))) {
throw new Error('数组参数中有某一项不是h函数')
}
// 这里不用执行c[i],因为你的调用语句中已经有了执行,此时只需要收集好就可以了
children.push(c[i]);
}
return vnode(sel, data, children, undefined, undefined);
} else if (typeof c === 'object' && c.hasOwnProperty('sel')) {
// 形态3,第三个参数是h函数,而h函数会返回一个vnode对象(因为直接调用了)
// 说明传入的c是children中唯一的元素
let children = [c];
return vnode(sel, data, children, undefined, undefined);
} else {
throw new Error('传入的第三个参数类型不对')
}
}
// 测试
import h from './snabbdom/h';
console.log(h('div',{
},[
h('p',{
},'苹果'),
h('p',{
},'香蕉'),
h('p',{
},'橘子')
]));
console.log('---------------------');
console.log(h('div',{
},h('p',{
},'橘子')));
使用snabbdom库
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 创建patch函数
let patch = init([classModule, propsModule, styleModule, eventListenersModule]);
let vnode1 = h('ul', {
}, [
h('li', {
}, 'A'),
h('li', {
}, 'B'),
h('li', {
}, 'C'),
h('li', {
}, 'D')
]);
let container = document.getElementById('container');
let btn = document.getElementById('btn');
patch(container, vnode1);
let vnode2 = h('ul', {
}, [
h('li', {
}, 'A'),
h('li', {
}, 'B'),
h('li', {
}, 'C'),
h('li', {
}, 'D'),
h('li', {
}, 'E')
]);
// 点击按钮时,将vnode1变为vnode2
btn.addEventListener('click',function () {
patch(vnode1,vnode2);
})
测试页面
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页title>
head>
<body>
<button id="btn">改变DOMbutton>
<div id="container">div>
body>
html>
点击按钮后,将虚拟结点vnode1替换为vnode2,两者的区别在于vnode2比vnode1在最后边多了一个li
,效果如图所示,进行了最小量更新:
如果我们把新增的li添加到最上边再看一下效果:
let vnode2 = h('ul', {
}, [
h('li', {
}, 'E'),
h('li', {
}, 'A'),
h('li', {
}, 'B'),
h('li', {
}, 'C'),
h('li', {
}, 'D')
]);
这次它在更新的时候是在最后插入了一个新的li,然后把原来的A改成了E,把B改成了A,把C改成了B,把D改成了C,最后一个li的文字是D。为什么呢?原因在于我们没有给这些结构加key
(回想一下vue中key
的作用),加上key再试一下:
let vnode1 = h('ul', {
}, [
h('li', {
key: 'A'}, 'A'),
h('li', {
key: 'B'}, 'B'),
h('li', {
key: 'C'}, 'C'),
h('li', {
key: 'D'}, 'D')
]);
let 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很重要。key是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点。如果没有key,它会销毁旧的,重新创建新的。
diff
最小量更新算法。diff算法可以进行精细化比对,实现最小量更新。
只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。延伸问题:如何定义是同一个虚拟节点?答:选择器相同且key相同。
只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是跨层了,此时diff算法不进行精细化比较。而是暴力删除旧的、然后插入新的。只要是在同一层进行比较,比如调换顺序,添加删除节点,diff算法就可以进行最小量更新。
// div中有4个p标签
let vnode1 = h('div', {
}, [
h('p', {
key:'A'}, 'A'),
h('p', {
key:'B'}, 'B'),
h('p', {
key:'C'}, 'C'),
h('p', {
key:'D'}, 'D')
]);
// div中又套了一个div,内层div中有4个p标签
let vnode2 = h('div', {
}, h('div',{
},[
h('p', {
key:'A'}, 'A'),
h('p', {
key:'B'}, 'B'),
h('p', {
key:'C'}, 'C'),
h('p', {
key:'D'}, 'D'),
h('p', {
key:'E'}, 'E'),
]));
let container = document.getElementById('container');
let btn = document.getElementById('btn');
patch(container, vnode1);
// 点击按钮时,将vnode1变为vnode2
btn.addEventListener('click', function () {
patch(vnode1, vnode2);
})
有点难,大家跟着这个视频学吧:https://www.bilibili.com/video/BV1v5411H7gZ