虚拟DOM
什么是虚拟DOM
Virtual DOM(虚拟DOM),是由普通的JS对象来描述真实的DOM对象。因为不是真实的DOM对象,所以叫Virtual DOM。真实的DOM成员需要创建大量的属性等,使用virtual DOM来描述DOM将会十分的简洁,减少性能开销。
虚拟DOM的结构
{
sel:'div', data:{
}, children:undefined, text:'Hello Virtual DOM' elm:undefined, key:undefined }
虚拟DOM的节点类型
元素节点 :节点类型 1 ;
属性节点: 节点类型 2;
文本节点: 节点类型 3;
注释节点: 节点类型 8;
文档节点: 节点类型 9;
为什么使用virtual DOM
-
手动操作DOM比较麻烦,且需要考虑浏览器兼容性问题,虽然有JQuery等库可以简化DOM操作,但是随着项目的复杂,DOM的操作复杂性提升。
-
为了简化DOM的复杂操作于是出现了各种MVVM框架,其解决了视图和状态的同步问题。
-
为了简化视图的操作我们可以使用模板引擎,但是因为没有解决跟踪动态变化的问题,而出现了Virtual DOM。
-
virtual DOM的好处是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述DOM,VIrtual DOM内部将弄清楚如何有效的更新DOM。
-
虚拟DOM可以维护程序的状态,跟踪上一次的状态。
-
通过比较前后两次状态差异更新真实DOM。
虚拟DOM有什么作用
- 维护视图和状态的关系
- 在复杂视图情况下提升渲染性能,除渲染DOM外,还可以实现SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等。
Virtual DOM 库之 Snabbdom
-
Vue 2.x内部使用的Virtual DOM 就是改造的Snabbdom
-
大约200 SLOC (single line of code)
-
通过模块可扩展
-
源码使用TypeScript开发
-
最快的Virtual DOM之一
注:Snabbdom的核心库并不能处理元素的属性、样式、事件等,如果需要处理的话,可以使用模块进行支持。
Snabbdom库的常用模块:
-
attribute
- 设置Dom元素的属性,使用setAttribute()
- 处理布尔类型的属性
-
props
- 和attribute模块相似,设置Dom属性element[attr] = value
- 不处理布尔类型的属性
-
class
- 切换类样式
- 注意:给元素设置类属性是通过sel选择器
-
dataset
- 设置data-*的自定义属性
-
eventlisteners
- 注册和移除事件
-
style
- 设置行内样式,支持动画
- delayed/remove/destory
Snabbdom源码解析
源码应该如何学习
- 先宏观了解
- 带着目标看源码
- 看源码的过程不求甚解
- 调试
- 参考资料
Snabbdom 核心内容
-
使用h()函数创建JavaScript对象(VNode)描述真实DOM
-
init()设置模块,创建patch() 这里init()是一个高阶函数 (在一个函数的内部返回一个函数)
-
patch()比较新旧两个VNode
-
将变化的内容更新到真实的DOM上
Snabbdom 的源码剖析
h函数
h()函数介绍
-
在使用vue时见过h函数
new Vue({ router, store, render: h => h(App) }).$mount('#app')
-
h()函数最早见于hyperscript,使用JavaScript创建超文本
-
Snabbdom中的h()函数不是用来创建超文本,而是创建VNOde
函数重载
-
概念
- 参数个数或类型不同的函数
- JavaScript中没有重载的概念
- TypeScript中有重载,不过重载的实现还是通过代码调整参数
-
重载的示意
function add (a,b) { console.log(a,b) } function add (a,b,c) { console.log(a + b + c) } add(1, 2) add(1, 2, 3)
Modules(模块)
Snabbdom为了保证核心库的精简,把处理元素的属性/事件/样式等工作 交给模块处理。模块是可以按需引入 ,实现的核心是基于Hooks 。在 Snabbdom 中的模块是可以自己扩展的模块的钩子函数
- pre patch函数开始执行的时候出发
- init createElm函数开始执行之前触发 在把VNode转化为真实的 DOM 之前触发
- create ceeateElm函数末尾调用 创建完真实DOM之后触发
- insert patch函数末尾执行 真实 DOM 添加到 DOM 树中触发
- prepatch patchVnode函数开头调用 开始对比两个 Vnode 差异之前触发
- update patchVnode函数开头调用 两个 Vnode 对比过程中出发,比prepatch稍微晚一些
- postpatch patchVnode的最末尾调用 两个 Vnode对比结束后执行
- destroy removeVnodes => invokeDestoryHook 中调用 在删除元素之前触发,子节点的destory也被触发
- remove removeVnodes 中调用 元素被删除的时候触发
- post patch函数的最后调用
源码位置: src/h.ts
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {
}, children: any, text: any, i: number;
if (c !== undefined) {
data = b;
if (is.array(c)) {
children = c; }
else if (is.primitive(c)) {
text = c; }
else if (c && c.sel) {
children = [c]; }
} else if (b !== undefined) {
if (is.array(b)) {
children = b; }
else if (is.primitive(b)) {
text = b; }
else if (b && b.sel) {
children = [b]; }
else {
data = b; }
}
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
addNS(data, children, sel);
}
return vnode(sel, data, children, text, undefined);
};