共计 11713 个字符,预计需要花费 30 分钟才能阅读完成。
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,比拟差别,更新到 DOM | |
let 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 | null | |
export 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): 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 = {} | |
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 =} | |
} 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) | |
} | |
} | |
} |
正文完