前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >3. 「snabbdom@3.5.1 源码分析」patch(如何打补丁?)

3. 「snabbdom@3.5.1 源码分析」patch(如何打补丁?)

作者头像
tinyant
发布2023-02-24 10:05:26
1.7K0
发布2023-02-24 10:05:26
举报

init:创建patch函数

看到会返回一个patch函数。看到init内部有很多函数,这些函数大都都是用到api进行DOM操作,而api依赖入参domApi(如果放在外侧,domApi需要作为参数传递)。 这里实际上通过闭包私有化这些函数作为方法存在。

看到在init方法入口处从入参modules中收集了指定的钩子回调。这样清楚了module的构造和用意。modules是一个对象,键就是init中cbs的key有:create\update\remove\destroy\pre\post,而值是函数。这样就可以参与从虚拟DOM到真实DOM的过程。

代码语言:javascript
复制
export function init(modules, domApi, options) {
    const cbs = {
        create: [],
        update: [],
        remove: [],
        destroy: [],
        pre: [],
        post: [],
    };
    
    const api = domApi !== ?developer/article/2223507/undefined ? domApi : htmlDomApi;
    for (const hook of hooks) {
        for (const module of modules) {
            const currentHook = module[hook];
            if (currentHook !== ?developer/article/2223507/undefined) {
                cbs[hook].push(currentHook);
            }
        }
    }
    
    function emptyNodeAt(elm) {
        //...
    }
    
    function emptyDocumentFragmentAt(frag) {
        //...
    }
    
    function createRmCb(childElm, listeners) {
        return function rmCb() {
            //...
        };
    }
    
    function createElm(vnode, insertedVnodeQueue) {
        //...
    }
    
    function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
        //...
    }
    
    function invokeDestroyHook(vnode) {
        //...
    }
    
    function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
        //...
    }
    
    function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
        //...
    }
    
    function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
        //...
    }
    
    return function patch(oldVnode, vnode) {
        //...
        return vnode;
    };
}

下面看下patch函数的实现

patch(打补丁):对比虚拟DOM树(根节点)

触发pre钩子,相当于会执行module提供的pre指向的函数

如果是真实的DOM节点,并根据该DOM创建对应的虚拟节点,【注意】此时会忽略其所有的孩子节点。

代码语言:javascript
复制
 function emptyNodeAt(elm: Element) {
    const id = elm.id ? "#" + elm.id : "";
    const classes = elm.getAttribute("class");
    const c = classes ? "." + classes.split(" ").join(".") : "";
    return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], ?developer/article/2223507/undefined, elm );
  }

如果判断为相同节点(sameVnode)则调用patchVnode进行对比 sameVnode:注意如果没有定义属性即为?developer/article/2223507/undefined?developer/article/2223507/undefined === ?developer/article/2223507/undefined 是真值

代码语言:javascript
复制
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
  const isSameKey = vnode1.key === vnode2.key;
  const isSameIs = vnode1.data?.is === vnode2.data?.is;
  const isSameSel = vnode1.sel === vnode2.sel;
  const isSameTextOrFragment =
    !vnode1.sel && vnode1.sel === vnode2.sel
      ? typeof vnode1.text === typeof vnode2.text
      : true;

  return isSameSel && isSameKey && isSameIs && isSameTextOrFragment;
}

patchVnode比较重要,后面单独说下。

如果判断不是相同的vnode,则根据新的vnode直接创建真实DOM取代老的vnode。

  1. createElm:根据新vnode创建新DOM,创建vnode时如果传递了data.hook.insert,会将该vnode保存到insertedVnodeQueue。另外:这个过程是递归的(如果有children)
  2. 插入新DOM 并且 删除旧DOM

钩子,有两类

  1. init 初始时收集到cbs的钩子(收集自init入参传递的modules),执行post
  2. 遍历insertedVnodeQueue,并执行vnode.data.hook.insert,显然这个钩子是当前vnode关联的。
代码语言:javascript
复制
export function init(modules, domApi, options) {
    // ...
    return function patch(oldVnode, vnode) {
        let i, elm, parent;
        const insertedVnodeQueue = [];
        for (i = 0; i < cbs.pre.length; ++i)
            cbs.pre[i]();
        if (isElement(api, oldVnode)) {
            oldVnode = emptyNodeAt(oldVnode);
        }
        else if (isDocumentFragment(api, oldVnode)) {
           //... 实验特性,暂时不看
        }
        if (sameVnode(oldVnode, vnode)) {
            patchVnode(oldVnode, vnode, insertedVnodeQueue);
        }
        else {
            elm = oldVnode.elm;
            parent = api.parentNode(elm);
            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;
    };
}

上面说过到如果新旧虚拟DOM被判断为相同节点,则会对这两个vnode进行对比,找出差异,并同步到界面上。 见patchVnode函数

patchVnode:对比单个虚拟DOM

vnode.data.hook.prepatch

因为被判断是相同的vnode,会复用oldVnode关联的DOM,因此(vnode.elm = oldVnode.elm)

如果vnode和oldVnode是相同的对象,则返回

校正vnode/oldVnode.data,并执行 [cbs].updatevnode.data.hook.update

代码语言:javascript
复制
// 怎么感觉应该是不需要第一个判断呢???
if (vnode.data !== ?developer/article/2223507/undefined || (isDef(vnode.text) &amp;&amp; vnode.text !== oldVnode.text)) {
  vnode.data ??= {};
  oldVnode.data ??= {};
  for (let i = 0; i < cbs.update.length; ++i)
    cbs.update[i](oldVnode, vnode);
  vnode.data?.hook?.update?.(oldVnode, vnode);
}

根据vnode.text是否存在(这一步是孩子的处理)

  • 不存在,又细分多种情况(else-if,下面case是进一步):
    • 新老vnode都存在孩子,调用updateChildren对孩子进行对比;
    • 只有新vnode有孩子:如果老vnode.text存在,则设置老vnode.text=''(实际上这里看出text和children的区别,虽然text也是孩子,但是拎出来单独处理了,是因为创建文本节点是特殊的api);调用addVnodes创建新元素并挂载到界面上
    • 如果只有老vnode有孩子,则调用removeVnodes删除孩子
    • vnode.text存在,设置为空串
  • 存在:调用removeVnodes删除孩子,调用setTextContent设置文本(新的孩子)

调用vnode.data.hook.postpatch

代码语言:javascript
复制
  function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    const hook = vnode.data?.hook;
    hook?.prepatch?.(oldVnode, vnode);
    const elm = (vnode.elm = oldVnode.elm)!;
    if (oldVnode === vnode) return;
    if (vnode.data !== ?developer/article/2223507/undefined || (isDef(vnode.text) &amp;&amp; vnode.text !== oldVnode.text)) {
      vnode.data ??= {};
      oldVnode.data ??= {};
      for (let i = 0; i < cbs.update.length; ++i)
        cbs.update[i](oldVnode, vnode);
      vnode.data?.hook?.update?.(oldVnode, vnode);
    }
    const oldCh = oldVnode.children as VNode[];
    const ch = vnode.children as VNode[];
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) &amp;&amp; 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);
      } 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!);
    }
    hook?.postpatch?.(oldVnode, vnode);
  }

上面过程用到了removeVnodesaddVnodes

addVnodes

创建DOM,通过insertBefore衔接父子节点(如updateChildren过程中)或挂载到界面。

代码语言:javascript
复制
function addVnodes(parentElm: Node, before: Node | null, vnodes: VNode[], startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx];
      if (ch != null) {
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
      }
    }
  }

removeVnodes

涉及destroy,remove钩子(cbs即模块上的、vnode.data.hook)

代码语言:javascript
复制
function removeVnodes(parentElm: Node, vnodes: VNode[], startIdx: number, endIdx: number): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let listeners: number;
      let rm: () => void;
      const ch = vnodes[startIdx];
      if (ch != null) {
        if (isDef(ch.sel)) {
          invokeDestroyHook(ch);
          listeners = cbs.remove.length + 1;
          rm = createRmCb(ch.elm!, listeners);
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
          const removeHook = ch?.data?.hook?.remove;
          if (isDef(removeHook)) {
            removeHook(ch, rm);
          } else {
            rm();
          }
        } else if (ch.children) {
          // Fragment node
          invokeDestroyHook(ch);
          removeVnodes(parentElm, ch.children as VNode[], 0, ch.children.length - 1);
        } else {
          // Text node
          api.removeChild(parentElm, ch.elm!);
        }
      }
    }
  }

invokeDestroyHook

代码语言:javascript
复制
function invokeDestroyHook(vnode: VNode) {
  const data = vnode.data;
  if (data !== ?developer/article/2223507/undefined) {
    data?.hook?.destroy?.(vnode);
    for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
    if (vnode.children !== ?developer/article/2223507/undefined) {
      for (let j = 0; j < vnode.children.length; ++j) {
        const child = vnode.children[j];
        if (child != null &amp;&amp; typeof child !== "string") {
          invokeDestroyHook(child);
        }
      }
    }
  }
}

updateChildren: 对比子虚拟节点(diff的本质)

diff的本质是什么

patch函数的目的是打补丁,找出差异,然后将差异同步到界面上。另外就是应该尽可能的复用已有的DOM。updateChildren的核心目的就是为了做这件事情:在考虑时间复杂度情况下去复用已有的DOM。

不考虑时间复杂度的情况下,给你新老两个vnode数组(oldCh: m 个元素,newCh: n 个元素)和samveVnode函数,你会如何实现呢?

我能想到的实现是两层遍历,外层是遍历 newCh,内层遍历oldCh,从oldCh中查找可以服用的节点。大致实现如下:

代码语言:javascript
复制
function patchEssential(oldCh,newCh) {
    let oldStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]

    // 外层遍历 newCh
    while (newStartVnode <= newEndIdx &amp;&amp; oldStartIdx <= oldEndIdx) {
      const newVnode = newCh[newStartVnode]
      
      // 内层遍历 oldCh
      let canReused = false; // 用来标识当前newVnode是否找打了可以被复用的oldVnode
      for (; oldStartIdx <= oldEndIdx; oldStartIdx++) {
        const oldVnode = oldCh[oldStartIdx]
        // 如果被复用过
        if (!oldVnode) {
          continue
        }

        // 找到了可以被复用的oldVnode
        if (sameVnode(newVnode, oldVnode)) {
          canReused = true; 
          patchVnode(newVnode, oldVnode) // 递归对比
          // 直接移动位置(避免了创建DOM的过程)
          nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) // 调整元素位置
          // 设置为?developer/article/2223507/undefined,表示被复用过
          oldCh[oldStartIdx] = ?developer/article/2223507/undefined; 
          break;
        }
      }
      // 如果没有找到可以复用的节点,则创建新的并挂载 
      !canReused &amp;&amp; createElm(newVnode, ...)
    }

   //... 删除oldVnode未被复用的,添加newVnode尚未遍历到的
  }

这里的流程不细说了,都在注释里了。另外这里的时间复杂度是O(m * n),在页面渲染这种高优的事情中,这个复杂度不能被接受。

所以snabbdom的实现为了在时间复杂度和复用率上取了平衡。在没有提供key的情况下,snabbdom的双端对比做不到完全复用,key场景下当然是可以的。实际上这种取舍是合理的,在少量DOM场景下可能不会涉及到性能问题,如果有性能问题,添加key就可以解决。也就是说给你偷懒的机会,但是偷懒造成了严重后果,也提供解决方案让你弥补。

snabbdom: updateChildren的实现

变量声明,没什么好说的

while循环,显然只有满足oldStartIdx <= oldEndIdx &amp;&amp; newStartIdx <= newEndIdx才可能查找复用的节点

如果遍历的节点是空的,跳过看下一个节点(前面四个if);被复用的节点(oldVnode)可能会被置为?developer/article/2223507/undefinednull == ?developer/article/2223507/undefined 为true(vue-2.6.11 中只考虑了该场景)

代码语言:javascript
复制
// vue-2.6.11 updateChildren 片段
if (isUndef(oldStartVnode)) {
  oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
  oldEndVnode = oldCh[--oldEndIdx];
}

双端对比,首尾相互比对

降级的关键:本来应该是要从所有的oldVnode中查找可以复用的节点,但是现在只考虑首尾两处,时间复杂度从O(n)降至o(1)。

实际上你可以随机挑常数个位置的节点进行对比,我理解首尾对比只是一种简单的随机策略,除非框架作者调研的结论是首尾对比能够较大概率的满足节点复用的场景。

代码语言:javascript
复制
sameVnode(oldEndVnode, newEndVnode)
sameVnode(oldEndVnode, newStartVnode)
sameVnode(oldStartVnode, newEndVnode)
sameVnode(oldStartVnode, newStartVnode)

最后一个else,是处理key场景的,用来提高性能的关键之处,逻辑显然,不赘述。

while循环退出后的两种情况

  1. 老节点都已经遍历完了,那未遍历到的新节点,当然要创建并挂载(addVnodes
  2. 新节点都已经遍历完了,那剩下的未被遍历的节点,当然要删除(removeVnodes注意: (index < oldStartIdx || index > oldEndIdx)的节点一定是被复用了的,只有[oldStartIdx, oldEndIdx]会存在未被复用的节点。因为只有被复用oldStartIdx和oldEndIdx才会向中间收缩,[oldStartIdx, oldEndIdx]中间的被复用的节点由于被重置为?developer/article/2223507/undefined,因此遍历该区间进行删除时原有的老节点不会被删除(因为没拿到)
代码语言:javascript
复制
  function updateChildren(parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue) {
    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 | ?developer/article/2223507/undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;

    while (oldStartIdx <= oldEndIdx &amp;&amp; newStartIdx <= newEndIdx) {
      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];
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // Vnode moved right
        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(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
      } else {
        if (oldKeyToIdx === ?developer/article/2223507/undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
        if (isUndef(idxInOld)) {
          // New element
          api.insertBefore(
            parentElm,
            createElm(newStartVnode, insertedVnodeQueue),
            oldStartVnode.elm!
          );
        } else {
          elmToMove = oldCh[idxInOld];
          if (elmToMove.sel !== newStartVnode.sel) {
            api.insertBefore(
              parentElm,
              createElm(newStartVnode, insertedVnodeQueue),
              oldStartVnode.elm!
            );
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = ?developer/article/2223507/undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
        }
        newStartVnode = newCh[++newStartIdx];
      }
    }

    if (newStartIdx <= newEndIdx) {
      before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
      addVnodes(
        parentElm,
        before,
        newCh,
        newStartIdx,
        newEndIdx,
        insertedVnodeQueue
      );
    }
    if (oldStartIdx <= oldEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
  }

老节点的宿命要么复用(原地保持,移动(左移、右移))要么删除。新节点都会被考虑到,要么复用老节点,要么新创建。

另外的细节之处是要注意:移动和插入的位置需要兄弟节点和父节点辅助

createElm:根据虚拟DOM创建真实DOM

执行钩子:vnode.data.hook.init (可能会修改data,因此调用完后重新赋值了)

判断sel==='!'时,则会创建一个注释节点,保存到vnode.elm中。注意: 卸载oldVnode,框架本身没有提供专门的api来删除,依然是基于patch能力来模拟(unmounting)

代码语言:javascript
复制
// patch -> createElm,在createElm中 ,而后新增该注释节点删除老的节点,实现老节点的删除。
patch(oldVnode, h("!", {}));

sel不为?developer/article/2223507/undefined

从sel中解析出tag如div

createElement创建DOM

设置idclass。注意: createElement的可以传递一个对象选项

执行钩子:cbs.create(创建完DOM)

如果有孩子,则递归创建孩子DOM,并append当前DOM下(父子关系嘛):

代码语言:javascript
复制
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));

vnode.text也算是孩子,如果有,createTextNode + appendChild

调用 vnode.data.hook.create

如果vnode.data.hook.insert存在则保存新创建的vnode到insertedVnodeQueue。后面挂载到界面上会触发。(createElm只是创建,并未挂载到界面

返回新创建的 vnode.elm

代码语言:javascript
复制
  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any;
    let data = vnode.data;
    if (data !== ?developer/article/2223507/undefined) {
      const init = data.hook?.init;
      if (isDef(init)) {
        init(vnode);
        data = vnode.data;
      }
    }
    const children = vnode.children;
    const sel = vnode.sel;
    if (sel === "!") {
      if (isUndef(vnode.text)) {
        vnode.text = "";
      }
      vnode.elm = api.createComment(vnode.text!);
    } else if (sel !== ?developer/article/2223507/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;
      const elm = (vnode.elm =
        isDef(data) &amp;&amp; isDef((i = data.ns))
          ? api.createElementNS(i, tag, data)
          : api.createElement(tag, data));
      if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
      if (dotIdx > 0)
        elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
      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));
          }
        }
      } else if (is.primitive(vnode.text)) {
        api.appendChild(elm, api.createTextNode(vnode.text));
      }
      const hook = vnode.data!.hook;
      if (isDef(hook)) {
        hook.create?.(emptyNode, vnode);
        if (hook.insert) {
          insertedVnodeQueue.push(vnode);
        }
      }
    } else if (options?.experimental?.fragments &amp;&amp; vnode.children) {
      //... 实验特性 暂不考虑
    } else {
      vnode.elm = api.createTextNode(vnode.text!);
    }
    return vnode.elm;
  }

总结

patch:对比整棵树(从根节点开始,也说明根节点只能有一颗) -> patchVnode:对比单个vnode -> updateChildren:对比孩子

打补丁的前提是:不考虑节点的跨层移动,只对比同层节点。

hook有哪些

Snabbdom 提供了一系列丰富的生命周期函数,这些生命周期函数适用于拓展 Snabbdom 模块或者在虚拟节点生命周期中执行任意代码。

名称

触发节点

回调参数

pre

patch 开始执行

none

init

vnode 被添加

vnode

create

一个基于 vnode 的 DOM 元素被创建

emptyVnode, vnode

insert

元素 被插入到 DOM

vnode

prepatch

元素 即将 patch

oldVnode, vnode

update

元素 已更新

oldVnode, vnode

postpatch

元素 已被 patch

oldVnode, vnode

destroy

元素 被直接或间接得移除

vnode

remove

元素 已从 DOM 中移除

vnode, removeCallback

post

已完成 patch 过程

none

适用于模块(module.xxx):pre, create,update, destroy, remove, post

适用于单个元素(vnode.data.hook.xxx):init, create, insert, prepatch, update,postpatch, destroy, remove

虽然很多钩子的触发时机是一致,但是为什么还要区分这两类钩子呢?因为有些逻辑是共同的,这些逻辑收敛到模块中,而有些逻辑对于不同的vnode有差异,因此交个具体的vnode自己处理。

可以根据钩子的执行位置,回忆从data -> vnode -> dom -> 界面的过程。

本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-01-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • init:创建patch函数
  • patch(打补丁):对比虚拟DOM树(根节点)
    • patchVnode:对比单个虚拟DOM
      • addVnodes
        • removeVnodes
        • updateChildren: 对比子虚拟节点(diff的本质)
          • diff的本质是什么
            • snabbdom: updateChildren的实现
            • createElm:根据虚拟DOM创建真实DOM
            • 总结
              • hook有哪些
              相关产品与服务
              云硬盘
              云硬盘(Cloud Block Storage,CBS)为您提供用于 CVM 的持久性数据块级存储服务。云硬盘中的数据自动地在可用区内以多副本冗余方式存储,避免数据的单点故障风险,提供高达99.9999999%的数据可靠性。同时提供多种类型及规格,满足稳定低延迟的存储性能要求。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
              http://www.vxiaotou.com