maodouchen / note

学习笔记
0 stars 0 forks source link

snabbdom #24

Open maodouchen opened 5 years ago

maodouchen commented 5 years ago

snabbdom.js里有几个核心的函数 h,createElm, init patchVnode upgradeChildren 。 前几个还是很好理解的,upgradeChildren不太好理解。 他主要是用h函数生成了Vnode。 再调用用patch,diff dom, 计算出最新dom,插入节点。再此过程中有各种钩子函数,这里不详细讲解钩子。 使用方式如下:

let snabbdom = require('snabbdom')

var patch = snabbdom.init([
    require('snabbdom/modules/class').default,
    require('snabbdom/modules/props').default,
    require('snabbdom/modules/style').default,
    require('snabbdom/modules/eventlisteners').default,
]);

// h 函数  入参,对象包括下列属性: sel,data(属性),children,text文本,key。
// 出参 Vnode节点对象  包括下列属性 sel,data(属性),children,text文本,key,elm真实dom
var h = require('snabbdom/h').default;

var vnode = h(
    'ul.className#id',
    {style: 'xxxx'},
    [
        h(
            'li',
            {},
            '1'
        ),
        h(
            'li',
            {},
            '2'
        ),
        h(
            'li',
            {},
            '3'
        ),

    ]
);

var newVnode = h(
    'ul',
     {style: 'xxxx'},
    [
        h(
            'li',
            {},
            'a'
        ),
        h(
            'li',
            {},
            'b'
        ),
        h(
            'li',
            {},
            'c'
        ),

    ]
);
// diff新旧vnode的区别  计算出最新的dom结构
patch(vnode, newVnode);

h函数

// h函数  入参 sel代表tagName和class和id等,比如传入的sel是 div.container#box(tagName是div class是container id是box),  b代表props(data)就是tagName的一些属性,  c代表children代表这个节点的子节点
// h函数  出参  return vnode(sel, data, children, text, undefined);  最终返回了一个vnode的节点
//  此函数的作用: 入参b是可传可不传的。  此函数的作用是即使不传b, 也可以判断出 sel和children和text。最后根据这三个参数来调用vnode函数,最终得到vnode节点。SVG tagName特殊处理。
function h(sel, b, c) {

    var data = {}, children, text, i;
    // 如果传了children
    if (c !== undefined) {
        data = b;
        if (is.array(c)) {
            children = c;
        }
        else if (is.primitive(c)) {
            text = c;
        }
        else if (c && c.sel) {
            children = [c];
        }
    }
     // 如果不传children  但是传了props
    else if (b !== undefined) {
        if (is.array(b)) {
            children = b;
        }
        else if (is.primitive(b)) {
            text = b;
        }
        else if (b && b.sel) {
            children = [b];
        }
        else {
            data = b;
        }
    }
    if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
            if (is.primitive(children[i]))
                children[i] = vnode_1.vnode(undefined, undefined, undefined, children[i]);
        }
    }
    if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
        (sel.length === 3 || sel[3] === '.' || sel[3] === '#')) {
        addNS(data, children, sel);
    }
    return vnode(sel, data, children, text, undefined);
}

// 生成vnode节点
function vnode(sel, data, children, text, elm) {
    var key = data === undefined ? undefined : data.key;
    return { sel: sel, data: data, children: children,
        text: text, elm: elm, key: key };
}
exports.vnode = vnode;

createElm函数

//  给这个vnode 添加vnode.elm(真实dom)如果sel等于undefined则添加一个文本节点(createTextNode) 如果sel不等于undefined 按照sel中的class id 创建一个element(深度遍历子节点)
//  总结下:传入一个Vnode  深度遍历这个vnode后,给vnode.ele = 该Vnode代表的真实的dom节点
function createElm(vnode, insertedVnodeQueue) {
    var i, data = vnode.data;
    if (data !== undefined) {
        if (isDef(i = data.hook) && isDef(i = i.init)) {
            i(vnode);
            data = vnode.data;
        }
    }
    var children = vnode.children, sel = vnode.sel;
    if (sel === '!') {
        if (isUndef(vnode.text)) {
            vnode.text = '';
        }
        vnode.elm = api.createComment(vnode.text);
    }
    else if (sel !== undefined) {
        // Parse selector
        var hashIdx = sel.indexOf('#');
        var dotIdx = sel.indexOf('.', hashIdx);
        var hash = hashIdx > 0 ? hashIdx : sel.length;
        var dot = dotIdx > 0 ? dotIdx : sel.length;
        var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
        var elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? api.createElementNS(i, tag)
            : api.createElement(tag);
        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) {
                var ch = children[i];
                if (ch != null) {
                    api.appendChild(elm, createElm(ch, insertedVnodeQueue));
                }
            }
        }
        else if (is.primitive(vnode.text)) {
            api.appendChild(elm, api.createTextNode(vnode.text));
        }
        i = vnode.data.hook; // Reuse variable
        if (isDef(i)) {
            if (i.create)
                i.create(emptyNode, vnode);
            if (i.insert)
                insertedVnodeQueue.push(vnode);
        }
    }
    else {
        vnode.elm = api.createTextNode(vnode.text);
    }
    return vnode.elm;
}

init函数

//  init函数是snabbdom的核心函数,他返回了一个patch函数,
// patch函数的作用
function init(modules, domApi) {

    // 定义domAPI (createElement insert ......)
    // modules :  require('snabbdom/modules/class').default, // makes it easy to toggle classes
    //            require('snabbdom/modules/props').default, // for setting properties on DOM elements
    //            require('snabbdom/modules/style').default, // handles styling on elements with support for animations
    //            require('snabbdom/modules/eventlisteners').default,
    // 定义cbs{create:  'class'  update: 'class'  remove: xxx destory: xxx  pre: xxx   post: xxx} 就是在声明周期create的时候  要去执行classModule    差不多就是这个意思
    var i, j, cbs = {};
    var api = domApi !== undefined ? domApi : htmldomapi_1.default;
    for (i = 0; i < hooks.length; ++i) {
        cbs[hooks[i]] = [];
        for (j = 0; j < modules.length; ++j) {
            var hook = modules[j][hooks[i]];
            if (hook !== undefined) {
                cbs[hooks[i]].push(hook);
            }
        }
    }

    // return上面定义了若干函数patchVnode  updateChildren  removeVnodes  invokeDestroyHook  addVnodes  createElm emptyNodeAt等
    // snabbdom里面有个很常用的函数是sameVnode函数,顾名思义判断两个Vnode是否相同,如果key和sel相等,就认为两个vnode可能相等。 注意是可能。
    //  patch函数的作用  如果sameVnode(newVnode, oldVnode)函数返回false,那么则不需要在进一步的对比了,直接在parent里插入newVnode.elm,并且parent里删掉oldVnode.elm
    // 如果如果sameVnode(newVnode, oldVnode)函数返回true则认为可以进一步对比两个Vnode,调用patchVnode函数
    return function patch(oldVnode, vnode) {
        var i, elm, parent;
        var insertedVnodeQueue = [];
        if (!isVnode(oldVnode)) {
            oldVnode = emptyNodeAt(oldVnode);
        }
        if (sameVnode(oldVnode, vnode)) {
            patchVnode(oldVnode, vnode, insertedVnodeQueue);
        } else {
            // 给vnode添加一个ele属性
            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);
            }
        }
        return vnode;
    };
}

// 辅助函数
function sameVnode(vnode1, vnode2) {
    return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}

patchVnode函数

// snabbdom要求 如果该node有text属性 则不能用children,如果有children,则不能有text。不存在这种dom结构<div>hello world <span>xxx</span></div>
// 先赋值 newVnode.elm = oldVnode.elm; 新旧节点的elm相等,根据后面的对比,修改新节点的elm.
// 对比两个节点的方式(对比两个节点的顺序):
// 1. 如果两个节点的引用相同,则不需要在进行对比。
// 2. newVnode没有text节点
//   a. 如果新旧节点都有children,则updateChildren(newVnode, oldVnode) 后面讲这个函数
//   b. 如果新节点有children,旧节点没有children, 将newVnode.elm最后面插入dom节点(所有的children生成的dom节点)
//   c. 如果新节点有没有children,旧节点有children,则将newVnode.elm的所有子节点删除
//   d. 如果新旧节点都没有children,则将newVnode.text设置为空
// 3. newVnode有text节点
//   a. newVnode.text和oldVnode.text不相等,则重置newVnode.elm的text
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
    var i, hook;
    var elm = vnode.elm = oldVnode.elm;
    var oldCh = oldVnode.children;
    var ch = vnode.children;
    if (oldVnode === vnode)
        return;
    if (isUndef(vnode.text)) {
        // 如果child和oldchild都存在的话  并且不相等  就update children
        if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch)
                updateChildren(elm, oldCh, ch, insertedVnodeQueue);
        }
        // 如果child存在 oldchild不存在  并且oldVnode.tex存在那么就给elm设置个空text, 给el 增加子元素
        else if (isDef(ch)) {
            if (isDef(oldVnode.text))
                api.setTextContent(elm, '');
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
        }
       //  如果oldchild存在 和child都不存在  那么久移除elm下面的 0 到 oldch.length-1 的子元素
        else if (isDef(oldCh)) {
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
        }
        // oldchild 和child都不存在  并且oldVnode.text存在 则给elm设置个空text
        else if (isDef(oldVnode.text)) {
            api.setTextContent(elm, '');
        }
    }

    // 如果vnode.text不存在   并且oldVnode.text !== vnode.text 则给elm 设置vnode.text 属性
    else if (oldVnode.text !== vnode.text) {
        api.setTextContent(elm, vnode.text);
    }
}

// 辅助函数
// 在parentElm里,before前,  插入子节点(vnode[startIdx~endIdx])
function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
        var ch = vnodes[startIdx];
        if (ch != null) {
            api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
        }
    }
}

updateChildren 函数,图示:

image

// 对比s1s2, e1e2,s1e2,e1s2这四个节点是否sameVnode,将dom节点移动,比如s1 sameVnode e2,说明在dom节点中,s1移动到了e1的后面。
// 如果进行了上述的四次对比,发现都不是sameVnode, 就继续用key值进行对比,oldKeyIdx存放的是所有旧节点的key值,如果s2的key值不在oldKeyIdx中,说明s2是新增节点,在真实dom中,将s2的dom节点放在s1的前面。
// 如果如果s2的key值在oldKeyIdx中,就继续通过tagName来对比两个节点是否相同
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
    var oldStartIdx = 0, newStartIdx = 0;
    var oldEndIdx = oldCh.length - 1;
    var oldStartVnode = oldCh[0];
    var oldEndVnode = oldCh[oldEndIdx];
    var newEndIdx = newCh.length - 1;
    var newStartVnode = newCh[0];
    var newEndVnode = newCh[newEndIdx];
    var oldKeyToIdx;
    var idxInOld;
    var elmToMove;
    var before;
    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];
        }
        //  如果s1 sameVnode s2说明这两个vnode节点有继续对比的必要  所以执行patchVnode通过children text属性 进一步对比 。s1++, s2++指针往后移
        else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        }
        // 如果e1和e2 sameVnode,说明这两个vnode节点有继续对比的必要,所以要执行patchVnode 通过children text属性 进一步对比, e1--, e2--往前移
        else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        }
        // 如果s1和e2 sameVnode, 说明这两个vnode节点有继续对比的必要,所以要执行patchVnode 通过children text属性 进一步对比。  s1++ e2--
        // 且说明新的dom节点在位置上发生了变化:s1移动到了最后面,所以执行 api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
        else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
            api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        }
        // 如果e1 s2 sameVnode,说明新的节点位置上发生了变化,e1的dom节点移动到了最前面
        else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
            api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        }
        // 对比了四次,都不是sameVnode,则继续用key进行对比。将所有旧节点的key存放在oldKeyToIdx中。
        else {
            if (oldKeyToIdx === undefined) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
            }
            idxInOld = oldKeyToIdx[newStartVnode.key];
            // 如果s2在oldKeyToIdx中不存在,则说明s2是新增的节点,创建这个新节点的dom,并且将它插在s1的前面。s2++
            if (isUndef(idxInOld)) {
                api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
                newStartVnode = newCh[++newStartIdx];
            }
            // 如果s2在oldKeyToIdx中存在,且记为 elmToMove = oldCh[idxInOld]; 如果s2和elmToMove tagName不同,和上面的处理方式一样,当成新增节点来处理。如果s2和elmToMove tagName相同,
            // 则通过patchVnode继续对比子节点,不需要在根据s2重复创建一个dom节点,可以直接用elmToMove的dom节点,将它插在s1的前面。
            else {
                elmToMove = oldCh[idxInOld];
                if (elmToMove.sel !== newStartVnode.sel) {
                    api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
                }
                else {
                    patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
                    oldCh[idxInOld] = undefined;
                    api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
                }
                newStartVnode = newCh[++newStartIdx];
            }
        }
    }
    // 如果s1 > e1 说明oldChild先遍历完,那么newchild中间没有遍历的就是新增节点,将这些新增节点插入到dom中。
    if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
    }  
    // 如果s2 > e2 说明newChild先遍历完,那么oldChild中间没有遍历的就是要删除的节点,将这些dom节点删掉。
    else if (newStartIdx > newEndIdx) {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
}

可以总结下各个函数的作用

maodouchen commented 3 years ago

https://blog.csdn.net/qq_39290323/article/details/108336194