Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

虚拟dom: snabbdom源码解析 #23

Open
h476564406 opened this issue Feb 16, 2019 · 0 comments
Open

虚拟dom: snabbdom源码解析 #23

h476564406 opened this issue Feb 16, 2019 · 0 comments

Comments

@h476564406
Copy link
Owner

h476564406 commented Feb 16, 2019

参考:解析 snabbdom 源码,教你实现精简的 Virtual DOM 库

h 和 patch 函数

h函数,生成vnode对象

function h(type, config, ...children) {
    const key = getKeyFromConfig(config);
    const props = extractPropsFromConifg(config);

    return vnode(
        type,
        key,
        props,
        flattenArray(children).map(c => {
            return isPrimitive(c)
                ? vnode(undefined, undefined, undefined, undefined, c)
                : c;
        }),
    );
}

function vnode(type, key, data, children, text, elm) {
  const element = {
    __type: VNODE_TYPE,  // const VNODE_TYPE = Symbol('virtual-node')
    type, key, data, children, text, elm
  }

  return element
}
const vnode = h('div', { id: 'container' }, [
    h('p', { style: { fontWeight: 'bold' } }, 'This is greeting'),
    'Hello world',
]);

image

console.log('vnode', vnode);

image

init得到patch函数

/**
 * hook name | 所处阶段 | 参数
 * pre | the patch process begins | none
 * init | a vnode has been added | vnode
 * create | a DOM element has been created based on a vnode | emptyVnode, vnode
 * insert | an element has been inserted into the DOM | vnode
 * update | an element is being updated | oldVnode, vnode
 * destroy | an element is directly or indirectly being removed | vnode
 * remove | an element is directly being removed from the DOM | vnode, removeCallback
 * post | the patch process is done | none
 */
const hooks = ['pre', 'create', 'update', 'destroy', 'remove', 'post'];
const emptyNode = vnode('', '', {}, [], undefined, undefined);

function init(modules = [classModule, propsModule, styleModule], domApi) {
    const cbs = collectCallbacksFromModules(modules);

   return function patch(oldVnode, vnode) {};
}

const patch = init([
  classModule,
  propsModule,
  styleModule
])

console.log('cbs', cbs);

image

脚本里调用patch, 来对比找出同级dom中不一样的地方,进行更新。

  1. patch(container, vnode); 第一次把所有元素生成一个 vnode 然后 patch 到一个空 DOM 元素。
 function patch(oldVnode, vnode) {
        presCbsIterator();

        if (!isVnode(oldVnode)) {
            oldVnode = emptyNodeAt(oldVnode);
        }

        if (isSameKeyVnode(oldVnode, vnode)) {
            patchVnode(oldVnode, vnode);
        } else {
            // 否则,直接把 oldVnode 替换为 vnode。
            parent = api.parentNode(oldVnode.elm);
            // 为 vnode(tree) vnode.elm=xxx; 实际创建 dom但是未插入文档
            createElmToVnode(vnode);

            if (parent !== null) {
               // parent 真实dom
                api.insertBefore(parent, vnode.elm, api.nextSibling(oldVnode.elm));
                // 删除原有dom
                removeVnodes(parent, [oldVnode], 0, 0);
            }
        }

        postCbsIterator();

        return vnode;
    }
 function createElmToVnode(vnode, insertedVnodeQueue) {
        switch vnode.type {
            1. comment =>  vnode.elm = api.createComment(vnode.text);
            2. text =>  vnode.elm = api.createTextNode(vnode.text);
            3. other type => vnode.elm = (vnode.elm = data.ns
                ? api.createElementNS(data.ns, type)
                : api.createElement(type));
            // other type, 需要分别处理 children 和 text。隐含逻辑:vnode 的 children 和 text 不会同时存在。
           // 每创建一个dom,都调用createCbsIterator();
             createCbsIterator();
            if (isArray(vnode.children)) {
                // 递归 children,保证 vnode tree 中每个 vnode 都有自己对应的 dom; 构建 vnode tree 对应的 dom tree。
                children.forEach(ch => {
                    ch &&
                        api.appendChild(vnode.elm, createElmToVnode(ch));
                });
            } else if (isPrimitive(vnode.text)) {
                api.appendChild(vnode.elm, api.createTextNode(vnode.text));
            }
        }

        return vnode.elm;
    }
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
        for (; startIdx <= endIdx; ++startIdx) {
            let ch = vnodes[startIdx];
            api.removeChild(parentElm, ch.elm);
            removeCbsIterator(ch.elm);
        }
}

  1. patch(vnode, newVnode); 当数据源有所改变, 第二次渲染会重新生成 newVnode, patch 的时候会进行对比, 找出需要更新的部分再进行更新。
function patchVnode(oldVnode, vnode) {
    // 因为 vnode 和 oldVnode是相同的,所以可以复用 oldVnode.elm。
    const elm = (vnode.elm = oldVnode.elm);
    let oldCh = oldVnode.children;
    let ch = vnode.children;

    updateCbsIterator(oldVnode, vnode);

    // 如果不是文本节点
    if (vnode.text === undefined) {
        // 比较新旧 children 并更新
       // oldVnode 和 vnode 是相同的才 patch,且 oldVnode 自己对应的 dom总是已经存在的,vnode 的 dom 是不存在的,直接复用 oldVnode 对应的 dom。
        if (oldCh && ch) {
            if (oldCh !== ch) {
                updateChildren(elm, oldCh, ch);
            }

            return;
        }

        // 如果原来没有children而现在有,添加新children。
        if (ch) {
            if (oldVnode.text) api.setTextContent(elm, '');
            // 然后添加新 dom(对 ch 中每个 vnode 递归创建 dom 并插入到 elm)
            addVnodes(elm, null, ch, 0, ch.length - 1);
            return;
        }

        // 如果原来有children而现在没有,删除旧children。
        if (oldCh) {
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
            return;
        } else if (oldVnode.text) {
            // 最后,如果 oldVnode 有 text,删除。
            api.setTextContent(elm, '');
        }
    } else if (oldVnode.text !== vnode.text) {
        api.setTextContent(elm, vnode.text);
    }
}
function addVnodes(parentElm, before, vnodes, startIdx, endIdx) {
    for (; startIdx <= endIdx; ++startIdx) {
        const ch = vnodes[startIdx];

        if (ch != null) {
            api.insertBefore(
                parentElm,
                createElmToVnode(ch),
                before,
            );
        }
    }
}
  1. 最复杂的地方updateChildren, oldCh!==newCh
 function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
    let oldStartIdx = 0,
        newStartIdx = 0;
    let oldEndIdx = oldCh.length - 1;
    let newEndIdx = newCh.length - 1;

    let oldStartVnode = oldCh[0];
    let oldEndVnode = oldCh[oldEndIdx];
    let newStartVnode = newCh[0];
    let newEndVnode = newCh[newEndIdx];

    let oldKeyToIdx;
    let idxInOld;
    let elmToMove;
    let before;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        // 前4种情况,过滤掉 vnode为空。
        if (oldStartVnode == null) {
            oldStartVnode = oldCh[++oldStartIdx];
        } else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx];
        } else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx];
        } else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx];
        } else if (isSameKeyVnode(oldStartVnode, newStartVnode)) {
            // 1. 如果 oldStartVnode 和 newStartVnode 相同(key相同), 执行 patch, 不需要移动 dom
            patchVnode(oldStartVnode, newStartVnode);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        } else if (isSameKeyVnode(oldEndVnode, newEndVnode)) {
            // 2. 如果 oldEndVnode 和 newEndVnode 相同,执行 patch,不需要移动 dom
            patchVnode(oldEndVnode, newEndVnode);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (isSameKeyVnode(oldStartVnode, newEndVnode)) {
            // 3. 如果 oldStartVnode 和 newEndVnode 相同,执行 patch。
            patchVnode(oldStartVnode, newEndVnode);
            //  把获得更新后的 (oldStartVnode/newEndVnode)的dom右移,移动到oldEndVnode 对应的 dom 的右边。
            api.insertBefore(
                parentElm,
                oldStartVnode.elm,
                api.nextSibling(oldEndVnode.elm),
            );
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        } else if (isSameVnode(oldEndVnode, newStartVnode)) {
            // 4. 如果 oldEndVnode 和 newStartVnode 相同,执行 patch
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
            api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        } else {
            // 4 个 vnode 都不相同,要从 oldCh 数组建立 key --> index 的 map。
            // 1. 处理 newStartVnode,以它的 key 从上面的 map 里拿到 index;
            // 2. 如果 idxInOld 存在,说明有对应的 old vnode,patch;
            // 3. 如果 idxInOld 不存在,那么说明 newStartVnode 是全新的 vnode,直接创建对应的 dom 并插入。
            // 4. 如果 oldKeyToIdx 不存在,创建 old children 中 vnode 的 key 到 index的映射,方便之后通过 key 去拿下标。
            if (oldKeyToIdx === undefined) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
            }

            // 通过 newStartVnode 的 key 去拿下标
            idxInOld = oldKeyToIdx[newStartVnode.key];
            
            // 下标不存在,说明 newStartVnode 是全新的 vnode。
            if (idxInOld == null) {
                // 那么为 newStartVnode 创建 dom 并插入到 oldStartVnode.elm 的前面。
                api.insertBefore(
                    parentElm,
                    createElmToVnode(newStartVnode),
                    oldStartVnode.elm,
                );
                newStartVnode = newCh[++newStartIdx];
            } else {
                // 下标存在,说明 old children 中有相同 key 的 vnode,
                elmToMove = oldCh[idxInOld];

                // 如果 type 不同,没办法,只能创建新 dom;
                if (elmToMove.type !== newStartVnode.type) {
                    api.insertBefore(
                        parentElm,
                        createElmToVnode(newStartVnode),
                        oldStartVnode.elm,
                    );
                } else {
                    // type 相同(且key相同),那么说明是相同的 vnode,执行 patch。
                    patchVnode(elmToMove, newStartVnode);
                   // 这行代码很重要,用于调整oldStartIdx,oldEndIdx
                  // e.g. 前dom顺序:  1 2 3 4, 后dom顺序 2 1 3 4. 第一次调整位置后,原来的oldCh数组变为 1 null 3 4
                    oldCh[idxInOld] =null;

                    api.insertBefore(
                        parentElm,
                        elmToMove.elm,
                        oldStartVnode.elm,
                    );
                }
                newStartVnode = newCh[++newStartIdx];
            }
        }
    }

    // 上面的循环结束后(循环条件有两个),处理可能的未处理到的 vnode。
    // oldStartIdx > oldEndIdx,说明 old vnodes 已经处理完毕
    // 检查 new vnodes 是否有未处理的
    if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
        addVnodes(
            parentElm,
            before,
            newCh,
            newStartIdx,
            newEndIdx,
            insertedVnodeQueue,
        );
    } else if (newStartIdx > newEndIdx) {
        // 相反,oldStartIdx <= oldEndIdx并且newStartIdx > newEndIdx,
        // 说明 new vnodes 已经处理完毕, old vnodes 有未处理的,删除多余的 dom。
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
}
@h476564406 h476564406 changed the title 虚拟dom: snabbdom 虚拟dom: snabbdom源码解析 Feb 16, 2019
@h476564406 h476564406 reopened this Mar 3, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant