Vue2中的虚构DOM实现革新了了第三方库snabbdom
Snabbdom 是一个虚构 DOM 库,专一提供简略、模块性的体验,以及弱小的性能和性能。
应用起来也很简略package.json
{ "name": "parcel-test", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "parcel index.html --open", "build": "parcel build index.html" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "parcel-bundler": "^1.12.5" }, "dependencies": { "snabbdom": "^2.1.0" }}
index.html
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Snabbdom-demo</title></head><body> <div id="app"></div> <!-- <script src="./src/01-test.js"></script> --> <script src="./src/02-test.js"></script></body></html>
01-test.js
中测试了单个元素的替换
import {init} from 'snabbdom/build/package/init'; // parcel这里要导入全门路,不然会报错import {h} from 'snabbdom/build/package/h'const patch = init([])// 第一个参数:标签+选择器// 第二个参数:如果是字符串就是标签中的文本内容let vnode = h('div#container.cls','hello world')let app = document.querySelector('#app')// 第一个参数:旧的vnode,能够是DOM元素// 第二个参数:新的vnode// 返回新的vnode// 比照两个vnode,比拟差别,更新到DOMlet oldVnode = patch(app,vnode)vnode = h('div#container.another','snabbdom test')patch(oldVnode,vnode)
02-test.js
中测试了多个标签的替换,还有异步更新和清空
import { init } from "snabbdom/build/package/init";import { h } from "snabbdom/build/package/h";const patch = init([]);// 多个子元素let vnode = h("div#container", [h("h1", "snabbdom"), h("p", "p tip")]);let app = document.querySelector("#app");let oldVnode = patch(app, vnode);setTimeout(() => { vnode = h("div#container", [ h("h1", "timeout snabbdom"), h("p", "tmeout p tip"), ]); patch(oldVnode, vnode); // 3秒后更新 patch(oldVnode, h("!")); // 革除div中的内容,创立空的正文节点}, 3000);
Snabbdom模块作用
- Snabbdom的外围课并不能解决DOM元素的属性/款式/事件等,能够通过注册Snabbdom默认提供的模块来实现
- Snabbdom中的模块能够用来扩大Snabbdo的性能
- Snabbdom中的模块的实现是通过注册全局钩子函数来实现的
模块
- attributes 设置DOM的属性,通过setAttribute()办法。解决布尔类型的属性。
- props 解决非布尔类型的属性。
- class 切换款式
- dataset 设置自定义data-*属性。
- eventlisteners
- style 设置行内款式
模块应用
- 导入模块。如
import { styleModule } from "snabbdom/build/package/modules/style";
- init()函数注册模块,参数是个数组
应用h()函数创立VNode时,在第二个参数中传入对象。
模块导入注入款式,增加点击事件案例import { init } from "snabbdom/build/package/init";import { h } from "snabbdom/build/package/h";// 1导入模块import { styleModule } from "snabbdom/build/package/modules/style";import { eventListenersModule } from "snabbdom/build/package/modules/eventlisteners";// 2注册模块const patch = init([styleModule, eventListenersModule]);// 3应用h()函数的第二个参数传入模块中应用的数据(对象)let vnode = h("div", [ h("h1", { style: { backgroundColor: "skyblue" } }, "hello world"), h("p", { on: { click: eventHandler } }, "p click"),]);function eventHandler() { console.log("p click....");}let app = document.querySelector("#app");patch(app, vnode);
Snabbdom外围
版本2.1.0
init()设置模块,创立patch()函数
应用h()函数创立JavaScript对象(VNode)形容实在DOM(即一个JavaScript对象)
patch()比拟新旧两个Vnode,patch函数如果第一个参数为实在DOM,会先转化为VNode。
把变动的内容更新到实在DOM树
snabbdom中h函数的源码
import { vnode, VNode, VNodeData } from './vnode'import * as is from './is'export type VNodes = VNode[]export type VNodeChildElement = VNode | string | number | undefined | nullexport type ArrayOrElement<T> = T | T[]export type VNodeChildren = ArrayOrElement<VNodeChildElement>function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void { data.ns = 'http://www.w3.org/2000/svg' if (sel !== 'foreignObject' && children !== undefined) { for (let i = 0; i < children.length; ++i) { const childData = children[i].data if (childData !== undefined) { addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel) } } }}export function h (sel: string): VNodeexport function h (sel: string, data: VNodeData | null): VNodeexport function h (sel: string, children: VNodeChildren): VNodeexport function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNodeexport function h (sel: any, b?: any, c?: any): VNode { var data: VNodeData = {} var children: any var text: any var 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是字符串或者数字,primitive办法判断是否是字符串或者数字 } 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) // 转换为vnode对象 } } 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)};
patch整体过程剖析
- patch(oldVnode, newVnode)
- 把新节点中变动的内容渲染到实在DOM,最初返回新节点作为下一次解决的旧节点
- 比照新旧 VNode 是否雷同节点(节点的key 和sel 雷同)
- 如果不是雷同节点,删除之前的内容,从新渲染
- 如果是雷同节点,再判断新的 VNode 是否有text,如果有并且和oldVnode 的 text 不同,间接更新文本内容
- 如果新的 VNode 有 children,判断子节点是否有变动,会顺次比照节点
patch函数在init.ts
中实现
const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']init (modules: Array<Partial<Module>>, domApi?: DOMAPI){}
钩子函数在module遍历时被挂载
for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { const hook = modules[j][hooks[i]] if (hook !== undefined) { // cbs执行完数据结构 --> { create: [fn1,fn2],update:[fn1,fn2]...} (cbs[hooks[i]] as any[]).push(hook) } } }
patch函数源码
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node const insertedVnodeQueue: VNodeQueue = [] // 新插入节点队列,为了触发新插入节点insert函数 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]() if (!isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode) // DOM对象转化为VNode对象,解决id,类款式 } if (sameVnode(oldVnode, vnode)) { // 判断是否是雷同节点,比拟key属性和sel(选择器)是否都雷同 patchVnode(oldVnode, vnode, insertedVnodeQueue) } else { // 不是雷同节点 elm = oldVnode.elm! // !是typescript语法,示意肯定有值 parent = api.parentNode(elm) as Node // 获取父元素,不便新元素挂载到父元素 createElm(vnode, insertedVnodeQueue) // 创立VNode节点对应的DOM元素,并且把新插入的队列作为参数传递给createElm,函数外部会触发一些函数 if (parent !== null) { api.insertBefore(parent, vnode.elm!, api.nextSibling(elm)) // 父元素插入新元素,api.nextSibling(elm)代表老节点对应DOM元素的下一个兄弟节点 removeVnodes(parent, [oldVnode], 0, 0) // 老节点对应的DOM元素从parent中移除 } } for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]) // insertedVnodeQueue存储的是具备insert钩子函数的新的VNode节点,队列中元素是在createElm中增加 } for (i = 0; i < cbs.post.length; ++i) cbs.post[i]() // 触发cbs中post钩子函数 return vnode }
patch办法中调用了createElm办法,用于创立DOM元素
function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node { // 过程1:执行用户设置的init钩子函数 let i: any let data = vnode.data if (data !== undefined) { const init = data.hook?.init if (isDef(init)) { init(vnode) // data = vnode.data } } const children = vnode.children // vnode子节点 const sel = vnode.sel // sel为选择器 // 过程2:把vnode转换成实在DOM对象(没有渲染到页面) if (sel === '!') { // 选择器是!,创立正文节点 if (isUndef(vnode.text)) { vnode.text = '' } vnode.elm = api.createComment(vnode.text!) // 调用html API,创立正文节点 } else if (sel !== undefined) { // 创立对应DOM元素 // 如果选择器不为空,解析选择器 // Parse selector const hashIdx = sel.indexOf('#') const dotIdx = sel.indexOf('.', hashIdx) const hash = hashIdx > 0 ? hashIdx : sel.length const dot = dotIdx > 0 ? dotIdx : sel.length const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel const elm = vnode.elm = isDef(data) && isDef(i = data.ns) // data 和 data.ns是否有值 ? api.createElementNS(i, tag) : api.createElement(tag) // 判断是否有id和class选择器,有就增加id和类款式 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) // 触发create钩子函数 if (is.array(children)) { for (i = 0; i < children.length; ++i) { // 遍历子节点,可能会递归调用createElm const ch = children[i] if (ch != null) { api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue)) } } } else if (is.primitive(vnode.text)) { // 判断参数是否为string或者number api.appendChild(elm, api.createTextNode(vnode.text)) } const hook = vnode.data!.hook if (isDef(hook)) { hook.create?.(emptyNode, vnode) if (hook.insert) { insertedVnodeQueue.push(vnode) } } } else { // sel为空,创立文本节点 vnode.elm = api.createTextNode(vnode.text!) } // return vnode.elm }
patch中patchVnode函数
// 比照新旧两个vnode节点,找到差别,更新到DOM上 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! // 旧节点ele属性赋值给新节点ele const oldCh = oldVnode.children as VNode[] const ch = vnode.children as VNode[] if (oldVnode === vnode) return if (vnode.data !== undefined) { for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) // cbs中的钩子函数 vnode.data.hook?.update?.(oldVnode, vnode) // 用户传入的钩子函数 } // 第二个过程:真正比照新旧vnode差别的中央 if (isUndef(vnode.text)) { // 判断新节点是否有text属性 if (isDef(oldCh) && isDef(ch)) { // 判断新旧节点是否都有子节点 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) // 都有子节点并且不雷同,比照新旧节点子节点并更新DOM } else if (isDef(ch)) { // 新节点是否有子节点 if (isDef(oldVnode.text)) api.setTextContent(elm, '') // 老节点是否有text属性,有就清空DOM元素文本内容,并更新到elm addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) // 插入到elm } else if (isDef(oldCh)) { // 老节点有子节点 removeVnodes(elm, oldCh, 0, oldCh.length - 1) // 老节点对应的子节点从DOM树移除 } else if (isDef(oldVnode.text)) { // 判断老节点是否有text属性 api.setTextContent(elm, '') // 清空DOM元素对应文本内容 } } else if (oldVnode.text !== vnode.text) { // oldVnode.text有值并且新旧节点text不相等 if (isDef(oldCh)) { // 老节点有子节点,remove removeVnodes(elm, oldCh, 0, oldCh.length - 1) } api.setTextContent(elm, vnode.text!) // 更新text } // 第三个过程:触发postpatch钩子函数 hook?.postpatch?.(oldVnode, vnode) }
patchVnode中新旧节点都有子节点,并且不雷同时会调用updateChildren,是整个diff算法的外围
function updateChildren (parentElm: Node, // 父DOM元素 oldCh: VNode[], // 旧子节点 newCh: VNode[], // 新子节点 insertedVnodeQueue: VNodeQueue) { 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 | undefined // 存储一个对象,键是老节点对应的key,值是老节点的索引 let idxInOld: number let elmToMove: VNode let 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!)) // 旧开始节点对应DOM元素挪动到旧完结节点对应DOM元素之后 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!) // 旧完结节点对应DOM元素挪动到旧开始节点对应DOM元素之后 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // 开始和结尾比拟完结 if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) } idxInOld = oldKeyToIdx[newStartVnode.key as string] // 新节点的key在oldKeyToIdx中找到老节点索引存储到idxInOld if (isUndef(idxInOld)) { // New element 找不到,就是新元素 api.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 any api.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].elm addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else { // 新节点数组遍历完,旧节点有残余 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } } }