什么是虚拟 DOM
- Virtual DOM(虚拟DOM),是由普通的 JS 对象描述 DOM 对象,因为不是真实的 DOM对象,所以叫 Virtual DOM
- 真实的 DOM成员 非常非常多,所以创建一个 DOM 对象的成本非常高
- 可以通过 Virtual DOM 来描述真实 DOM
Virtual DOM 对象非常小的,我们创建一个 Virtual DOM 的成本比创建一个真实 DOM 的成本小很多{sel:'div',data: {},children: undefined,text: 'Hello Virtual DOM',elm: undefined,key: undefined}
为什么使用虚拟DOM
- 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery等简化 DOM 操作,但是随着项目的复杂 DOM操作复杂提升
- 为了简化 DOM 的复杂操作于是出现来各种 MVVM框架,MVVM框架解决来视图和状态的同步问题
- 为了简化视图的操作我们可以使用模版引擎,但是模版引擎没有解决跟踪状态变化的问题,于是 Virtual DOM 出现了;Virtual DOM的好处是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述 DOM,Virtual DOM 内部将弄清楚如果有效(diff) 的更新DOM
参考github上virtual-dom的描述
维护视图和状态的关系
- 复杂视图情况下提升渲染性能
除了浏览器平台渲染DOM以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)
虚拟DOM库
-
- Vue2.x内部使用的Virtual DOM 就是改造的 Snabbdom
- 大约200SLOC(Single Line Of Code)
- 通过模块可扩展
- 源码使用TypeScript开发
- 最快的Virtual DOM之一
- virtual-dom
Snabbdom 基本使用
```powershell创建项目目录
mkdir snabbdom-demo
进入项目目录
cd snabbdom-demo
初始化 package.json
npm init -y
本地安装 parcel
npm install parcel-bundler -D
- 配置 package.json 的 script```json"scripts": {"dev": "parcel index.html --open","build": "parcel build index.html"}
- 创建项目基本结构
|-index.html|-package.json|-src|-01.basicusage.js
导入 Snabbdom
- 安装
```javascript import { init, h } from ‘snabbdom’npm install snabbdom@3.0.1
const patch = init([])
<a name="GGeeI"></a>### 案例1-只包含文本内容```javascript// 01-BasicUsage.jsimport { init, h } from 'snabbdom'// 1 通过 h 函数创建 VNodelet vNode = h('div#box.container', '新内容')// 获取挂载元素const dom = document.querySelector('#app')// 2 通过 init 函数得到 patch 函数const patch = init([])// 3 通过 patch,将 vNode 渲染到 DOMlet oldVNode = patch(dom, vNode)// 4 创建新的 VNode,更新给 oldVNodevNode = h('p#text.abc', '这是p标签的内容')patch(oldVNode, vNode)
案例2-包含子节点
import { h, init} from 'snabbdom'const patch = init([])// 创建包含子节点的 VNode// - 参数2的数组为子节点列表,内部就应该传入 vNodelet vNode = h('div#container', [h('h1', '标题文本'),h('p', '内容文本')])// 获取挂载元素const dom = document.querySelector('#app')// 渲染 vNodeconst oldVNode = patch(dom, vNode)// 清空页面元素patch(oldVNode, h('!'))
Snabbdom 中的模块
- Snabbdom 的核心库并不能处理 DOM 元素的属性/样式/事件等,可以通过注册 Snabbdom 默认提供的模块来实现
- Snabbdom 中的模块可以用来扩展 Snabbdom 的功能
- Snabbdom 中的模块的实现是通过 注册全局的钩子函数来实现的
官方提供的模块:
- attributes
- 设置了DOM 元素的属性,使用 setAttribute()
- 处理布尔类型的属性
- props
- 和 attributes 模块相似,设置 DOM元素 的属性
element[attr] = value - 不处理布尔类型的属性
- 和 attributes 模块相似,设置 DOM元素 的属性
- class
- 切换类样式
- 给元素设置类样式是通过 set 选择器
- dataset
- 设置 data-*的自定义属性
- eventlisteners
- 注册和移除事件
style
导入模块
- init() 注册模块
- h() 函数的第二个参数处使用模块 ```javascript // 1 导入模块(注意拼写,导入的名称不要拼错) import { init, h, styleModule, eventListenersModule } from ‘snabbdom’
// 2 注册模块(为 patch 函数添加模块对应的能力) const patch = init([ styleModule, eventListenersModule ])
// 3 使用模块 let vNode = h(‘div#box’, { style: { backgroundColor: ‘green’, height: ‘200px’, width: ‘200px’ } }, [ h(‘h1#title’, { style: { color: ‘#fff’ }, on: { click () { console.log(‘点击了 h1 标签’) } } }, ‘这是标题内容’), h(‘p’, ‘这是内容文本’) ])
const dom = document.getElementById(‘app’) patch(dom, vNode)
<a name="601Kr"></a>## Snabbdom 源码解析- 如何学习源码- 先宏观了解- 带着目标看源码- 看源码的过程要不求甚解- 调试- 参考资料- Snabbdom的核心- 使用 h() 函数创建 JavaScript 对象(VNode)描述真实DOM- init设置模块,创建patch()- patch()比较新旧两个 VNode- 把变化的内容更新到真实 DOM树上- Snabbdom源码<br />- [源码地址](https://github.com/snabbdom/snabbdom)<br /><a name="OTkHP"></a>### h函数- 作用:创建 VNode 对象- Vue中的 h函数```javascriptnew Vue ({render: h=> h(app)}).$mount('#app')
- h函数 最早见于 hyperscript,使用 JavaScript 创建超文本
- Snabbdom中的h函数不是用来创建超文本,而是创建VNode
函数的重载
- 参数个数或参数类型不同的函数
- JavaScript 中没有重载的概念
- TypeScript 中有重载,不过重载的实现还是通过代码调整参数
- 参数个数不同的重载示例 ```javascript function add (a: number, b: number) { console.log(a + b) }
function add (a: number, b: number, c: number) { console.log(a + b + c) }
add(1, 2) add(1, 2, 3)
- [源码位置](https://gitee.com/zhou__jian/lagou-basis/blob/master/part04-05/codes/02-virtual-DOM/snabbdom/src/h.ts)<a name="q3BFM"></a>### VNode- [源码地址](https://gitee.com/zhou__jian/lagou-basis/blob/master/part04-05/codes/02-virtual-DOM/snabbdom/src/vnode.ts)<a name="yRysf"></a>### patch 整体分析- patch(oldVnode, newVnode)- 把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点- 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)- 如果不是相同节点,删除之前的内容,重新渲染- 如果是相同节点,再判断新的 VNode 是否是 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容- 如果新的 VNode 有 children,判断子节点是否有变化<a name="0tKAf"></a>####<a name="sD5MS"></a>### init函数[返回patch函数源码](https://gitee.com/zhou__jian/lagou-fed/blob/master/part03-01/codes/snabbdom-master/src/package/init.ts)<a name="RhuiV"></a>####<a name="SWXuL"></a>### patch```javascript//./codes/snabbdom-master/src/package/init.tsreturn function patch (oldVnode: VNode | Element, vnode: VNode): VNode {let i: number, elm: Node, parent: Node//存储新插入节点的队列const insertedVnodeQueue: VNodeQueue = []//遍历 pre中的钩子函数并执行 正式处理 VNode 之前触发的第一个钩子函数for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()if (!isVnode(oldVnode)) {//如果不是 VNode 对象,将转换成 VNode 对象oldVnode = emptyNodeAt(oldVnode)}//判断新旧 VNode 是否是相同节点if (sameVnode(oldVnode, vnode)) {//对比新旧 VNode 差异,更新 DOMpatchVnode(oldVnode, vnode, insertedVnodeQueue)} else {elm = oldVnode.elm!// 获取父元素parent = api.parentNode(elm) as Node// 创建 VNode 对于的 DOM 元素,并将新插入的节点队列传入createElm(vnode, insertedVnodeQueue)if (parent !== null) {// 插入到父元素中api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))//移除老节点removeVnodes(parent, [oldVnode], 0, 0)}}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]()//返回 VNodereturn vnode}
createElm
function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {//1. 执行用户设置的 init 钩子函数let i: anylet data = vnode.dataif (data !== undefined) {const init = data.hook?.init// 判断 init 是否有定义if (isDef(init)) {init(vnode)data = vnode.data}}// 2. 把 VNode 转换成真实 DOM 对象(没有渲染到页面)const children = vnode.childrenconst sel = vnode.selif (sel === '!') {//undefinedif (isUndef(vnode.text)) {vnode.text = ''}// 如果选择器是 '!' 创建注释节点vnode.elm = api.createComment(vnode.text!)} else if (sel !== undefined) {// 如果选择器不为空//解析选择器// Parse selectorconst hashIdx = sel.indexOf('#')const dotIdx = sel.indexOf('.', hashIdx)const hash = hashIdx > 0 ? hashIdx : sel.lengthconst dot = dotIdx > 0 ? dotIdx : sel.lengthconst tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : selconst elm = vnode.elm = isDef(data) && isDef(i = data.ns)? api.createElementNS(i, tag): api.createElement(tag)if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)// 如果 VNode 中有子节点,创建 VNode 对应的 DOM 元素并追加到 DOM树上if (is.array(children)) {for (i = 0; i < children.length; ++i) {const ch = children[i]if (ch != null) {api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))}}//是否是 原始值 string/number} else if (is.primitive(vnode.text)) {// 创建文本节点,追加到 DOM 树上api.appendChild(elm, api.createTextNode(vnode.text))}// 判断 hook 是否有定义const hook = vnode.data!.hookif (isDef(hook)) {// 触发 create 钩子函数hook.create?.(emptyNode, vnode)if (hook.insert) {// 将设置了 insert 钩子函数的 VNode 存入数组队列,在 DOM插入 DOM 树后执行insertedVnodeQueue.push(vnode)}}} else {// 如果选择器为空,创建文本节点vnode.elm = api.createTextNode(vnode.text!)}// 3. 返回新创建的 DOMreturn vnode.elm}
removeVnodes
function removeVnodes (parentElm: Node,//父元素vnodes: VNode[],//需要删除的元素startIdx: number,//开始索引endIdx: number//结束索引): void {for (; startIdx <= endIdx; ++startIdx) {let listeners: numberlet rm: () => voidconst ch = vnodes[startIdx]if (ch != null) {if (isDef(ch.sel)) {//元素节点invokeDestroyHook(ch)listeners = cbs.remove.length + 1 //防止重复删除 DOM 元素rm = createRmCb(ch.elm!, listeners) // 返回真正删除DOM元素 的函数for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)const removeHook = ch?.data?.hook?.removeif (isDef(removeHook)) {//如果用户传入remove 钩子函数,需要手动调用 rm 删除 DOM 元素removeHook(ch, rm)} else {rm()}} else { // Text node 文本节点api.removeChild(parentElm, ch.elm!)}}}}
addVnodes
function addVnodes (parentElm: Node,//父节点before: Node | null,//参考节点,插入 before 之前vnodes: VNode[],//被添加节点startIdx: number,//开始索引endIdx: number,//结束索引insertedVnodeQueue: VNodeQueue //存储 具有 insert 钩子函数的 VNode 节点) {for (; startIdx <= endIdx; ++startIdx) {const ch = vnodes[startIdx]if (ch != null) {// 插入到 DOM 树中api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)}}}
patchVnode

function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {// 第一个过程:触发 prepatch 和 update 钩子函数const hook = vnode.data?.hook //用户传入的钩子函数hook?.prepatch?.(oldVnode, vnode)const elm = vnode.elm = oldVnode.elm!const oldCh = oldVnode.children as VNode[]const ch = vnode.children as VNode[]if (oldVnode === vnode) returnif (vnode.data !== undefined) {for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)vnode.data.hook?.update?.(oldVnode, vnode)}// 第二个过程:真正对比新旧 vnode 差异的地方if (isUndef(vnode.text)) {//新旧节点是否都有子节点if (isDef(oldCh) && isDef(ch)) {//对比新旧节点的子节点 更新DOMif (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)} else if (isDef(ch)) {if (isDef(oldVnode.text)) api.setTextContent(elm, '') //清空老节点的 文本内容//将新节点 子节点插入 elm 中addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)} else if (isDef(oldCh)) {//清空老节点 的子节点removeVnodes(elm, oldCh, 0, oldCh.length - 1)} else if (isDef(oldVnode.text)) {//清空老节点的 文本内容api.setTextContent(elm, '')}} else if (oldVnode.text !== vnode.text) {//新旧节点是文本节点,且不想等if (isDef(oldCh)) {//清空老的文本节点removeVnodes(elm, oldCh, 0, oldCh.length - 1)}//更新文本节点api.setTextContent(elm, vnode.text!)}// 第三个过程:触发 postpatch// 这个钩子函数可以获取最新的数据hook?.postpatch?.(oldVnode, vnode)}
updateChildren
function updateChildren (parentElm: Node,//父元素oldCh: VNode[],//旧Vnode 子节点newCh: VNode[],//新Vnode 子节点insertedVnodeQueue: VNodeQueue//有 insert 钩子函数的 Vnode 节点队列) {let oldStartIdx = 0 //旧开始索引let newStartIdx = 0 //新开始索引let oldEndIdx = oldCh.length - 1 //旧结束索引let oldStartVnode = oldCh[0] // 旧的开始节点let oldEndVnode = oldCh[oldEndIdx] //旧结束节点let newEndIdx = newCh.length - 1 //新结束索引let newStartVnode = newCh[0] //新开始节点let newEndVnode = newCh[newEndIdx] //新结束节点let oldKeyToIdx: KeyToIndexMap | undefinedlet idxInOld: numberlet elmToMove: VNodelet before: any// 同级别节点比较while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (oldStartVnode == null) {oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left} else if (oldEndVnode == null) {oldEndVnode = oldCh[--oldEndIdx]} else if (newStartVnode == null) {newStartVnode = newCh[++newStartIdx]} else if (newEndVnode == null) {newEndVnode = newCh[--newEndIdx]// 比较开始和结束的 4 中情况} else if (sameVnode(oldStartVnode, newStartVnode)) {//旧开始节点 和 新开始节点 是相同节点patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)oldStartVnode = oldCh[++oldStartIdx]newStartVnode = newCh[++newStartIdx]} else if (sameVnode(oldEndVnode, newEndVnode)) {// 旧结束节点 和 新结束节点 是相同节点patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)oldEndVnode = oldCh[--oldEndIdx]newEndVnode = newCh[--newEndIdx]} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right// 旧开始节点 和 新就是节点 是相同节点patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))oldStartVnode = oldCh[++oldStartIdx]newEndVnode = newCh[--newEndIdx]} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left// 旧结束节点 和 新 开始节点 是相同节点patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)oldEndVnode = oldCh[--oldEndIdx]newStartVnode = newCh[++newStartIdx]} else {// 开始和结尾比较结束if (oldKeyToIdx === undefined) {oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)}idxInOld = oldKeyToIdx[newStartVnode.key as string]if (isUndef(idxInOld)) { // New elementapi.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)} else {elmToMove = oldCh[idxInOld]if (elmToMove.sel !== newStartVnode.sel) {api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)} else {patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)oldCh[idxInOld] = undefined as anyapi.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)}}newStartVnode = newCh[++newStartIdx]}}//循环结束的收尾工作if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {if (oldStartIdx > oldEndIdx) {// 老节点数组遍历完,新节点数组有剩余before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elmaddVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)} else {// 新节点数组遍历完,旧节点有剩余removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)}}}
