资讯专栏INFORMATION COLUMN

snabbdom源码解析(四) patch 方法

huhud / 2196人阅读

摘要:就近复用为了尽可能不发生的移动,会就近复用相同的节点,复用的依据是判断是否是同类型的元素方法在中,主要是方法。例如元素的之类的详细了解请查看模块模块判断是否是相同的虚拟节点判断是否是相同的虚拟节点方法最后返回一个方法。

patch 方法 前言

在开始解析这块源码的时候,先给大家补一个知识点。关于 两颗 Virtual Dom 树对比的策略

diff 策略

同级对比

对比的时候,只针对同级的对比,减少算法复杂度。

就近复用
为了尽可能不发生 DOM 的移动,会就近复用相同的 DOM 节点,复用的依据是判断是否是同类型的 dom 元素

init 方法

./src/snabbdom.ts 中,主要是 init 方法。

init 方法主要是传入 modulesdomApi , 然后返回一个 patch 方法

注册钩子

</>复制代码

  1. // 钩子 ,
  2. const hooks: (keyof Module)[] = [
  3. "create",
  4. "update",
  5. "remove",
  6. "destroy",
  7. "pre",
  8. "post"
  9. ];

这里主要是注册一系列的钩子,在不同的阶段触发,细节可看 钩子

将各个模块的钩子方法,挂到统一的钩子上

这里主要是将每个 modules 下的 hook 方法提取出来存到 cbs 里面

初始化的时候,将每个 modules 下的相应的钩子都追加都一个数组里面。create、update....

在进行 patch 的各个阶段,触发对应的钩子去处理对应的事情

这种方式比较方便扩展。新增钩子的时候,不需要更改到主要的流程

</>复制代码

  1. // 循环 hooks , 将每个 modules 下的 hook 方法提取出来存到 cbs 里面
  2. // 返回结果 eg : cbs["create"] = [modules[0]["create"],modules[1]["create"],...];
  3. for (i = 0; i < hooks.length; ++i) {
  4. cbs[hooks[i]] = [];
  5. for (j = 0; j < modules.length; ++j) {
  6. const hook = modules[j][hooks[i]];
  7. if (hook !== undefined) {
  8. (cbs[hooks[i]] as Array).push(hook);
  9. }
  10. }
  11. }

</>复制代码

  1. 这些模块的钩子,主要用在更新节点的时候,会在不同的生命周期里面去触发对应的钩子,从而更新这些模块。

    例如元素的 attr、props、class 之类的!

  2. 详细了解请查看模块:模块

sameVnode

判断是否是相同的虚拟节点

</>复制代码

  1. /**
  2. * 判断是否是相同的虚拟节点
  3. */
  4. function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
  5. return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
  6. }
patch

init 方法最后返回一个 patch 方法 。

patch 方法主要的逻辑如下 :

触发 pre 钩子

如果老节点非 vnode, 则新创建空的 vnode

新旧节点为 sameVnode 的话,则调用 patchVnode 更新 vnode , 否则创建新节点

触发收集到的新元素 insert 钩子

触发 post 钩子

</>复制代码

  1. /**
  2. * 修补节点
  3. */
  4. return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
  5. let i: number, elm: Node, parent: Node;
  6. // 用于收集所有插入的元素
  7. const insertedVnodeQueue: VNodeQueue = [];
  8. // 先调用 pre 回调
  9. for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
  10. // 如果老节点非 vnode , 则创建一个空的 vnode
  11. if (!isVnode(oldVnode)) {
  12. oldVnode = emptyNodeAt(oldVnode);
  13. }
  14. // 如果是同个节点,则进行修补
  15. if (sameVnode(oldVnode, vnode)) {
  16. patchVnode(oldVnode, vnode, insertedVnodeQueue);
  17. } else {
  18. // 不同 Vnode 节点则新建
  19. elm = oldVnode.elm as Node;
  20. parent = api.parentNode(elm);
  21. createElm(vnode, insertedVnodeQueue);
  22. // 插入新节点,删除老节点
  23. if (parent !== null) {
  24. api.insertBefore(
  25. parent,
  26. vnode.elm as Node,
  27. api.nextSibling(elm)
  28. );
  29. removeVnodes(parent, [oldVnode], 0, 0);
  30. }
  31. }
  32. // 遍历所有收集到的插入节点,调用插入的钩子,
  33. for (i = 0; i < insertedVnodeQueue.length; ++i) {
  34. (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks)
  35. .insert as any)(insertedVnodeQueue[i]);
  36. }
  37. // 调用post的钩子
  38. for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
  39. return vnode;
  40. };

整体的流程大体上是这样子,接下来我们来关注更多的细节!

patchVnode 方法

首先我们研究 patchVnode 了解相同节点是如何更新的

patchVnode 方法主要的逻辑如下 :

触发 prepatch 钩子

触发 update 钩子, 这里主要为了更新对应的 module 内容

非文本节点的情况 , 调用 updateChildren 更新所有子节点

文本节点的情况 , 直接 api.setTextContent(elm, vnode.text as string);

</>复制代码

  1. 这里在对比的时候,就会直接更新元素内容了。并不会等到对比完才更新 DOM 元素

具体代码细节:

</>复制代码

  1. /**
  2. * 更新节点
  3. */
  4. function patchVnode(
  5. oldVnode: VNode,
  6. vnode: VNode,
  7. insertedVnodeQueue: VNodeQueue
  8. ) {
  9. let i: any, hook: any;
  10. // 调用 prepatch 回调
  11. if (
  12. isDef((i = vnode.data)) &&
  13. isDef((hook = i.hook)) &&
  14. isDef((i = hook.prepatch))
  15. ) {
  16. i(oldVnode, vnode);
  17. }
  18. const elm = (vnode.elm = oldVnode.elm as Node);
  19. let oldCh = oldVnode.children;
  20. let ch = vnode.children;
  21. if (oldVnode === vnode) return;
  22. // 调用 cbs 中的所有模块的update回调 更新对应的实际内容。
  23. if (vnode.data !== undefined) {
  24. for (i = 0; i < cbs.update.length; ++i)
  25. cbs.update[i](oldVnode, vnode);
  26. i = vnode.data.hook;
  27. if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
  28. }
  29. if (isUndef(vnode.text)) {
  30. if (isDef(oldCh) && isDef(ch)) {
  31. // 新老子节点都存在的情况,更新 子节点
  32. if (oldCh !== ch)
  33. updateChildren(
  34. elm,
  35. oldCh as Array,
  36. ch as Array,
  37. insertedVnodeQueue
  38. );
  39. } else if (isDef(ch)) {
  40. // 老节点不存在子节点,情况下,新建元素
  41. if (isDef(oldVnode.text)) api.setTextContent(elm, "");
  42. addVnodes(
  43. elm,
  44. null,
  45. ch as Array,
  46. 0,
  47. (ch as Array).length - 1,
  48. insertedVnodeQueue
  49. );
  50. } else if (isDef(oldCh)) {
  51. // 新节点不存在子节点,情况下,删除元素
  52. removeVnodes(
  53. elm,
  54. oldCh as Array,
  55. 0,
  56. (oldCh as Array).length - 1
  57. );
  58. } else if (isDef(oldVnode.text)) {
  59. // 如果老节点存在文本节点,而新节点不存在,所以清空
  60. api.setTextContent(elm, "");
  61. }
  62. } else if (oldVnode.text !== vnode.text) {
  63. // 子节点文本不一样的情况下,更新文本
  64. api.setTextContent(elm, vnode.text as string);
  65. }
  66. // 调用 postpatch
  67. if (isDef(hook) && isDef((i = hook.postpatch))) {
  68. i(oldVnode, vnode);
  69. }
  70. }

</>复制代码

  1. 一开始,看到这种写法总有点不习惯,不过后面看着就习惯了。

    if (isDef((i = data.hook)) && isDef((i = i.init))) {i(vnode);}

  2. 约等于

  3. if(data.hook.init){data.hook.init(vnode)}

updateChildren 方法

patchVnode 里面最重要的方法,也是整个 diff 里面的最核心方法

updateChildren 主要的逻辑如下:

优先处理特殊场景,先对比两端。也就是

旧 vnode 头 vs 新 vnode 头

旧 vnode 尾 vs 新 vnode 尾

旧 vnode 头 vs 新 vnode 尾

旧 vnode 尾 vs 新 vnode 头

首尾不一样的情况,寻找 key 相同的节点,找不到则新建元素

如果找到 key,但是,元素选择器变化了,也新建元素

如果找到 key,并且元素选择没变, 则移动元素

两个列表对比完之后,清理多余的元素,新增添加的元素

</>复制代码

  1. 不提供 key 的情况下,如果只是顺序改变的情况,例如第一个移动到末尾。这个时候,会导致其实更新了后面的所有元素

具体代码细节:

</>复制代码

  1. /**
  2. * 更新子节点
  3. */
  4. function updateChildren(
  5. parentElm: Node,
  6. oldCh: Array,
  7. newCh: Array,
  8. insertedVnodeQueue: VNodeQueue
  9. ) {
  10. let oldStartIdx = 0,
  11. newStartIdx = 0;
  12. let oldEndIdx = oldCh.length - 1;
  13. let oldStartVnode = oldCh[0];
  14. let oldEndVnode = oldCh[oldEndIdx];
  15. let newEndIdx = newCh.length - 1;
  16. let newStartVnode = newCh[0];
  17. let newEndVnode = newCh[newEndIdx];
  18. let oldKeyToIdx: any;
  19. let idxInOld: number;
  20. let elmToMove: VNode;
  21. let before: any;
  22. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  23. if (oldStartVnode == null) {
  24. // 移动索引,因为节点处理过了会置空,所以这里向右移
  25. oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
  26. } else if (oldEndVnode == null) {
  27. // 原理同上
  28. oldEndVnode = oldCh[--oldEndIdx];
  29. } else if (newStartVnode == null) {
  30. // 原理同上
  31. newStartVnode = newCh[++newStartIdx];
  32. } else if (newEndVnode == null) {
  33. // 原理同上
  34. newEndVnode = newCh[--newEndIdx];
  35. } else if (sameVnode(oldStartVnode, newStartVnode)) {
  36. // 从左对比
  37. patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
  38. oldStartVnode = oldCh[++oldStartIdx];
  39. newStartVnode = newCh[++newStartIdx];
  40. } else if (sameVnode(oldEndVnode, newEndVnode)) {
  41. // 从右对比
  42. patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
  43. oldEndVnode = oldCh[--oldEndIdx];
  44. newEndVnode = newCh[--newEndIdx];
  45. } else if (sameVnode(oldStartVnode, newEndVnode)) {
  46. // Vnode moved right
  47. // 最左侧 对比 最右侧
  48. patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
  49. // 移动元素到右侧指针的后面
  50. api.insertBefore(
  51. parentElm,
  52. oldStartVnode.elm as Node,
  53. api.nextSibling(oldEndVnode.elm as Node)
  54. );
  55. oldStartVnode = oldCh[++oldStartIdx];
  56. newEndVnode = newCh[--newEndIdx];
  57. } else if (sameVnode(oldEndVnode, newStartVnode)) {
  58. // Vnode moved left
  59. // 最右侧对比最左侧
  60. patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
  61. // 移动元素到左侧指针的后面
  62. api.insertBefore(
  63. parentElm,
  64. oldEndVnode.elm as Node,
  65. oldStartVnode.elm as Node
  66. );
  67. oldEndVnode = oldCh[--oldEndIdx];
  68. newStartVnode = newCh[++newStartIdx];
  69. } else {
  70. // 首尾都不一样的情况,寻找相同 key 的节点,所以使用的时候加上key可以调高效率
  71. if (oldKeyToIdx === undefined) {
  72. oldKeyToIdx = createKeyToOldIdx(
  73. oldCh,
  74. oldStartIdx,
  75. oldEndIdx
  76. );
  77. }
  78. idxInOld = oldKeyToIdx[newStartVnode.key as string];
  79. if (isUndef(idxInOld)) {
  80. // New element
  81. // 如果找不到 key 对应的元素,就新建元素
  82. api.insertBefore(
  83. parentElm,
  84. createElm(newStartVnode, insertedVnodeQueue),
  85. oldStartVnode.elm as Node
  86. );
  87. newStartVnode = newCh[++newStartIdx];
  88. } else {
  89. // 如果找到 key 对应的元素,就移动元素
  90. elmToMove = oldCh[idxInOld];
  91. if (elmToMove.sel !== newStartVnode.sel) {
  92. api.insertBefore(
  93. parentElm,
  94. createElm(newStartVnode, insertedVnodeQueue),
  95. oldStartVnode.elm as Node
  96. );
  97. } else {
  98. patchVnode(
  99. elmToMove,
  100. newStartVnode,
  101. insertedVnodeQueue
  102. );
  103. oldCh[idxInOld] = undefined as any;
  104. api.insertBefore(
  105. parentElm,
  106. elmToMove.elm as Node,
  107. oldStartVnode.elm as Node
  108. );
  109. }
  110. newStartVnode = newCh[++newStartIdx];
  111. }
  112. }
  113. }
  114. // 新老数组其中一个到达末尾
  115. if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
  116. if (oldStartIdx > oldEndIdx) {
  117. // 如果老数组先到达末尾,说明新数组还有更多的元素,这些元素都是新增的,说以一次性插入
  118. before =
  119. newCh[newEndIdx + 1] == null
  120. ? null
  121. : newCh[newEndIdx + 1].elm;
  122. addVnodes(
  123. parentElm,
  124. before,
  125. newCh,
  126. newStartIdx,
  127. newEndIdx,
  128. insertedVnodeQueue
  129. );
  130. } else {
  131. // 如果新数组先到达末尾,说明新数组比老数组少了一些元素,所以一次性删除
  132. removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
  133. }
  134. }
  135. }
addVnodes 方法

addVnodes 就比较简单了,主要功能就是添加 Vnodes 到 真实 DOM 中

</>复制代码

  1. /**
  2. * 添加 Vnodes 到 真实 DOM 中
  3. */
  4. function addVnodes(
  5. parentElm: Node,
  6. before: Node | null,
  7. vnodes: Array,
  8. startIdx: number,
  9. endIdx: number,
  10. insertedVnodeQueue: VNodeQueue
  11. ) {
  12. for (; startIdx <= endIdx; ++startIdx) {
  13. const ch = vnodes[startIdx];
  14. if (ch != null) {
  15. api.insertBefore(
  16. parentElm,
  17. createElm(ch, insertedVnodeQueue),
  18. before
  19. );
  20. }
  21. }
  22. }
removeVnodes 方法

删除 VNodes 的主要逻辑如下:

循环触发 destroy 钩子,递归触发子节点的钩子

触发 remove 钩子,利用 createRmCb , 在所有监听器执行后,才调用 api.removeChild,删除真正的 DOM 节点

</>复制代码

  1. /**
  2. * 创建一个删除的回调,多次调用这个回调,直到监听器都没了,就删除元素
  3. */
  4. function createRmCb(childElm: Node, listeners: number) {
  5. return function rmCb() {
  6. if (--listeners === 0) {
  7. const parent = api.parentNode(childElm);
  8. api.removeChild(parent, childElm);
  9. }
  10. };
  11. }

</>复制代码

  1. /**
  2. * 删除 VNodes
  3. */
  4. function removeVnodes(
  5. parentElm: Node,
  6. vnodes: Array,
  7. startIdx: number,
  8. endIdx: number
  9. ): void {
  10. for (; startIdx <= endIdx; ++startIdx) {
  11. let i: any,
  12. listeners: number,
  13. rm: () => void,
  14. ch = vnodes[startIdx];
  15. if (ch != null) {
  16. if (isDef(ch.sel)) {
  17. invokeDestroyHook(ch);
  18. listeners = cbs.remove.length + 1;
  19. // 所有监听删除
  20. rm = createRmCb(ch.elm as Node, listeners);
  21. for (i = 0; i < cbs.remove.length; ++i)
  22. cbs.remove[i](ch, rm);
  23. // 如果有钩子则调用钩子后再调删除回调,如果没,则直接调用回调
  24. if (
  25. isDef((i = ch.data)) &&
  26. isDef((i = i.hook)) &&
  27. isDef((i = i.remove))
  28. ) {
  29. i(ch, rm);
  30. } else {
  31. rm();
  32. }
  33. } else {
  34. // Text node
  35. api.removeChild(parentElm, ch.elm as Node);
  36. }
  37. }
  38. }
  39. }
createElm 方法

将 vnode 转换成真正的 DOM 元素

主要逻辑如下:

触发 init 钩子

处理注释节点

创建元素并设置 id , class

触发模块 create 钩子 。

处理子节点

处理文本节点

触发 vnodeData 的 create 钩子

</>复制代码

  1. /**
  2. * VNode ==> 真实DOM
  3. */
  4. function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
  5. let i: any,
  6. data = vnode.data;
  7. if (data !== undefined) {
  8. // 如果存在 data.hook.init ,则调用该钩子
  9. if (isDef((i = data.hook)) && isDef((i = i.init))) {
  10. i(vnode);
  11. data = vnode.data;
  12. }
  13. }
  14. let children = vnode.children,
  15. sel = vnode.sel;
  16. // ! 来代表注释
  17. if (sel === "!") {
  18. if (isUndef(vnode.text)) {
  19. vnode.text = "";
  20. }
  21. vnode.elm = api.createComment(vnode.text as string);
  22. } else if (sel !== undefined) {
  23. // Parse selector
  24. // 解析选择器
  25. const hashIdx = sel.indexOf("#");
  26. const dotIdx = sel.indexOf(".", hashIdx);
  27. const hash = hashIdx > 0 ? hashIdx : sel.length;
  28. const dot = dotIdx > 0 ? dotIdx : sel.length;
  29. const tag =
  30. hashIdx !== -1 || dotIdx !== -1
  31. ? sel.slice(0, Math.min(hash, dot))
  32. : sel;
  33. // 根据 tag 创建元素
  34. const elm = (vnode.elm =
  35. isDef(data) && isDef((i = (data as VNodeData).ns))
  36. ? api.createElementNS(i, tag)
  37. : api.createElement(tag));
  38. // 设置 id
  39. if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
  40. // 设置 className
  41. if (dotIdx > 0)
  42. elm.setAttribute("class",sel.slice(dot + 1).replace(/./g, " "));
  43. // 执行所有模块的 create 钩子,创建对应的内容
  44. for (i = 0; i < cbs.create.length; ++i)
  45. cbs.create[i](emptyNode, vnode);
  46. // 如果存在 children ,则创建children
  47. if (is.array(children)) {
  48. for (i = 0; i < children.length; ++i) {
  49. const ch = children[i];
  50. if (ch != null) {
  51. api.appendChild(
  52. elm,
  53. createElm(ch as VNode, insertedVnodeQueue)
  54. );
  55. }
  56. }
  57. } else if (is.primitive(vnode.text)) {
  58. // 追加文本节点
  59. api.appendChild(elm, api.createTextNode(vnode.text));
  60. }
  61. // 执行 vnode.data.hook 中的 create 钩子
  62. i = (vnode.data as VNodeData).hook; // Reuse variable
  63. if (isDef(i)) {
  64. if (i.create) i.create(emptyNode, vnode);
  65. if (i.insert) insertedVnodeQueue.push(vnode);
  66. }
  67. } else {
  68. // sel 不存在的情况, 即为文本节点
  69. vnode.elm = api.createTextNode(vnode.text as string);
  70. }
  71. return vnode.elm;
  72. }
其他

想了解在各个生命周期都有哪些钩子,请查看:钩子

想了解在各个生命周期里面如何更新具体的模块请查看:模块

snabbdom源码解析系列

snabbdom源码解析(一) 准备工作

snabbdom源码解析(二) h函数

snabbdom源码解析(三) vnode对象

snabbdom源码解析(四) patch 方法

snabbdom源码解析(五) 钩子

snabbdom源码解析(六) 模块

snabbdom源码解析(七) 事件处理

个人博客地址

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108829.html

相关文章

  • snabbdom源码解析(一) 准备工作

    摘要:阅读源码的时候,想了解虚拟结构的实现,发现在的地方。然而慢慢的人们发现,在我们的代码中布满了一系列操作的代码。源码解析系列源码解析一准备工作源码解析二函数源码解析三对象源码解析四方法源码解析五钩子源码解析六模块源码解析七事件处理个人博客地址 前言 虚拟 DOM 结构概念随着 react 的诞生而火起来,之后 vue2.0 也加入了虚拟 DOM 的概念。 阅读 vue 源码的时候,想了解...

    defcon 评论0 收藏0
  • snabbdom源码解析(五) 钩子

    摘要:元素从父节点删除时触发,和略有不同,只影响到被移除节点中最顶层的节点在方法的最后调用,也就是完成后触发源码解析系列源码解析一准备工作源码解析二函数源码解析三对象源码解析四方法源码解析五钩子源码解析六模块源码解析七事件处理个人博客地址 文件路径 : ./src/hooks.ts 这个文件主要是定义了 Virtual Dom 在实现过程中,在其执行过程中的一系列钩子。方便外部做一些处理 /...

    Worktile 评论0 收藏0
  • snabbdom源码解析(二) h函数

    介绍 这里是 typescript 的语法,定义了一系列的重载方法。h 函数主要根据传进来的参数,返回一个 vnode 对象 代码 代码位置 : ./src/h.ts /** * 根据选择器 ,数据 ,创建 vnode */ export function h(sel: string): VNode; export function h(sel: string, data: VNodeData...

    Jensen 评论0 收藏0
  • snabbdom源码解析(七) 事件处理

    摘要:这种解决方式也是相当优雅,值得学习源码解析系列源码解析一准备工作源码解析二函数源码解析三对象源码解析四方法源码解析五钩子源码解析六模块源码解析七事件处理个人博客地址 事件处理 我们在使用 vue 的时候,相信你一定也会对事件的处理比较感兴趣。 我们通过 @click 的时候,到底是发生了什么呢! 虽然我们用 @click绑定在模板上,不过事件严格绑定在 vnode 上的 。 event...

    Kross 评论0 收藏0
  • snabbdom源码解析(三) vnode对象

    摘要:对象是一个对象,用来表示相应的结构代码位置定义类型定义类型选择器数据,主要包括属性样式数据绑定时间等子节点关联的原生节点文本唯一值,为了优化性能定义的类型定义绑定的数据类型属性能直接用访问的属性样式类样式数据绑定的事件钩子创建对象根据传入的 vnode 对象 vnode 是一个对象,用来表示相应的 dom 结构 代码位置 :./src/vnode.ts 定义 vnode 类型 /** ...

    willin 评论0 收藏0

发表评论

0条评论

huhud

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<