虚拟DOM原理分析
什么是 Virtual DOM
Virtual DOM
(虚拟 DOM),是由普通的JS
对象来描述DOM
对象,因为不是真实的DOM
对象,所以叫Virtual 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,onanimationend,onanimationiteration ,onanimationstart,ontransitionend,dataset,nonce,autofocus,tabIndex,click ,focus,blur,enterKeyHint,onformdata,onpointerrawupdate,attachInternals,n amespaceURI,prefix,localName,tagName,id,className,classList,slot,part,at tributes,shadowRoot,assignedSlot,innerHTML,outerHTML,scrollTop,scrollLef t,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight ,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,eleme ntTiming,previousElementSibling,nextElementSibling,children,firstElement Child,lastElementChild,childElementCount,onfullscreenchange,onfullscreen error,onwebkitfullscreenchange,onwebkitfullscreenerror,setPointerCapture ,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames ,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute ,removeAttributeNS,hasAttribute,hasAttributeNS,toggleAttribute,getAttrib uteNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAtt ributeNode,closest,matches,webkitMatchesSelector,attachShadow,getElement sByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentE lement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClien tRects,getBoundingClientRect,scrollIntoView,scroll,scrollTo,scrollBy,scr ollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,re move,prepend,append,querySelector,querySelectorAll,requestFullscreen,web kitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestina tionInsertionPoints,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_ NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSING_INSTRUCTION_NODE,COMME NT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION _NODE,DOCUMENT_POSITION_DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMEN T_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAI NED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseU RI,isConnected,ownerDocument,parentNode,parentElement,childNodes,firstCh ild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChild Nodes,getRootNode,normalize,cloneNode,isEqualNode,isSameNode,compareDocu mentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace ,insertBefore,appendChild,replaceChild,removeChild,addEventListener,remo veEventListener,dispatchEvent
- 可以使用
Virtual DOM
来描述真实DOM
{
sel: "div",
data: {},
children: undefined,
text: "Hello Virtual DOM",
elm: undefined,
key: undefined
}
为什么使用 Virtual DOM
- 手动操作
DOM
比较麻烦,还需要考虑浏览器兼容性问题,虽然有jQuery
等库简化DOM
操作,但是随着项目的复杂 DOM 操作复杂提升 - 为了简化
DOM
的复杂操作于是出现了各种MVVM
框架,MVVM
框架解决了视图和状态的同步问题 - 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是
Virtual DOM
出现了 Virtual DOM
的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述DOM
,Virtual DOM
内部将弄清楚如何有效(diff
)的更新DOM
- 虚拟
DOM
可以维护程序的状态,跟踪上一次的状态 - 通过比较前后两次状态的差异更新真实
DOM
虚拟 DOM 的作用
- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 除了渲染
DOM
以外,还可以实现SSR(Nuxt.js/Next.js)
、原生应用(Weex/React Native
)、小程序(mpvue/uni-app
)等
Virtual DOM 库
- Snabbdom(opens new window)
Vue 2.x
内部使用的Virtual DOM
就是改造的Snabbdom
- 通过模块可扩展
- 源码使用
TypeScript
开发 - 最快的
Virtual DOM
之一
- virtual-dom(opens new window)
Snabbdom 基本使用
创建项目
创建项目目录
md snabbdom-demo
进入项目目录
cd snabbdom-demo
创建 package.json yarn init -y
本地安装 parcel
yarn add parcel-bundler
配置 package.json
的 scripts
"scripts": {
"dev": "parcel index.html --open", "build": "parcel build index.html"
}
创建目录结构
yarn add snabbdom
import{init,h,thunk}from'snabbdom'
snabbdom
的核心仅提供最基本的功能,只导出了三个函数init()
、h()
、thunk()
init()
是一个高阶函数,返回patch()
h()
返回虚拟节点VNode
,这个函数我们在使用Vue.js
的时候见过
new Vue({
router,
store,
render: h => h(App)
}).$mount('app')
thunk()
是一种优化策略,可以在处理不可变数据时使用
注意:导入时候不能使用
import snabbdom from 'snabbdom'
。原因:node_modules/src/snabbdom.ts
末尾导出使用的语法是export
导出API
,没有使用export default
导出默认输出
基本使用
例子1
import { h, init } from 'snabbdom'
// 1. hello world
// 参数:数组,模块
// 返回值:patch函数,作用对比两个vnode的差异更新到真实DOM
let patch = init([])
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串的话就是标签中的内容
let vnode = h('divcontainer.cls', {
hook: {
init (vnode) {
console.log(vnode.elm)
},
create (emptyVnode, vnode) {
console.log(vnode.elm)
}
}
}, 'Hello World')
let app = document.querySelector('app')
// 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
// 第二个参数:VNode
// 返回值:VNde
let oldVnode = patch(app, vnode)
// 假设的时刻
vnode = h('div', 'Hello Snabbdom')
patch(oldVnode, vnode)
例子2
// 2. div中放置子元素 h1,p
import { h, init } from 'snabbdom'
let patch = init([])
let vnode = h('divcontainer', [
h('h1', 'Hello Snabbdom'),
h('p', '这是一个p标签')
])
let app = document.querySelector('app')
let oldVnode = patch(app, vnode)
setTimeout(() => {
vnode = h('divcontainer', [
h('h1', 'Hello World'),
h('p', 'Hello P')
])
patch(oldVnode, vnode)
// 清空页面元素 -- 错误
// patch(oldVnode, null)
patch(oldVnode, h('!'))
}, 2000);
例子3 debug-patchVnode
import { h, init } from 'snabbdom'
let patch = init([])
// 首次渲染
let vnode = h('div', 'Hello World')
let app = document.querySelector('app')
let oldVnode = patch(app, vnode)
// patchVnode 的执行过程
vnode = h('div', 'Hello Snabbdom')
patch(oldVnode, vnode)
例子4 debug-updateChildren
import { h, init } from 'snabbdom'
let patch = init([])
// 首次渲染
let vnode = h('ul', [
h('li', '首页'),
h('li', '视频'),
h('li', '微博')
])
let app = document.querySelector('app')
let oldVnode = patch(app, vnode)
// updateChildren 的执行过程
vnode = h('ul', [
h('li', '首页'),
h('li', '微博'),
h('li', '视频')
])
patch(oldVnode, vnode)
例子5 debug-updateChildren-key
import { h, init } from 'snabbdom'
let patch = init([])
// 首次渲染
let vnode = h('ul', [
h('li', { key: 'a' }, '首页'),
h('li', { key: 'b' }, '视频'),
h('li', { key: 'c' }, '微博')
])
let app = document.querySelector('app')
let oldVnode = patch(app, vnode)
// updateChildren 的执行过程
vnode = h('ul', [
h('li', { key: 'a' }, '首页'),
h('li', { key: 'c' }, '微博'),
h('li', { key: 'b' }, '视频')
])
patch(oldVnode, vnode)
模块
Snabbdom
的核心库并不能处理元素的属性/样式/事件
等,如果需要处理的话,可以使用模块
常用模块
官方提供了 6 个模块
attributes
- 设置
DOM
元素的属性,使用setAttribute ()
- 处理布尔类型的属性
- 设置
props
- 和
attributes
模块相似,设置DOM
元素的属性element[attr] = value
- 不处理布尔类型的属性
- 和
class
- 切换类样式
- 注意:给元素设置类样式是通过
sel
选择器
dataset
- 设置
data-*
的自定义属性eventlisteners
- 注册和移除事件
- 设置
style
- 设置行内样式,支持动画
delayed/remove/destroy
模块使用
模块使用步骤:
- 导入需要的模块
init()
中注册模块- 使用
h()
函数创建VNode
的时候,可以把第二个参数设置为对象,其他参数往后移
import { init, h } from 'snabbdom'
// 1. 导入模块
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'
// 2. 注册模块
let patch = init([
style,
eventlisteners
])
// 3. 使用 h() 函数的第二个参数传入模块需要的数据(对象)
let vnode = h('div', {
style: {
backgroundColor: 'red'
},
on: {
click: eventHandler
}
}, [
h('h1', 'Hello Snabbdom'),
h('p', '这是p标签')
])
function eventHandler () {
console.log('点击我了')
}
let app = document.querySelector('app')
let oldVnode = patch(app, vnode)
vnode = h('div', 'hello')
patch(oldVnode, vnode)
Snabbdom 源码解析
Snabbdom 的核心
- 使用
h()
函数创建 JavaScript 对象(VNode
)描述真实 DOM init()
设置模块,创建patch()
patch()
比较新旧两个VNode
- 把变化的内容更新到真实
DOM
树上
Snabbdom 源码
源码地址: https://github.com/snabbdom/snabbdom
src 目录结构
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)
源码位置:
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 {
let data: VNodeData = {}
let children: any
let text: any
let 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
} else if (c && c.sel) {
children = [c]
}
} else if (b !== undefined && b !== null) {
// 处理两个参数的情况
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] ===