Snabbdom 源码解析与如何调试(一)

life.cao
2023-08-23 23:20:15
阅读 6
如何学习源码
- 先宏观了解
- 带着目标看源码
- 看源码的过程要不求甚解
- 调试
- 参考资料
Snabbdom 的核心
- 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
- init() 设置模块,创建 patch()
- patch() 比较新旧两个 VNode
- 把变化的内容更新到真实 DOM 树上
Snabbdom 源码
源码地址:
src 目录结构
├── package │ ├── helpers │ │ └── attachto.ts 定义了 vnode.ts 中 AttachData 的数据结构 │ ├── modules │ │ ├── attributes.ts │ │ ├── class.ts │ │ ├── dataset.ts │ │ ├── eventlisteners.ts │ │ ├── hero.ts example 中使用到的自定义钩子 │ │ ├── module.ts 定义了模块中用到的钩子函数 │ │ ├── props.ts │ │ └── style.ts │ ├── h.ts h() 函数,用来创建 VNode │ ├── hooks.ts 所有钩子函数的定义 │ ├── htmldomapi.ts 对 DOM API 的包装 │ ├── init.ts 加载 modules、DOMAPI,返回 patch 函数 │ ├── is.ts 判断数组和原始值的函数 │ ├── jsx-global.ts jsx 的类型声明文件 │ ├── jsx.ts 处理 jsx │ ├── thunk.ts 优化处理,对复杂视图不可变值得优化 │ ├── tovnode.ts DOM 转换成 VNode │ ├── ts-transform-js-extension.cjs │ ├── tsconfig.json ts 的编译配置文件 │ └── vnode.ts 虚拟节点定义
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: 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)function add (a: number, b: number) { console.log(a + b) } function add (a: number, b: string) { console.log(a + b) } add(1, 2) add(1, '2')
源码位置:src/package/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 = {} 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 } 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 } else if (is.primitive(b)) { // 如果 c 是字符串或者数字 text = b } else if (b && b.sel) { // 如果 b 是 VNode 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) };
VNode
一个 VNode 就是一个虚拟节点用来描述一个 DOM 元素,如果这个 VNode 有 children 就是 Virtual DOM
源码位置:src/package/vnode.ts
export interface VNode {
// 选择器
sel: 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 {
const key = data === undefined ? undefined : data.key
return { sel, data, children, text, elm, key }
}snabbdom
- patch(oldVnode, newVnode)
- 打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
- 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
- 如果新的 VNode 有 children,判断子节点是否有变化,判断子节点的过程使用的就是 diff 算法
- diff 过程只进行同层级比较

init
功能:init(modules, domApi),返回 patch() 函数(高阶函数)
为什么要使用高阶函数?
- 因为 patch() 函数再外部会调用多次,每次调用依赖一些参数,比如:modules/domApi/cbs
- 通过高阶函数让 init() 内部形成闭包,返回的 patch() 可以访问到 modules/domApi/cbs,而不需要重新创建
init() 在返回 patch() 之前,首先收集了所有模块中的钩子函数存储到 cbs 对象中
源码位置:src/package/init.ts
const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post'] export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) { let i: number let j: number const cbs: ModuleHooks = { create: [], update: [], remove: [], destroy: [], pre: [], post: [] } // 初始化 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 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/package/init.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) as Node // 触发 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]() return vnode }
评论