下载 Snabbdom 源码
拉取代码
使用 v2.1.0 作为示例:
git
git clone https://github.com/snabbdom/snabbdom.git
git checkout v2.1.0
安装依赖
对于 node
版本有要求,我用的 v12
,可以成功安装。
js
npm i
编译
因为测试案例中使用了编译之后的目录,所以在运行测试代码之前需要先编译:
js
npm run compile
运行查看测试案例
需要先在 vscode 中安装 Live Server 插件,然后在 snabbdom\examples\reorder-animation\index.html 页面右键,选择 Open with Live Server,就会在5500端口打开这个测试界面,我们可以发现测试界面的删除和排序是不起作用的,这是因为 demo 中使用的 onclick 写法是被废除的,需要修改为正确的写法:
js
- h('div.btn.rm-btn', { on: { click: [remove, movie] } }, 'x'),
+ h('div.btn.rm-btn', { on: { click: () => { remove(movie) } } }, 'x'),
js
- h('a.btn.rank', { class: { active: sortBy === 'rank' }, on: { click: [changeSort, 'rank'] } }, 'Rank'),
- h('a.btn.title', { class: { active: sortBy === 'title' }, on: { click: [changeSort, 'title'] } }, 'Title'),
- h('a.btn.desc', { class: { active: sortBy === 'desc' }, on: { click: [changeSort, 'desc'] } }, 'Description'),
+ ('a.btn.rank', { class: { active: sortBy === 'rank' }, on: { click: () => {changeSort('rank')} } }, 'Rank'),
+ h('a.btn.title', { class: { active: sortBy === 'title' }, on: { click: () => {changeSort('title')} } }, 'Title'),
+ ('a.btn.desc', { class: { active: sortBy === 'desc' }, on: { click: () => {changeSort('desc')} } }, 'Description')
模拟核心
init()
设置模块,建立patch
函数- 使用
h()
函数创建js
对象(vnode
)描述真实DOM
patch
比较新旧 2 个vnode
,如果第一个参数是真实DOM
,会转换为vnode
- 把变化的内容更新到真实
DOM
树
h
h
函数的核心就是处理参数 ,调用 vnode
函数创建一个 vnode
对象返回。
h
函数中定义了 4 种 h
函数的重载,ts
支持重载,js
不支持重载,当 ts
代码被编译成 js
后,只有一个 h
函数,所以参数的差异是通过在函数内部判断类型来支持的。
js
// 定义了 4 种 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
// 真正的 h 函数
export function h (sel: any, b?: any, c?: any): VNode {
...
// 参数处理
if (c !== undefined) {
// 三个参数的情况:sel(选择器),data,children/text
...
}
...
// h 函数的核心就是处理参数,调用 vnode 函数创建一个 vnode 对象返回
return vnode(sel, data, children, text, undefined)
};
vnode
接收 5 个参数,返回 VNode
类型的对象。
js
export function vnode (sel, data, children, text, elm) {
const key = data === undefined ? undefined : data.key
return { sel, data, children, text, elm, key }
}
init
参数
modules
: 模块数组domApi
: 把vnode
对象转换为其他平台下的元素,不传值时默认是浏览器Dom
操作的Api
,传递domApi
就可以实现跨平台
返回值
init
作为一个高阶函数,返回 patch
函数。
这样做的好处是本来调用 patch
要传递4个参数:modules
, domApi
, oldVnode
, vnode
。因为 patch
函数的使用频率很高,所以先通过 init
初始化前2个参数,并将前2个参数缓存,以后再调用 patch
函数时只需传入 oldVnode
、vnode
即可。
方法体
js
// 定义了一些辅助函数和一些类型
type ...
function ...
// 定义了 hooks 数组,存储的是钩子函数的名称,在 init 的时候被初始化,然后在特定的时间执行
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: []}
// domApi不传值时默认是浏览器Dom操作的Api,传递domApi就可以实现跨平台
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
// 嵌套循环
// 第一层循环初始化 cbs,这里上面已经在定义的时候就初始化了,这段就没有意义了
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
// 第二层循环遍历 modules 模块,将模块的钩子函数添加到对应的 cbs 数组中
// cbs => { create: [fn1, fn2], update: [fn1, fn2],...}
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]]
if (hook !== undefined) {
(cbs[hooks[i]] as any[]).push(hook)
}
}
}
// 定义很多内部函数
function ...
// 返回 patch 函数
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
...
}
}
patch
patch
整体过程:
- 判断新节点是否为
vnode
对象(有sel
选择器属性),如果是Dom
对象则转换为vnode
对象 - 对比新旧
VNode
是否相同节点(节点的key
标识和sel
选择器相同) - 如果是相同节点,对比这两个
vnode
节点的内容是否有变化,调用 patchVnode 方法 - 如果不是相同节点,调用 createElm 创建新的
Vnode
对应的Dom
元素,并将新创建的dom
元素插入到dom
树上,还要在dom
树上将旧的vnode
节点对应的元素移除 - 把新节点中变化的内容渲染到真实
DOM
,最后返回新节点作为下一次处理的旧节点
js
function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node
// 存储新插入节点的队列,为了触发这些节点上的 insert 钩子函数
const insertedVnodeQueue: VNodeQueue = []
// 触发 pre 预处理钩子函数
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
// 判断一个对象是否为 vnode 对象,如果一个对象有 sel 选择器属性,就认作 vnode 对象
if (!isVnode(oldVnode)) {
// emptyNodeAt 把 Dom 对象转换为 vnode 对象
oldVnode = emptyNodeAt(oldVnode)
}
// 判断新旧 vnode 是否是相同节点,key 和 sel 相同即判定为相同节点
if (sameVnode(oldVnode, vnode)) {
// 对比这两个 vnode 节点的内容是否有变化,而 vnode 对应的 Dom 元素是不需要重新创建的,这样会提升我们的效率
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
// 新旧 vnode 不是同一个节点,会创建新的 Vnode 对应的 Dom 元素
// 并将新创建的 dom 元素插入到 dom 树上
// 还要在 dom 树上将旧的 vnode 节点对应的元素移除
elm = oldVnode.elm! // elm 为 oldVnode 对应的 dom 元素
parent = api.parentNode(elm) as Node // 获取 elm 的父元素,使用的就是 node.parentNode
// 创建 vnode 节点对应的 dom 元素,同时触发一些钩子函数
createElm(vnode, insertedVnodeQueue)
if (parent !== null) {
// 如果有 parent 父元素的话,将新 vnode 对应的 dom 元素插入到旧元素的后一个节点之前,也就是插入到旧节点之后
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
// 把旧节点从 parent 中移除
removeVnodes(parent, [oldVnode], 0, 0)
}
}
// 触发对应的钩子函数
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
// 返回新节点作为下一次处理的旧节点
return vnode
}