什么是 Virtual DOM
- Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,
所以叫 Virtual DOM
{
sel: "div",
data: {},
children: undefined,
text: "Hello Virtual DOM",
elm: undefined,
key: undefined
}
真实 DOM 成员
let element = document.querySelector('#app');
let s = '';
for (var key in element) {
s += key + ',';
}
console.log(s);
// 打印结果 align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,aut ocapitalize,contentEditable,isContentEditable,inputMode,offsetParent,off setTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,onc opy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,onc hange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondrag end,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchan ge,onemptied,onended,onerror,onfocus,oninput,oninvalid,onkeydown,onkeypr ess,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown ,onmouseenter,onmouseleave,onmousemove,onmouseout,onmouseover,onmouseup, onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,on resize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend ,ontimeupdate,ontoggle,onvolumechange,onwaiting,onwheel,onauxclick,ongot pointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointe rup,onpointercancel,onpointerover,onpointerout,onpointerenter,onpointerl eave,onselectstart,onselectionchange,
为什么使用 Virtual DOM
-
手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,但是随着项目的复杂 DOM 操作复杂提升
-
为了简化 DOM 的复杂操作于是出现了各种 MVVM 框架,MVVM 框架解决了视图和状态的同步问题
-
为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是 Virtual DOM 出现了
-
Virtual DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述 DOM, Virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM
-
参考 github 上 virtual-dom 的描述
- 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
- 通过比较前后两次状态的差异更新真实 DOM
虚拟 DOM 的作用
-
维护视图和状态的关系
-
复杂视图情况下提升渲染性能
-
除了渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等
常见的 Virtual DOM 库
Snabbdom 使用
// 安装
$ yarn add snabbdom
// 引入
import { init, h, thunk } from 'snabbdom'
- init()是一个高阶函数,返回 patch()
- h() 返回虚拟节点 VNode,这个函数我们在使用 Vue.js 的时候见过
new Vue({ router, store, render: h => h(App) }).$mount('#app');
- thunk() 是一种优化策略,可以在处理不可变数据时使用
基本使用
import { h, init } from 'snabbdom';
// patch是个核心函数
const patch = init([]);
// 创建虚拟dom
let vnode = h('div#container', 'Hello World');
const app = document.querySelector('#app');
let oldVnode = patch(app, vnode);
vnode = h('div', 'hello,glh');
patch(oldVnode, vnode);
模块
Snabbdom 的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块
常用模块
-
attributes
- 设置 DOM 元素的属性,使用 setAttribute ()
- 处理布尔类型的属性
-
props
- 和 attributes 模块相似,设置 DOM 元素的属性 element[attr] = value
- 不处理布尔类型的属性
-
class
- 切换类样式
- 注意:给元素设置类样式是通过 sel 选择器
-
dataset
- 设置 data-*的自定义属性
-
eventlisteners
注册和移除事件 -
style
- 设置行内样式,支持动画
- delayed/remove/destroy
模块的使用
import { init, h } from 'snabbdom';
import style from 'snabbdom/modules/style';
import eventlisteners from 'snabbdom/modules/eventlisteners';
const patch = init([style, eventlisteners]);
let vnode = h(
'div',
{
style: {
backgroundColor: 'red',
},
on: {
click: eventHandler,
},
},
[h('h1', 'hello,Snabbdom')]
);
function eventHandler() {
console.log('点一下');
}
const app = document.querySelector('#app');
patch(app, vnode);
Snabbdom 源码解析
- h() 函数创建虚拟 Dom。源码位置:src/h.ts
// h 函数的重载
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData | null, 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) {
// 处理三个参数的情况 // sel、data、children/text
if (b !== null) {
data = b;
}
if (is.array(c)) {
children = c;
}
// 如果 c 是字符串或者数字
else if (is.primitive(c)) {
text = c;
}
// 如果 c 是 VNode
else if (c && c.sel) {
children = [c];
}
} else if (b !== undefined && b !== null) {
// 处理两个参数的情况
// 如果 b 是数组
if (is.array(b)) {
children = b;
}
// 如果 b 是字符串或者数字
else if (is.primitive(b)) {
text = b;
}
// 如果 b 是 VNode
else if (b && b.sel) {
children = [b];
} else {
data = b;
}
}
if (children !== undefined) {
// 处理 children 中的原始值(string/number)
for (i = 0; i < children.length; ++i) {
// 如果 child 是 string/number,创建文本节点
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] === '#')
) {
// 如果是 svg,添加命名空间 addNS(data, children, sel);
}
// 返回 VNode
return vnode(sel, data, children, text, undefined);
}
// 导出模块
export default h;
- VNode 源码位置:src/vnode.ts
export interface VNode {
// 选择器
: string | undefined;
// 节点数据:属性/样式/事件等
data: VNodeData | undefined;
// 子节点,和 text 只能互斥
children: Array<VNode | string> | undefined; // 记录 vnode 对应的真实 DOM
elm: Node | undefined;
// 节点中的内容,和 children 只能互斥
text: string | undefined; // 优化用 key: Key | undefined;
}
export function vnode(sel: string | undefined, data: any | undefined, children: Array<VNode | string> | undefined, text: string | undefined, elm: Element | Text | undefined): VNode {
let key = data === undefined ? undefined : data.key;
return {sel, data, children, text, elm, key};
}
export default vnode;
- init 返回 patch 函数,依赖 modules/domApi/cbs 通过高阶函数让 init() 内部形成闭包,返回的 patch() 可以访问到 modules/domApi/cbs,而不需要重新创建
源码位置:src/snabbdom.ts
const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) { let i: number, j: number, cbs = ({} as ModuleHooks);
// 初始化 api
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
// 把传入的所有模块的钩子方法,统一存储到 cbs 对象中 // 最终构建的 cbs 对象的形式 cbs = [ create: [fn1, fn2], update: [], ... ]
for (i = 0; i < hooks.length; ++i) {
// cbs['create'] = []
cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) {
// const hook = modules[0]['create']
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
(cbs[hooks[i]] as Array<any>).push(hook);
}
}
}
…
…
…
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {}}
- patch()
- 功能:
- 传入新旧 VNode,对比差异,把差异渲染到 DOM
- 返回新的 VNode,作为下一次 patch() 的 oldVnode
- 执行过程:
- 首先执行模块中的钩子函数 pre
- 如果 oldVnode 和 vnode 相同(key 和 sel 相同)调用 patchVnode(),找节点的差异并更新 DOM
- 如果 oldVnode 是 DOM 元素,把 DOM 元素转换成 oldVnode
- 调用 createElm() 把 vnode 转换为真实 DOM,记录到 vnode.elm
- 把刚创建的 DOM 元素插入到 parent 中
- 移除老节点
- 触发用户设置的 create 钩子函数
- 源码位置:src/snabbdom.ts
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node;
// 保存新插入节点的队列,为了触发钩子函数
const insertedVnodeQueue: VNodeQueue = [];
// 执行模块的 pre
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 如果 oldVnode 不是 VNode,创建 VNode 并设置 elm
if (!isVnode(oldVnode)) {
// 把 DOM 元素转换成空的 VNode
oldVnode = emptyNodeAt(oldVnode);
}
// 如果新旧节点是相同节点(key 和 sel 相同)
if (sameVnode(oldVnode, vnode)) {
// 找节点的差异并更新 DOM
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
// 如果新旧节点不同,vnode 创建对应的 DOM
// 获取当前的 DOM 元素
elm = oldVnode.elm!; parent = api.parentNode(elm);
// 触发 init/create 钩子函数,创建 DOM
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
// 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
// 移除老节点
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 执行用户设置的 insert 钩子函数
for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]); }
// 执行模块的 post 钩子函数
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
// 返回 vnode
return vnode;
};
网友评论