引言
最近在读snabbdom源码,在学习途中发现下载的GitHub源码结构和别人博客上的有一些差别,查看提交记录发现最近几周 snabbdom 做了一点点变动。我们到npm网站上也可以看到,18天前版本从0.7.4升级到了1.0.1。
通过以下链接看到snabbdom的npm相关信息:npm地址
所以如果想跑一下目前博客上的Demo就只能指定snabbdom包的版本了
npm i snabbdom@0.7.4
更新后源码变化
简单比对了下,比较明显的就是:
-
modules、helpers文件夹合并到了package中
-
snabbdom.ts改为=>init.ts
文件目录结构
结构简单,就三个文件夹:
- examples:官方案例,下载后先使用npm安装依赖包,在运行
npm run compile
命令打包后即可运行
- pref:Snabbdom benchmarks(暂时没啥用)
- src: 存放snabbdom源码
核心函数
h.ts
其实在Vue实例中已经见过h()了,它的作用就是创建一个虚拟节点VNode。
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
在h.ts文件中,h函数通过函数重载的方式定义,再通过if语句判断变量是否是undefined,来处理三个参数、两个参数时的情况。
// 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) {
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)) {
text = b
} else if (b && b.sel) {
children = [b]
} else { data = b }
}
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
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] === '#')
) {
addNS(data, children, sel)
}
return vnode(sel, data, children, text, undefined)
};
vnode.ts
VNode 就是用一个虚拟节点用来描述一个 DOM 元素,如果这个 VNode 有 children 就是 Virtual DOM。
结构比较简单,定义了VNode的接口和函数实现,该函数参数分别表示的是:
- sel: 选择器;
- data:节点数据VNodeData (属性/样式/事件等 )
- children:子节点,要么是VNode,要么是text;
- elm:记录 vnode 对应的真实 DOM
- text :节点中的内容,和 children 只能互斥
- key: 该参数不在接口中,值由data所影响
import { Hooks } from './hooks'
import { AttachData } from './helpers/attachto'
import { VNodeStyle } from './modules/style'
import { On } from './modules/eventlisteners'
import { Attrs } from './modules/attributes'
import { Classes } from './modules/class'
import { Props } from './modules/props'
import { Dataset } from './modules/dataset'
import { Hero } from './modules/hero'
export type Key = string | number
export interface VNode {
sel: string | undefined
data: VNodeData | undefined
children: Array<VNode | string> | undefined
elm: Node | undefined
text: string | undefined
key: Key | undefined
}
export interface VNodeData {
props?: Props
attrs?: Attrs
class?: Classes
style?: VNodeStyle
dataset?: Dataset
on?: On
hero?: Hero
attachData?: AttachData
hook?: Hooks
key?: Key
ns?: string // for SVGs
fn?: () => VNode // for thunks
args?: any[] // for thunks
[key: string]: any // for any other 3rd party module
}
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 }
}
init.ts(原snabbdom.ts)
核心代码,调用init函数时会返回一个patch函数,用于比较两个vnode差异并更新,这也是虚拟DOM的原理。
大致结构如下:
- 开头部分先定义了所需的类型以及所需的辅助函数
- 第46行定义了常量hooks,存放钩子函数名称的一个数组(不同的钩子函数在不同时期执行)
- 最后就是导出部分了,将init导出(这一块与0.7.4版本不同,老版导出了h,thunk,init核心三件套)
从48行到开始到最后都是init的实现,首先init传入两个参数:
- modules:模块,处理元素的样式、属性、事件(都存放在package/modules中)
- domapi:执行一些DOM操作(定义在htmldomapi.ts中),其实就是调用了ducoment下dom操作的api
代码实现上,先初始化转换虚拟节点的domapi
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
再把传入的所有modules的钩子函数,通过遍历全部存储到一个cbs对象中
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[hooks[i]] as any[]).push(hook)
}
}
}
接下来就是在函数内部定义了很多的辅助函数,它们主要是用到patch函数内部,也就是init返回值,它会在合适的时机调用:
最后就是返回的patch函数了,参数传入新旧节点,比较后进行更新。首先定义的insertedVnodeQueue是一个队列,保存新插入的节点,它是为了触发节点的钩子函数。之后for循环先执行了cbs对象中pre的钩子函数,再判断oldVnode是真实DOM还是VNode,如果是真实DOM则转为空的VNode。转化后,判断新旧节点是不是相同的节点,是则比较新旧节点差异并更新到DOM中,不是则需要把新节点渲染成DOM,再触发init/create的钩子函数。最后两个for循环分别执行用户的insert和模块的post钩子函数,执行完后把新节点返回,作为更新后的旧节点,以便于下次更新比较。
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node
const insertedVnodeQueue: VNodeQueue = []
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode)
}
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
elm = oldVnode.elm!
parent = api.parentNode(elm) as Node
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])
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
return vnode
}
}