snabbdom源码剖析(四) patch 要领

patch 要领

媒介

在最先剖析这块源码的时刻,先给人人补一个知识点。关于 两颗 Virtual Dom 树对照的战略

diff 战略

  1. 同级对照
    《snabbdom源码剖析(四) patch 要领》
    对照的时刻,只针对同级的对照,削减算法复杂度。
  2. 就近复用
    为了尽量不发生 DOM 的挪动,会就近复用雷同的 DOM 节点,复用的依据是推断是不是是同范例的 dom 元素

init 要领

./src/snabbdom.ts 中,主假如 init 要领。

init 要领主假如传入 modulesdomApi , 然后返回一个 patch 要领

注册钩子

// 钩子 ,
const hooks: (keyof Module)[] = [
    'create',
    'update',
    'remove',
    'destroy',
    'pre',
    'post'
];

这里主假如注册一系列的钩子,在差别的阶段触发,细节可看 钩子

将各个模块的钩子要领,挂到一致的钩子上

这里主假如将每一个 modules 下的 hook 要领提取出来存到 cbs 内里

  • 初始化的时刻,将每一个 modules 下的响应的钩子都追加都一个数组内里。create、update….
  • 在举行 patch 的各个阶段,触发对应的钩子去处置惩罚对应的事变
  • 这类体式格局比较轻易扩大。新增钩子的时刻,不需要更改到主要的流程
    // 轮回 hooks , 将每一个 modules 下的 hook 要领提取出来存到 cbs 内里
    // 返回效果 eg : cbs['create'] = [modules[0]['create'],modules[1]['create'],...];
    for (i = 0; i < hooks.length; ++i) {
        cbs[hooks[i]] = [];
        for (j = 0; j < modules.length; ++j) {
            const hook = modules[j][hooks[i]];
            if (hook !== undefined) {
                (cbs[hooks[i]] as Array<any>).push(hook);
            }
        }
    }

这些模块的钩子,主要用在更新节点的时刻,会在差别的生命周期内里去触发对应的钩子,从而更新这些模块。

比方元素的 attr、props、class 之类的!

细致相识请检察模块:模块

sameVnode

推断是不是是雷同的假造节点

/**
 *  推断是不是是雷同的假造节点
 */
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
    return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}

patch

init 要领末了返回一个 patch 要领 。

patch 要领主要的逻辑以下 :

  • 触发 pre 钩子
  • 假如老节点非 vnode, 则新建立空的 vnode
  • 新旧节点为 sameVnode 的话,则挪用 patchVnode 更新 vnode , 不然建立新节点
  • 触发网络到的新元素 insert 钩子
  • 触发 post 钩子
    /**
     * 修补节点
     */
    return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
        let i: number, elm: Node, parent: Node;

        // 用于网络一切插进去的元素
        const insertedVnodeQueue: VNodeQueue = [];

        // 先挪用 pre 回调
        for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

        // 假如老节点非 vnode , 则建立一个空的 vnode
        if (!isVnode(oldVnode)) {
            oldVnode = emptyNodeAt(oldVnode);
        }

        // 假如是同个节点,则举行修补
        if (sameVnode(oldVnode, vnode)) {
            patchVnode(oldVnode, vnode, insertedVnodeQueue);
        } else {
            // 差别 Vnode 节点则新建
            elm = oldVnode.elm as Node;
            parent = api.parentNode(elm);

            createElm(vnode, insertedVnodeQueue);

            // 插进去新节点,删除老节点
            if (parent !== null) {
                api.insertBefore(
                    parent,
                    vnode.elm as Node,
                    api.nextSibling(elm)
                );
                removeVnodes(parent, [oldVnode], 0, 0);
            }
        }

        // 遍历一切网络到的插进去节点,挪用插进去的钩子,
        for (i = 0; i < insertedVnodeQueue.length; ++i) {
            (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks)
                .insert as any)(insertedVnodeQueue[i]);
        }
        // 挪用post的钩子
        for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();

        return vnode;
    };

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

patchVnode 要领

起首我们研讨 patchVnode 相识雷同节点是怎样更新的

patchVnode 要领主要的逻辑以下 :

  • 触发 prepatch 钩子
  • 触发 update 钩子, 这里主要为了更新对应的 module 内容
  • 非文本节点的状况 , 挪用 updateChildren 更新一切子节点
  • 文本节点的状况 , 直接 api.setTextContent(elm, vnode.text as string);

这里在对照的时刻,就会直接更新元素内容了。并不会比及对照完才更新 DOM 元素

详细代码细节:

    /**
     * 更新节点
     */
    function patchVnode(
        oldVnode: VNode,
        vnode: VNode,
        insertedVnodeQueue: VNodeQueue
    ) {
        let i: any, hook: any;
        // 挪用 prepatch 回调
        if (
            isDef((i = vnode.data)) &&
            isDef((hook = i.hook)) &&
            isDef((i = hook.prepatch))
        ) {
            i(oldVnode, vnode);
        }

        const elm = (vnode.elm = oldVnode.elm as Node);
        let oldCh = oldVnode.children;
        let ch = vnode.children;
        if (oldVnode === vnode) return;

        // 挪用 cbs 中的一切模块的update回调 更新对应的现实内容。
        if (vnode.data !== undefined) {
            for (i = 0; i < cbs.update.length; ++i)
                cbs.update[i](oldVnode, vnode);

            i = vnode.data.hook;
            if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
        }

        if (isUndef(vnode.text)) {
            if (isDef(oldCh) && isDef(ch)) {
                // 新老子节点都存在的状况,更新 子节点
                if (oldCh !== ch)
                    updateChildren(
                        elm,
                        oldCh as Array<VNode>,
                        ch as Array<VNode>,
                        insertedVnodeQueue
                    );
            } else if (isDef(ch)) {
                // 老节点不存在子节点,状况下,新建元素
                if (isDef(oldVnode.text)) api.setTextContent(elm, '');
                addVnodes(
                    elm,
                    null,
                    ch as Array<VNode>,
                    0,
                    (ch as Array<VNode>).length - 1,
                    insertedVnodeQueue
                );
            } else if (isDef(oldCh)) {
                // 新节点不存在子节点,状况下,删除元素
                removeVnodes(
                    elm,
                    oldCh as Array<VNode>,
                    0,
                    (oldCh as Array<VNode>).length - 1
                );
            } else if (isDef(oldVnode.text)) {
                // 假如老节点存在文本节点,而新节点不存在,所以清空
                api.setTextContent(elm, '');
            }
        } else if (oldVnode.text !== vnode.text) {
            // 子节点文本不一样的状况下,更新文本
            api.setTextContent(elm, vnode.text as string);
        }

        // 挪用 postpatch
        if (isDef(hook) && isDef((i = hook.postpatch))) {
            i(oldVnode, vnode);
        }
    }

一最先,看到这类写法总有点不习惯,不过背面看着就习惯了。

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

约等于

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

updateChildren 要领

patchVnode 内里最主要的要领,也是全部 diff 内里的最中心要领

updateChildren 主要的逻辑以下:

  1. 优先处置惩罚特别场景,先对照两头。也就是

    • 旧 vnode 头 vs 新 vnode 头
    • 旧 vnode 尾 vs 新 vnode 尾
    • 旧 vnode 头 vs 新 vnode 尾
    • 旧 vnode 尾 vs 新 vnode 头
  2. 首尾不一样的状况,寻觅 key 雷同的节点,找不到则新建元素
  3. 假如找到 key,然则,元素挑选器变化了,也新建元素
  4. 假如找到 key,而且元素挑选没变, 则挪动元素
  5. 两个列表对照完以后,清算过剩的元素,新增增加的元素

不供应 key 的状况下,假如只是递次转变的状况,比方第一个挪动到末端。这个时刻,会致使实在更新了背面的一切元素

详细代码细节:

    /**
     * 更新子节点
     */
    function updateChildren(
        parentElm: Node,
        oldCh: Array<VNode>,
        newCh: Array<VNode>,
        insertedVnodeQueue: VNodeQueue
    ) {
        let oldStartIdx = 0,
            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: any;
        let idxInOld: number;
        let elmToMove: VNode;
        let before: any;

        while (oldStartIdx <= oldEndIdx && 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 as Node,
                    api.nextSibling(oldEndVnode.elm as Node)
                );
                oldStartVnode = oldCh[++oldStartIdx];
                newEndVnode = newCh[--newEndIdx];
            } else if (sameVnode(oldEndVnode, newStartVnode)) {
                // Vnode moved left
                // 最右边对照最左边
                patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
                // 挪动元素到左边指针的背面
                api.insertBefore(
                    parentElm,
                    oldEndVnode.elm as Node,
                    oldStartVnode.elm as Node
                );
                oldEndVnode = oldCh[--oldEndIdx];
                newStartVnode = newCh[++newStartIdx];
            } else {
                // 首尾都不一样的状况,寻觅雷同 key 的节点,所以运用的时刻加上key能够调高效力
                if (oldKeyToIdx === undefined) {
                    oldKeyToIdx = createKeyToOldIdx(
                        oldCh,
                        oldStartIdx,
                        oldEndIdx
                    );
                }
                idxInOld = oldKeyToIdx[newStartVnode.key as string];

                if (isUndef(idxInOld)) {
                    // New element
                    // 假如找不到 key 对应的元素,就新建元素
                    api.insertBefore(
                        parentElm,
                        createElm(newStartVnode, insertedVnodeQueue),
                        oldStartVnode.elm as Node
                    );
                    newStartVnode = newCh[++newStartIdx];
                } else {
                    // 假如找到 key 对应的元素,就挪动元素
                    elmToMove = oldCh[idxInOld];
                    if (elmToMove.sel !== newStartVnode.sel) {
                        api.insertBefore(
                            parentElm,
                            createElm(newStartVnode, insertedVnodeQueue),
                            oldStartVnode.elm as Node
                        );
                    } else {
                        patchVnode(
                            elmToMove,
                            newStartVnode,
                            insertedVnodeQueue
                        );
                        oldCh[idxInOld] = undefined as any;
                        api.insertBefore(
                            parentElm,
                            elmToMove.elm as Node,
                            oldStartVnode.elm as Node
                        );
                    }
                    newStartVnode = newCh[++newStartIdx];
                }
            }
        }
        // 新老数组个中一个抵达末端
        if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
            if (oldStartIdx > oldEndIdx) {
                // 假如老数组先抵达末端,申明新数组另有更多的元素,这些元素都是新增的,说以一次性插进去
                before =
                    newCh[newEndIdx + 1] == null
                        ? null
                        : newCh[newEndIdx + 1].elm;
                addVnodes(
                    parentElm,
                    before,
                    newCh,
                    newStartIdx,
                    newEndIdx,
                    insertedVnodeQueue
                );
            } else {
                // 假如新数组先抵达末端,申明新数组比老数组少了一些元素,所以一次性删除
                removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
            }
        }
    }

addVnodes 要领

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

/**
 * 增加 Vnodes 到 实在 DOM 中
 */
function addVnodes(
    parentElm: Node,
    before: Node | null,
    vnodes: Array<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 要领

删除 VNodes 的主要逻辑以下:

  • 轮回触发 destroy 钩子,递归触发子节点的钩子
  • 触发 remove 钩子,应用 createRmCb , 在一切监听器实行后,才挪用 api.removeChild,删除真正的 DOM 节点
/**
 * 建立一个删除的回调,屡次挪用这个回调,直到监听器都没了,就删除元素
 */
function createRmCb(childElm: Node, listeners: number) {
    return function rmCb() {
        if (--listeners === 0) {
            const parent = api.parentNode(childElm);
            api.removeChild(parent, childElm);
        }
    };
}
/**
 * 删除 VNodes
 */
function removeVnodes(
    parentElm: Node,
    vnodes: Array<VNode>,
    startIdx: number,
    endIdx: number
): void {
    for (; startIdx <= endIdx; ++startIdx) {
        let i: any,
            listeners: number,
            rm: () => void,
            ch = vnodes[startIdx];
        if (ch != null) {
            if (isDef(ch.sel)) {
                invokeDestroyHook(ch);
                listeners = cbs.remove.length + 1;
                // 一切监听删除
                rm = createRmCb(ch.elm as Node, listeners);
                for (i = 0; i < cbs.remove.length; ++i)
                    cbs.remove[i](ch, rm);
                // 假如有钩子则挪用钩子后再调删除回调,假如没,则直接挪用回调
                if (
                    isDef((i = ch.data)) &&
                    isDef((i = i.hook)) &&
                    isDef((i = i.remove))
                ) {
                   i(ch, rm);
                } else {
                    rm();
                }
            } else {
                // Text node
                api.removeChild(parentElm, ch.elm as Node);
            }
        }
    }
}

createElm 要领

将 vnode 转换成真正的 DOM 元素

主要逻辑以下:

  • 触发 init 钩子
  • 处置惩罚解释节点
  • 建立元素并设置 id , class
  • 触发模块 create 钩子 。
  • 处置惩罚子节点
  • 处置惩罚文本节点
  • 触发 vnodeData 的 create 钩子
/**
*  VNode ==> 实在DOM
*/
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any,
        data = vnode.data;

    if (data !== undefined) {
        // 假如存在 data.hook.init ,则挪用该钩子
        if (isDef((i = data.hook)) && isDef((i = i.init))) {
            i(vnode);
            data = vnode.data;
        }
    }

    let children = vnode.children,
        sel = vnode.sel;

    // ! 来代表解释
    if (sel === '!') {
        if (isUndef(vnode.text)) {
            vnode.text = '';
        }
        vnode.elm = api.createComment(vnode.text as string);
    } 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;

        // 依据 tag 建立元素
        const elm = (vnode.elm =
            isDef(data) && isDef((i = (data as VNodeData).ns))
                ? api.createElementNS(i, tag)
                : api.createElement(tag));

        // 设置 id
        if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));

        // 设置 className
        if (dotIdx > 0)
            elm.setAttribute('class',sel.slice(dot + 1).replace(/\./g, ' '));

        // 实行一切模块的 create 钩子,建立对应的内容
        for (i = 0; i < cbs.create.length; ++i)
            cbs.create[i](emptyNode, vnode);

        // 假如存在 children ,则建立children
        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));
        }

        // 实行 vnode.data.hook 中的 create 钩子
        i = (vnode.data as VNodeData).hook; // Reuse variable
        if (isDef(i)) {
            if (i.create) i.create(emptyNode, vnode);
            if (i.insert) insertedVnodeQueue.push(vnode);
        }
    } else {
        // sel 不存在的状况, 即为文本节点
        vnode.elm = api.createTextNode(vnode.text as string);
    }
    return vnode.elm;
}

其他

想相识在各个生命周期都有哪些钩子,请检察:钩子

想相识在各个生命周期内里怎样更新详细的模块请检察:模块

snabbdom源码剖析系列

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

snabbdom源码剖析(二) h函数

snabbdom源码剖析(三) vnode对象

snabbdom源码剖析(四) patch 要领

snabbdom源码剖析(五) 钩子

snabbdom源码剖析(六) 模块

snabbdom源码剖析(七) 事宜处置惩罚

个人博客地址

    原文作者:chen4342024
    原文地址: https://segmentfault.com/a/1190000017519084
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞