Virtual DOM源码阅读记录

78 阅读5分钟

虚拟 dom

1.dom 操作开销很大 2.使用 js 来描述 dom 有更强的灵活性

大体过程

  1. h 函数将参数解析,并调用 vnode 函数创建虚拟节点
  2. 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。

  1. 执行所有的 pre钩子函数
  2. 如果第一个参数不是 VNode,就会传给 emptyNodeAt 函数,其作用是将真实 DOM 转为 VNode:把自身作为 VNode 的 elm 属性,并生成 sel。
  3. 新旧节点比较,调用 sameVnode:如果 key 和 sel 都相等,则视为相同节点。
    1. 对于相同节点,调用 patchVnode函数,对比两者内容的差异
    2. 对于不同节点,获取到旧节点的父节点,并把新节点转换成真实 DOM,之后调用insertBefore将新节点的DOM移到父节点中、nextSibling(旧节点的下一节点)之前———也就是紧贴着旧节点后面,最后移除旧节点。
  4. 顺序执行insertedpost钩子函数
  5. 返回新节点

createElm 将VNode转换为真实节点

  1. 解析 sel,通过标签创建节点放入 elm,并把id和class通过 setAttribute设置上去。如果没有 sel,则把 text 创建为文本节点。
  2. 执行 create 钩子函数
  3. 遍历 children,将每一项作为参数递归调用,并通过appendChild放入 elm。如果没有 children,则把 text 创建为文本节点。
  4. 返回 elm

addVnodes/removeVnodes

addVnodes:调用createElm将 vnode 转变为真实节点,并将其添加入真实 DOM 中 removeVnodes:

patchVnode

patchVnode(
    oldVnode: VNode,
    vnode: VNode,
    insertedVnodeQueue: VNodeQueue
  )
  1. 将 oldVnode.elm 赋给 vnode.elm
  2. 如果两个节点全等,则直接返回
  3. 如果 vnode.text 不存在,代表其可能有 children 或者啥也没有,做以下操作:
    1. 如果 oldVnode.children 和 vnode.children 都存在且不相等,则调用updateChildren更新子节点
    2. 如果只有 vnode.children 存在,意味着 oldVnode 要么是一个只有文本内容的节点或没有内容,只需要调用addVnodes,把 vnode.children 添加到 elm 中。对于前者,还需要把文本内容设为空字符
    3. 如果只有 oldVnode.children 存在,意味着 vnode 是空节点,只需要调用removeVnodes,移除 elm 中所有的子节点
    4. 如果都没有 children 且 oldVnode.text 存在,代表是一个只有文本的节点,将其设为空字符串
  4. 如果 vnode.text 存在且与 oldVnode.text 不同,代表其为只有文本的节点且需要更新,做以下操作:
    1. 如果 oldVnodes.children 存在,则直接调用removeVnodes清空子节点
    2. 将文本内容设置为 vnode.text

updateChildren

  1. 分别设置新、老节点的 children 的开始、结束索引值以及元素
  2. 循环,条件为 oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,以下操作每次循环只按照判断语句执行其中一条
    1. 如果四个端点节点其一为 null,则向数组中间靠拢(已移动的vnode会被null替换)
    2. 调用sameVnode(oldStartVnode, newStartVnode),如果相等则对其调用patchVnode,并移动指针
    3. 调用sameVnode(oldEndVnode, newEndVnode),如果相等则对其调用patchVnode,并移动指针
    4. 调用sameVnode(oldEndVnode, newEndVnode),如果相等则对其调用patchVnode,并移动指针
    5. 调用sameVnode(oldStartVnode, newEndVnode),相等则调用patchVnode,并把 oldStartVnode移动到oldEndVnode之后,并移动指针
    6. 调用sameVnode(oldEndVnode, newStartVnode),相等则调用patchVnode,并把oldEndVnode移动到oldStartVnode之前,并移动指针
    7. 查找新节点的 key 是否在旧节点的 key map中出现(key map查找复杂度O(1))
      1. 未出现则代表是新节点,直接插入到 oldStartVnode之前
      2. 出现了则判断选择器是否相等,相等则代表是同一节点,调用patchVnode,并插入到oldStartVnode之前,并把老节点置空,不相等则直接插入无须比较
  3. 判断新旧节点是哪个先遍历完成,对于老节点先遍历完,把剩余的新节点插入到右边,对于新节点先遍历完,把老节点没遍历到的部分移除,不过由于移动后的老节点已经被置空了,所以通过key找到的老节点不受影响