虚拟 dom
1.dom 操作开销很大 2.使用 js 来描述 dom 有更强的灵活性
大体过程
- h 函数将参数解析,并调用 vnode 函数创建虚拟节点
- patch 函数对比新旧节点,删掉旧节点独一无二的部分,并渲染新节点独有的部分
init
返回一个 patch 函数,此处使用了闭包,就能直接在 patch 中使用模块和 DOMAPI 而无需传入
第一个参数为模块数组,第二个参数为 DOMAPI
init(modules: Array<Partial<Module>>, domApi?: DOMAPI)
模块
就是 VNode 的 data,总共包括6个模块 attributes: 设置 DOM 属性 -> setAttribute() props: 设置 DOM 属性 -> element[attr] = value class: 类 dataset: 以 :data- 开头的属性 eventlisteners: 注册/移除事件 style: 行内样式
首先会设定好一个存放钩子函数的对象cbs,并遍历模块,把所有的钩子函数放入cbs
const hooks: Array<keyof Module> = [
"create",
"update",
"remove",
"destroy",
"pre",
"post",
];
/*
**********************
** in init function **
**********************
*/
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: [],
};
for (const hook of hooks) {
for (const module of modules) {
const currentHook = module[hook];
if (currentHook !== undefined) {
(cbs[hook] as any[]).push(currentHook);
}
}
}
DOMAPI
默认为 htmlDomApi,里面包含了浏览器 DOM 操作的众多 API,通过调用这些 API 来操作/转换真实 DOM
const htmlDomApi: DOMAPI = {
createElement,
createElementNS,
createTextNode,
createComment,
insertBefore,
removeChild,
appendChild,
parentNode,
nextSibling,
tagName,
setTextContent,
getTextContent,
isElement,
isText,
isComment,
};
h 解析参数
将传入的参数解析为 text,data,children 作为参数传给 vnode 函数,得到 VNode 节点,并将其返回
在调用 vnode 前会遍历一次 children,把其中所有的基本类型作为 VNode 函数的 text 参数(其他参数都为 undefined),并把生成的 VNode 节点替换掉数组中的对应元素
重载
涉及函数重载:根据参数的不同类型、个数的情况,函数做出不同的处理。
h(sel: string, data: Object, children: Array | string | Object)
h(sel: string, children: Array | string)
h(sel: string, data: Object)
h(sel: string)
vnode 生成虚拟节点
返回虚拟节点,参数 sel, data, children, text, elm
interface VNode {
sel: string | undefined;
data: VNodeData | undefined;
children: Array<VNode | string> | undefined;
elm: Node | undefined;
text: string | undefined;
key: Key | undefined;
}
patch 对比新旧节点并渲染
接收两个 VNode,不过第一个也可以是 Element。
- 执行所有的
pre钩子函数 - 如果第一个参数不是 VNode,就会传给
emptyNodeAt函数,其作用是将真实 DOM 转为 VNode:把自身作为 VNode 的 elm 属性,并生成 sel。 - 新旧节点比较,调用
sameVnode:如果 key 和 sel 都相等,则视为相同节点。- 对于相同节点,调用
patchVnode函数,对比两者内容的差异 - 对于不同节点,获取到旧节点的父节点,并把新节点转换成真实 DOM,之后调用
insertBefore将新节点的DOM移到父节点中、nextSibling(旧节点的下一节点)之前———也就是紧贴着旧节点后面,最后移除旧节点。
- 对于相同节点,调用
- 顺序执行
inserted、post钩子函数 - 返回新节点
createElm 将VNode转换为真实节点
- 解析 sel,通过标签创建节点放入 elm,并把id和class通过
setAttribute设置上去。如果没有 sel,则把 text 创建为文本节点。 - 执行 create 钩子函数
- 遍历 children,将每一项作为参数递归调用,并通过
appendChild放入 elm。如果没有 children,则把 text 创建为文本节点。 - 返回 elm
addVnodes/removeVnodes
addVnodes:调用createElm将 vnode 转变为真实节点,并将其添加入真实 DOM 中
removeVnodes:
patchVnode
patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
)
- 将 oldVnode.elm 赋给 vnode.elm
- 如果两个节点全等,则直接返回
- 如果 vnode.text 不存在,代表其可能有 children 或者啥也没有,做以下操作:
- 如果 oldVnode.children 和 vnode.children 都存在且不相等,则调用
updateChildren更新子节点 - 如果只有 vnode.children 存在,意味着 oldVnode 要么是一个只有文本内容的节点或没有内容,只需要调用
addVnodes,把 vnode.children 添加到 elm 中。对于前者,还需要把文本内容设为空字符 - 如果只有 oldVnode.children 存在,意味着 vnode 是空节点,只需要调用
removeVnodes,移除 elm 中所有的子节点 - 如果都没有 children 且 oldVnode.text 存在,代表是一个只有文本的节点,将其设为空字符串
- 如果 oldVnode.children 和 vnode.children 都存在且不相等,则调用
- 如果 vnode.text 存在且与 oldVnode.text 不同,代表其为只有文本的节点且需要更新,做以下操作:
- 如果 oldVnodes.children 存在,则直接调用
removeVnodes清空子节点 - 将文本内容设置为 vnode.text
- 如果 oldVnodes.children 存在,则直接调用
updateChildren
- 分别设置新、老节点的 children 的开始、结束索引值以及元素
- 循环,条件为
oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,以下操作每次循环只按照判断语句执行其中一条- 如果四个端点节点其一为 null,则向数组中间靠拢(已移动的vnode会被null替换)
- 调用
sameVnode(oldStartVnode, newStartVnode),如果相等则对其调用patchVnode,并移动指针 - 调用
sameVnode(oldEndVnode, newEndVnode),如果相等则对其调用patchVnode,并移动指针 - 调用
sameVnode(oldEndVnode, newEndVnode),如果相等则对其调用patchVnode,并移动指针 - 调用
sameVnode(oldStartVnode, newEndVnode),相等则调用patchVnode,并把oldStartVnode移动到oldEndVnode之后,并移动指针 - 调用
sameVnode(oldEndVnode, newStartVnode),相等则调用patchVnode,并把oldEndVnode移动到oldStartVnode之前,并移动指针 - 查找新节点的 key 是否在旧节点的 key map中出现(key map查找复杂度O(1))
- 未出现则代表是新节点,直接插入到
oldStartVnode之前 - 出现了则判断选择器是否相等,相等则代表是同一节点,调用
patchVnode,并插入到oldStartVnode之前,并把老节点置空,不相等则直接插入无须比较
- 未出现则代表是新节点,直接插入到
- 判断新旧节点是哪个先遍历完成,对于老节点先遍历完,把剩余的新节点插入到右边,对于新节点先遍历完,把老节点没遍历到的部分移除,不过由于移动后的老节点已经被置空了,所以通过key找到的老节点不受影响