Appearance
Snabbdom源码解析
如何学习源码
- 先宏观了解
- 带着目标看源码
- 看源码的过程要不求甚解(先看主线逻辑,其他分支可以忽略)
- 调试
- 参考资料
Snabbdom 的核心流程
- 使用 h() 函数创建 VNode(js对象)
- 对象(VNode)描述真实 DOM
- init() 设置模块,创建 patch()
- patch() 比较新旧两个 VNode
- 把变化的内容更新到真实 DOM 树上
源码目录结构
下面具体说一下src的目录结构
- helpers
- modules
h函数
创建vnode,怎么创建VNode的?
在使用 Vue 的时候见过 h() 函数,vue中的h函数就是snabbdom中的h函数,vue对其进行了增强,实现了h组件机制,snabbdom没有组件机制。在vue中可以和使用h函数一样,还可以给里面传入选择器。
js
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
- h() 函数最早见于 hyperscript,使用 JavaScript 创建超文本(html字符串),Snabbdom 中的 h() 函数是对hyperscript中h函数的增强,不是用来创建超文本,而是创建 VNode。
函数重载
- 概念:参数个数或类型不同的函数
- JavaScript 中没有重载的概念
- TypeScript 中有重载,不过重载的实现还是通过代码调整参数
重载的示意
js
// 如果在js中下面的函数会替换上面的函数
// 如果是支持重载的语言,传入两个参数执行的是第一个add函数,如果传入的是三个参数执行的是第二个add函数
function add (a, b) {
console.log(a + b)
}
function add (a, b, c) {
console.log(a + b + c)
}
add(1, 2)
add(1, 2, 3)
源码位置:src/h.ts
ts
// 在最上面导入了依赖的模块
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>
// 给svg添加命名空间
function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
data.ns = 'http://www.w3.org/2000/svg'
if (sel !== 'foreignObject' && children !== undefined) {
//循环给子元素也递归添加命名空间
...
}
}
// h函数的重载 TypeScript支持重载JS不支持重载
// 上面知识定义了一些形式
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) {
// b可以是null值,如果b有值就存在data中,data的数据是模块处理所需要的数据
if (b !== null) {
data = b
}
// 判断c的类型
if (is.array(c)) {
// 是数组说明是子元素
children = c
} else if (is.primitive(c)) {
// 是字符串就放到text里面,标签中的文本
text = c
// 如果c是VNode判断sel属性即可,Vnode就变成数组
} else if (c && c.sel) {
children = [c]
}
// 两个参数的情况
} else if (b !== undefined && b !== null) {
...
}
// 判断children是否有值,遍历children中的每一个元素判断其是否是string或者number,如果是就是文本节点,并调用vnode函数返回虚拟节点
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
}
}
// 判断如果选择器是svg,就用addNS给svg定义命名空间,
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函数 —— VNode如何创建?
ts
// 导入依赖
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
// 接口(重点):约束实现当前接口的所有对象都拥有相同的属性
// VNode接口约束所有VNode对象都拥有相同的属性
export interface VNode {
// 选择器,调用h函数传入的第一个参数
sel: string | undefined
// 节点所需要的数据,是模块所需要的数据,类型是VNodeData定义的
data: VNodeData | undefined
// 子节点,与text属性互斥
children: Array<VNode | string> | undefined
// element 元素 VNode转换成DOM中,真实DOM在elm里面
elm: Node | undefined
// 标签之间的内容,与children互斥
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
}
// 函数(重点)参数就是VNode接口
// 需要传5个参数,对应了VNode接口的前5个属性,第6个属性在下面
export function vnode (sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
// 第六个属性key在这里
const key = data === undefined ? undefined : data.key
// 返回一个js对象
return { sel, data, children, text, elm, key }
}
VNode如何渲染真实DOM
原理
- patch(oldVnode, newVnode)
- 打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
- 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
- 如果新的 VNode 有 children,判断子节点是否有变化,判断子节点的过程使用的就是 diff 算法
- diff 过程只进行同层级比较
init函数
要了解patch函数就必须先了解init函数,执行过程看ppt
js
// 依赖模块
import { Module } from './modules/module'
import { vnode, VNode } from './vnode'
import * as is from './is'
import { htmlDomApi, DOMAPI } from './htmldomapi'
// 一堆类型定义
...
// 定义常量钩子函数,是个数组
const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']
// 定义并导出init函数
// 两个参数,modules模块,和dom操作(可选)
// 默认将虚拟DOM转换成真实DOM,如果要将虚拟DOM转换成字符串或者其他操作,就传第二个参数
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number
let j: number
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: []
}
// 如果第二个参数传了就是第二个参数,如果没传就是htmlDomApi.ts里面的关于DOM操作的对象:虚拟DOM转换成真实DOM
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
// 循环内部将modules所有的成员存储到cbs中,将来patch内部处理的时候会在某个时机调用钩子函数
/**
* modules.ts中规定了所有的函数的格式都必须是一个钩子函数,modules接口保证所有的模块对象都有一个或几个钩子函数
* 例如:这些模块中都有create和update属性
*
* export const attributesModule: Module = { create: updateAttrs, update: updateAttrs }
*
* 循环hook里面的属性,['create', 'update', 'remove', 'destroy', 'pre', 'post'],通过键去模块中找值(钩子函数),然后将所有的钩子函数存储到cbs中,将来在特定时机触发
*/
for (i = 0; i < hooks.length; ++i) {
// cbs.create = [] ,cbs.update = []
// 为啥是数组,因为模块中的class模块中也有create,attributes模块中也有create
cbs[hooks[i]] = []
// 接下来遍历每个模块,将每一个模块的钩子函数放到hook常量里面,如果hook存在就直接放到cbs数组中来
// cbs的最终形式是:cbs = { create: [fn1, fn2], update: [],...}
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]]
if (hook !== undefined) {
(cbs[hooks[i]] as any[]).push(hook)
}
}
}
// 中间一堆关于patch函数的东西
...
// 返回patch函数,如果在一个函数内部返回一个函数,称为高阶函数
// 高阶函数的好处,形成闭包,patch函数调用的时候不需要再传入modules和domApi了,直接使用init定义的变量
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
// patch函数内部逻辑
...
}
}
patch函数
比较两个节点进行DOM操作,操作有:
- 创建DOM元素
- 移除DOM元素
- 更新文本节点
- 更新DOM内的子节点
具体的实现在模块中
执行过程看ppt
js
...
// 判断两个节点是不是同一个节点,通过key(节点的唯一值)和sel(选择器)
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
// 判断节点是否为虚拟节点,通过判断其对象上面有没有sel属性
function isVnode (vnode: any): vnode is VNode {
return vnode.sel !== undefined
}
...
// 导出init函数
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
...
// 返回patch函数
// 对比两个vnode,将vnode的差异渲染给真DOM,返回新vnode
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node
// 常量 insertedVnodeQueue :插入节点的队列
// 新插入节点放到队列中的目的,是为了后续触发这些节点上设置的钩子函数,创建vnode的时候也可以有一些自定义的钩子函数
const insertedVnodeQueue: VNodeQueue = []
// 触发模块中的pre钩子函数(之前所有的钩子函数都放在cbs中),pre钩子函数是在处理vnode节点前执行的函数
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
// oldVnode可能是虚拟节点,也可能是真实DOM,例如#app,如果是真实DOM就把他们转化成虚拟节点
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode)
}
// 判断是不是相同的节点
if (sameVnode(oldVnode, vnode)) {
// 如果是相同节点就找差异更新DOM节点
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
// 如果不是相同节点就把新节点渲染成DOM插入到文档中,把老节点移除
// 后面加感叹号是ts语法,指的是,觉得旧节点中的element不为空
elm = oldVnode.elm!
// 获取elm的父节点
parent = api.parentNode(elm) as Node
// 创建vnode对应的DOM元素,并触发对应的钩子函数,这个时候并没有渲染到页面中
createElm(vnode, insertedVnodeQueue)
if (parent !== null) {
// 如果父节点不为空,就把vnode对应的DOM渲染到父节点中来,并且在elm之后的兄弟节点中
// 例如:<div id="app"></div>之后
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
// 移除旧节点
removeVnodes(parent, [oldVnode], 0, 0)
}
}
// 遍历新节点队列并处罚insert钩子函数
// 这里的感叹号都是判断其不为空,如果js来写需要用到if语句
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
}
// 执行cbs中的post钩子函数
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
// 返回新节点
return vnode
}
}
patch里面有三个重要的函数:patchVnode,createElm和removeVnodes,比较复杂内部需要解释一下:
createElm函数
用思维导图总结一下
js
// 创建vnode对应的DOM元素,并触发对应的钩子函数
function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any
// vnode的属性,用h函数创建vnode的时候的第二个参数 —— 模块中需要的数据
let data = vnode.data
if (data !== undefined) {
// 执行用户设置的 init 钩子函数
const init = data.hook?.init
// 判断init钩子函数是否有定义
if (isDef(init)) {
// 如果init钩子函数有定义的时候就直接调用,init是用户传过来的,有可能在内部修改data的值,所以下一步要把新的vnode.data赋值给data
init(vnode)
data = vnode.data
}
}
// 把vnode 转换成真实 DOM 对象(没有渲染到页面上)
/**
* vnode.children —— vnode的子节点
* vnode.sel —— vnode的选择器
*/
const children = vnode.children
const sel = vnode.sel
// 如果是感叹号就创建一个注释节点
if (sel === '!') {
// 如果vnode.text是undefined就设置为空,为了成功调用createComment
if (isUndef(vnode.text)) {
vnode.text = ''
}
// 创建一个注释节点
vnode.elm = api.createComment(vnode.text!)
// 不为undefined就创建对应的DOM元素
} else if (sel !== undefined) {
// 解析选择器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
// 创建DOM元素
// data.ns是是否有命名空间的意思
// 如果有定义且有命名空间,会创建带有命名空间的标签,一般情况是svg
// 否则创建普通标签
const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
? api.createElementNS(i, tag)
: api.createElement(tag)
// 给DOM元素设置id或者class属性
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
// 执行cbs的create钩子函数(模块中的钩子函数)
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
// 如果子节点是个数组,数组成员不为空那么就遍历数组的成员递归调用createElm
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i]
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
}
}
// 如果是string 或者是 number就创建一个文本节点
} else if (is.primitive(vnode.text)) {
api.appendChild(elm, api.createTextNode(vnode.text))
}
// hook是不是有定义不为空就调用create钩子函数(用户传递的钩子函数)
const hook = vnode.data!.hook
if (isDef(hook)) {
// 如果hook.create有值,就直接调用并传入空节点和vnode,如果create函数没有值就不调用此函数
hook.create?.(emptyNode, vnode)
// 如果钩子函数中有insert节点,那么会将此节点追加到钩子队列中,在patch函数的最后会遍历并执行
if (hook.insert) {
insertedVnodeQueue.push(vnode)
}
}
} else {
// 选择器为空那就是文本节点
vnode.elm = api.createTextNode(vnode.text!)
}
// 直接返回创建好的DOM元素
return vnode.elm
}
用户可以自己传递hook钩子函数
js
let vnode = h('div#app.cls', {
hook: {
init (vnode) {
console.log(vnode.elm)
},
create (emptyVnode, vnode) {
console.log(vnode.elm)
}
}
}, 'Hello World')
addVnodes和removeVnodes
addVnodes:批量添加节点
removeVnodes:批量删除节点
参数(四个):
- 父节点
- 移除的子节点,数组
- 循环的变量
- 循环的变量
js
// 批量删除节点
function removeVnodes (parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number): void {
// 循环,循环数组中的每一个元素
for (; startIdx <= endIdx; ++startIdx) {
let listeners: number
let rm: () => void
// 获取vnodes的每一个元素放到ch变量中
const ch = vnodes[startIdx]
if (ch != null) {
// 判断ch中有没有sel选择器的属性,如果有就是一个元素节点
if (isDef(ch.sel)) {
// 内部触发destory构造函数
invokeDestroyHook(ch)
// remove钩子函数的个数,作用是防止我们重复调用删除节点的方法
listeners = cbs.remove.length + 1
// createRmCb是一个高阶函数
// 返回了一个删除节点的函数rm
rm = createRmCb(ch.elm!, listeners)
// 循环遍历模块中的remove钩子函数并触发,每次调用之前都会判断listeners是否为0,就是remove钩子函数是否已经执行完了,只有为0的时候(即钩子函数都触发完毕之后)才调用removeChild方法
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
// 判断用户有没有出入remove钩子函数,如果有就触发
const removeHook = ch?.data?.hook?.remove
if (isDef(removeHook)) {
removeHook(ch, rm)
} else {
// 如果用户没有设置钩子函数,直接调用删除元素的方法
rm()
}
} else { // 如果是undefined就是一个文本节点Text node,如果是文本节点就直接从父元素中移除
api.removeChild(parentElm, ch.elm!)
}
}
}
}
// 触发destory钩子函数
function invokeDestroyHook (vnode: VNode) {
const data = vnode.data
if (data !== undefined) {
// 获取data中的hook,用户传来的destroy函数
data?.hook?.destroy?.(vnode)
// 模块中的destory钩子函数
for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
// 判断有没有子节点,如果有就递归调用执行子节点的destory钩子函数
if (vnode.children !== undefined) {
for (let j = 0; j < vnode.children.length; ++j) {
const child = vnode.children[j]
if (child != null && typeof child !== 'string') {
invokeDestroyHook(child)
}
}
}
}
}
// 批量删除节点
function removeVnodes (parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number): void {
// 循环,循环数组中的每一个元素
for (; startIdx <= endIdx; ++startIdx) {
let listeners: number
let rm: () => void
// 获取vnodes的每一个元素放到ch变量中
const ch = vnodes[startIdx]
if (ch != null) {
// 判断ch中有没有sel选择器的属性,如果有就是一个元素节点
if (isDef(ch.sel)) {
// 内部触发destory构造函数
invokeDestroyHook(ch)
// remove钩子函数的个数,作用是防止我们重复调用删除节点的方法
listeners = cbs.remove.length + 1
// createRmCb是一个高阶函数
// 返回了一个删除节点的函数rm
rm = createRmCb(ch.elm!, listeners)
// 循环遍历模块中的remove钩子函数并触发,每次调用之前都会判断listeners是否为0,就是remove钩子函数是否已经执行完了,只有为0的时候(即钩子函数都触发完毕之后)才调用removeChild方法
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
// 判断用户有没有出入remove钩子函数,如果有就触发
const removeHook = ch?.data?.hook?.remove
if (isDef(removeHook)) {
removeHook(ch, rm)
} else {
// 如果用户没有设置钩子函数,直接调用删除元素的方法
rm()
}
} else { // 如果是undefined就是一个文本节点Text node,如果是文本节点就直接从父元素中移除
api.removeChild(parentElm, ch.elm!)
}
}
}
}
addVnodes:批量增加节点
参数(四个):
- 父节点
- 移除的子节点,数组
- 循环的变量
- 循环的变量
js
// 批量增加节点
/**
*
* @param parentElm 父节点
* @param before 参考节点,插入元素的时候会插入到before之前
* @param vnodes 添加的子节点,数组
* @param startIdx 循环变量
* @param endIdx 循环变量
* @param insertedVnodeQueue 队列
*/
function addVnodes (
parentElm: Node,
before: Node | null,
vnodes: VNode[],
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
// 循环遍历所有节点,如果节点不为null的话就转换成虚拟DOM,在调用insertBefore,插入到bdefore元素之前
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)
}
}
}
patchVnode
脑图ppt
js
// 创建key和老节点索引的关系
function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
// 定义map常量,是一个空对象
const map: KeyToIndexMap = {}
// map的key是节点的key,值是索引
for (let i = beginIdx; i <= endIdx; ++i) {
const key = children[i]?.key
if (key !== undefined) {
map[key] = i
}
}
// 返回map
return map
}
// 对比新旧节点更新差异
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
// 1. 执行prepatch和update两个钩子函数,两个钩子的差别是无论新旧节点是否相同,都会执行prepatch,但之后新旧节点不相同,才会执行update
// 获取用户传入的prepatch钩子函数
const hook = vnode.data?.hook
hook?.prepatch?.(oldVnode, vnode)
// 老节点的dom属性赋值给新节点的dom属性,并存一个常量elm
const elm = vnode.elm = oldVnode.elm!
// 获取新节点和老节点中的子节点
const oldCh = oldVnode.children as VNode[]
const ch = vnode.children as VNode[]
// 判断两个节点的内存地址是否相同,如果相同表示么有变化直接返回
if (oldVnode === vnode) return
// 判断节点的data属性是不是为空
if (vnode.data !== undefined) {
// 不为空就循环执行模块中的update钩子函数
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 再执行用户自定义的update钩子函数
vnode.data.hook?.update?.(oldVnode, vnode)
}
// 2. 对比啷个vnode
if (isUndef(vnode.text)) {
// 判断老节点的子节点和新节点的子节点是否都有定义
if (isDef(oldCh) && isDef(ch)) {
// 判断新老节点的子节点是否不相同,如果不相同就会对比新旧节点的子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
// 判断新节点的子节点有定义,老节点的子节点没有定义
} else if (isDef(ch)) {
// 老节点是否有文本内容,有的话就把文本内容清空,然后将新节点添加到字节点上来
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 判断老节点的子节点有定义,新节点的子节点没有定义
} else if (isDef(oldCh)) {
// 把老节点的所有节点全部移除
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果老节点有text属性,新节点没有text属性,那么把老节点的文本清空
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 新节点有文本节点,老节点和新节点的文本节点不同,将老节点的子节点全部移除,再设置文本节点
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
api.setTextContent(elm, vnode.text!)
}
// 3. 触发用户传入的postpatch钩子函数
hook?.postpatch?.(oldVnode, vnode)
}
updateChildren
diff算法的核心,内部对比新旧节点的children,更新DOM
js
/**
* updateChildren
* 整体分析:
* diff算法的核心,内部对比新旧节点的children,更新DOM
* @param parentElm 父节点
* @param oldCh 老节点数组
* @param newCh 新节点数组
* @param insertedVnodeQueue 调用addVnodes需要传的参数
*/
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
// 1. 定义了很多变量
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
let idxInOld: number
let elmToMove: VNode
let before: any
// 2. 循环,在循环中比较新旧两个元素
// 当老节点的开始索引小于等于老节点的结束索引 && 新节点开始索引小于等于新节点的结束索引 开始循环
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 先判断新旧开始结束节点是否为null,因为索引变化后可能会被节点设置为空
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]
// 开始比较开始和结束节点的四种情况
// 先比较旧开始节点和新开始节点是否相同,判断key和sel是否相同
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 如果是相同关节点,调用patchVnode判断新老节点的差异然后更新到DOM
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
// 判断完之后新老节点的开始索引++,同时到下一个节点,进入下一次循环
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// 如果两者开始节点不是相同节点,就判断其结束节点是不是相同节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 如果两个结束节点相同,就执行PatchVnode比较两个节点差异并更新DOM
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
// 将两个结束节点的索引--,同时到前一个节点,进入下一次循环
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// 下面两种情况可能是排序
// 如果两者的结束节点不同,就比较老的开始节点和新的结束节点是否是相同节点
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 如果他们是相同节点,就用patchVnode比较两个节点差异并更新DOM
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 要把旧的开始节点移动到旧的结束节点之后
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
// 老节点的开始索引向后移动,新节点的结束索引向前移动
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果前面都不满足,就比较旧的结束节点和新的开始节点是否是相同节点
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 如果是相同节点就执行patchVnode比较两个节点差异并更新DOM
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 把就的结束节点移动到旧的开始节点之前
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
// 老节点的结束索引向前移动,新节点的开始索引向后移动
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 新旧开始结束节点比较完毕
// 下面要去遍历所有新节点,使用新节点的key在老节点的数组中找相同key的节点
} else {
// 先设置记录 key 和 index 的对象
if (oldKeyToIdx === undefined) {
// 如果是undefined就通过函数初始化其值(传参,老节点数组,老节点开始索引,老节点结束索引)
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// 用新节点的key作为索引去老节点中找,可能找到可能没有找到
idxInOld = oldKeyToIdx[newStartVnode.key as string]
// 新节点没有在老节点中找到对应元素,就是新的元素
if (isUndef(idxInOld)) { // New element
//通过createElm创建DOM对象插入到老开始节点之前
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
// 在老元素中找到了对应元素,将其从数组中提取并记录到elmToMove中
elmToMove = oldCh[idxInOld]
// 判断老节点和新节点的选择器是否相同,如果不同说明被修改过,就创建新节点并把它插到对应的开始节点之前
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
// 如果老节点和新节点的选择器相同,就调用patchVnode两个节点的差异并更新到DOM上
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
// 将移动的老节点的数组对应位置设置成undefined,因为此节点已经被移动走了
oldCh[idxInOld] = undefined as any
// 再把老节点移动到老开始节点之前
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
}
}
// 重新给新开始节点赋值,指向下一个新节点
newStartVnode = newCh[++newStartIdx]
}
}
// 3. 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成
// 保证至少有一个没有被遍历完,即新老节点的个数如果是相同的,就不走下面的内容了。
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
// 两者其中一个还有剩余,判断新老节点哪个先遍历完成
if (oldStartIdx > oldEndIdx) {
// 新节点有剩余,老节点的数组先遍历完成,说明有些内容是要新增的,使用addVnodes添加新元素
// before是参考节点,表示将新节点插入到哪个位置中
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
// 参数是父节点,参考节点,新节点数组,添加的前后范围...
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else {
// 老节点有剩余,新节点的数组先遍历完成,说明有些内容是要删除的,使用removeVnodes删除元素
// 参数是父节点,老节点数组,继续剩余的位置
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
}
patch() -> patchVnode() -> updateChildren()
模块
- Snabbdom为了保证核心库的精简,把处理元素的属性、事件、样式等工作,放置到模块中。
- 模块默认不会被导入,如果需要则要按需引入。
- 模块使用查看官方文档
- 模块的核心基于Hooks
- 内部其他都是定义的模块(attributes:DOM元素属性,class:样式,dataset:data-属性,eventlisteners:事件,props,style:行内样式)
Hooks
定义了参数类型,和接口,接口中定义了钩子函数需要的名称以及对应的类型。
js
// 定义了参数类型,确定了要传什么参数和返回值
...
// 定义了接口,钩子函数需要的名称以及对应的类型
export interface Hooks {
// patch 函数执行开始的时候触发
pre?: PreHook
// createElm 函数开始之前的时候触发
// 在把 VNode 转换成真实 DOM 之前触发
init?: InitHook
// createElm 函数末尾调用
// 创建完真实 DOM 后触发
create?: CreateHook
// patch 函数末尾执行
// 真实 DOM 添加到 DOM 树中触发
insert?: InsertHook
// patchVnode 函数开头调用
// 开始对比两个 VNode 的差异之前触发
prepatch?: PrePatchHook
// patchVnode 函数开头调用
// 两个 VNode 对比过程中触发,比 prepatch 晚
update?: UpdateHook
// patchVnode 的最末尾嗲用
// 两个 VNode 对比结束执行
postpatch?: PostPatchHook
// removeVNodes -> invokeDestoryHook 中调用
// 在删除元素之前触发,子节点的 destory 也被触发
destroy?: DestroyHook
// removeVNodes中调用
// 元素被删除的时候触发
remove?: RemoveHook
// patch函数的最后调用
post?: PostHook
}
module
导入了hooks,定义了模块的钩子函数,并不是所有的钩子函数。
attributes
js
// 导入module
import { VNode, VNodeData } from '../vnode'
import { Module } from './module'
export type Attrs = Record<string, string | number | boolean>
// 定义了一些常量
const xlinkNS = 'http://www.w3.org/1999/xlink'
const xmlNS = 'http://www.w3.org/XML/1998/namespace'
const colonChar = 58
const xChar = 120
// attributes的核心函数
function updateAttrs (oldVnode: VNode, vnode: VNode): void {
var key: string
var elm: Element = vnode.elm as Element
// 老节点的属性
var oldAttrs = (oldVnode.data as VNodeData).attrs
// 新节点的属性
var attrs = (vnode.data as VNodeData).attrs
// 判断新老属性有没有定义,没有定义直接返回
if (!oldAttrs && !attrs) return
// 新老节点的attrs属性相同,说明修改也直接返回
if (oldAttrs === attrs) return
oldAttrs = oldAttrs || {}
attrs = attrs || {}
// 遍历新节点的所有属性
// update modified attributes, add new attributes
for (key in attrs) {
// 获取新老节点的属性值
const cur = attrs[key]
const old = oldAttrs[key]
// 判断属性值是否相同
if (old !== cur) {
// 新节点属性值是否是布尔类型的true
if (cur === true) {
// 如果是布尔类型就直接设置给DOM,值是空字符串
elm.setAttribute(key, '')
// 如果是布尔类型的false,就直接把属性移除掉
} else if (cur === false) {
elm.removeAttribute(key)
// 如果不是布尔类型
} else {
// 判断第一个元素是不是 x 如果是就是属于svg中处理命名空间属性
// xChar -> x
// <svg xmlns="http://www.w3.org/2000/svg">
// 如果不是就按照正常的设置属性值
if (key.charCodeAt(0) !== xChar) {
elm.setAttribute(key, cur as any)
// 下面是判断是不是命名空间的,如果是就用 setAttributeNS 方法
} else if (key.charCodeAt(3) === colonChar) {
// colonChar -> :
// Assume xml namespace
elm.setAttributeNS(xmlNS, key, cur as any)
} else if (key.charCodeAt(5) === colonChar) {
// <svg xmlns:xlink="https://www.w3.org/1999/xlink">
// Assume xlink namespace
elm.setAttributeNS(xlinkNS, key, cur as any)
} else {
// 其他的就用正常属性去设置
elm.setAttribute(key, cur as any)
}
}
}
}
// 遍历老节点的属性,如果在新节点中没有出现就直接移除
for (key in oldAttrs) {
if (!(key in attrs)) {
elm.removeAttribute(key)
}
}
}
// 导出 create钩子函数和update钩子函数
export const attributesModule: Module = { create: updateAttrs, update: updateAttrs }