zoukankan      html  css  js  c++  java
  • ④ 数据状态更新时的差异diff及patch机制

    1 数据更新视图

    2 跨平台

    因为使用了 Virtual DOM,Vue.js 具有了跨平台能力

    Virtual DOM 只是 js 对象,是如何调用不同平台的 api 的?

    • 依赖于适配层:将不同平台的 api 封装在内,以同样的接口对外暴露

    2.1 举个栗子

    • 提供 nodeOps 对象做适配,根据 platform 区分不同平台来执行当前平台对应的 api,而对外提供了一致的接口,供 Virtual DOM 来调用。
    const nodeOps = {
        setTextContent(text) {
            if(platform === 'weex') {
                node.parentNode.setAttr('value', text);
            } else if(platform === 'web') {
                node.textContent = text;
            }
        },
        parentNode() {
            // ...
        },
        removeChild() {
            // ...
        },
        nextSibling() {
            // ...
        },
        insertBefore() {
            // ...
        }
    }
    

    3 patch 过程使用到的 API

    • 这些 apipatch 的过程中使用,调用 nodeOps 中的相应函数来操作平台

    3.1 insert

    • 用来在 parent 这个父节点下插入一个子节点,如果指定了 ref 则插入到 ref 这个子节点前面
    function insert(parent, elm, ref) {
        if(parent) {
            if(ref) {
                if(ref.parentNode === parent) {
                    nodeOps.insertBefore(parent, elm, ref);
                }
            } else {
                nodeOps.appendChild(parent, elm);
            }
        }
    }
    

    3.2 createElm

    • 用来新建一个节点,tag 存放创建一个标签节点,否则创建一个文本节点
    function createELm(vnode, parentElm, refElm) {
        if(vnode.tag) {
            insert(parentElm, nodeOps.createElement(vnode.tag), ref);
        } else {
            insert(parentElm, nodeOps.createElement(vnode.text), ref);
        }
    }
    

    3.3 addVnodes

    • 用来批量调用 createElm 新建节点
    function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx) {
        for(; startIdx <= endIdx; ++startIdx) {
            createElm(vnodes[startIdx], parentElm, refElm);
        }
    }
    

    3.4 removeNode

    • 用来移除一个节点
    function removeNode(el) {
        const parent = nodeOps.parentNode(el);
        if(parent) {
            nodeOps.removeChild(parent, el);
        }
    }
    

    3.5 removeVnodes

    • 用来批量调用 removeNode 移除节点
    function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
        for(; startIdx <= endIdx; ++startIdx) {
            const ch = vnodes[startIdx];
            if(ch) {
                removeNode(cn.elm);
            }
        }
    }
    

    4 patch

    4.1 diff 算法

    diff 算法可以比对出两棵树的差异

    • diff 算法是通过同层的树节点进行比较 --> 时间复杂度只有 O(n),是一种相当高效的算法

    4.2 patch 的过程

    • patch 的主要功能:比对两个 Vnode 节点,将差异更新到视图上
    function patch(oldVnode, vnode, parentElm) {
        if(!oldVnode) {
            addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
        } else if(!vnode) {
            removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
        } else {
            if(sameVnode(oldVnode, vnode)) {
                patchVnode(oldVnode, vnode);
            } else {
                removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
                addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
            }
        }
    }
    
    1. 在 oldVnode 不存在时
    • 相当于新的 Vnode 替代原本没有的节点,所以直接用 addVnodes 将这些节点批量添加到 parentElm
    if(!oldVnode) {
        addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
    }
    
    2. 在 Vnode(新Vnode节点)不存在时
    • 相当于要把旧的节点删除,所以直接用 removeVnodes 进行批量的节点删除
    else if(!vnode) {
        removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
    }
    
    3. 当 oldVnode 与 Vnode 都存在时
    • 需要判断它们是否属于 sameVnode,如果是则进行 patchVnode 操作,否则,删除旧节点,增加新节点
    if(sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode);
    } else {
        removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
        addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
    }
    

    5 sameVnode

    • 只有当 keytagisComment(是否为注释节点)、data 同时定义(不定义),同时满足当标签类型为 input 的时候 type 相同即可。
    function sameVnode() {
        return (
        	a.key === b.key &&
            a.tag === b.tag &&
            a.isComment === b.isComment &&
            (!!a.data) === (!!b.data) &&
            sameInputType(a, b)
        )
    }
    function sameInputType(a, b) {
        if(a.tag !== 'input') return true;
        let i;
        const typeA = (i = a.data) && (i = i.attrs) && i.type;
        const typeB = (i = b.data) && (i = i.attrs) && i.type;
        return typeA === typeB;
    }
    

    6 patchVnode

    • patchVnode 是在符合 sameVnode 的条件下触发的,会进行比对
    function patchVnode(oldVnode, vnode) {
        if(oldVnode === vnode) return
        if(vnode.isStatic && oldVnode.isStatic && vnode.key === oldVnode.key) 
        {
        	vnode.elm = oldnode.elm;
        	vnode.componentInstance = oldVnode.componentInstance;
        	retirn;
        }
        if(vnode.text) {
            nodeOps.setTextContent(elm, vnode.text);
        }
        if(oldCh && ch && (oldCh !== ch)) {
            updateChildren(elm, oldCh, ch);
        } else if(ch) {
            if(oldVnode.text) nodeOps.setTextContent(elm, '');
            addVnodes(elm, null, ch, 0, ch.length - 1);
        } else if(oldCh) {
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
        } else if(oldVnode.text) {
            nodeOps.setTextContent(elm, '');
        }
    }
    

    6.1 在新旧 Vnode 节点相同时,直接 return

    if(oldVnode === vnode) return
    

    6.2 当新旧 Vnode 节点都是 isStatic(静态的),并且 key相同时

    • componentInstanceelm 从旧 Vnode 节点“拿过来”,跳过比对的过程
    if(vnode.isStatic && oldVnode.isStatic && vnode.key === oldVnode.key) {
        vnode.elm = oldnode.elm;
        vnode.componentInstance = oldVnode.componentInstance;
        retirn;
    }
    

    6.3 当新 Vnode 节点是文本节点时,直接用 setTextContent 来设置 text--nodeOps 是适配层

    if(vnode.text) {
        nodeOps.setTextContent(elm, vnode.text);
    }
    

    6.4 当新 Vnode 节点是非文本节点时

    • oldChch 都存在且不相同时,使用 UpdateChildren 函数来更新子节点

    • 当只有 ch 存在时,如果旧节点是文本节点则先将节点的文本清除,然后将 ch 批量插入到节点 elm

    • 当只有 oldCh 存在时,说明需要将旧节点通过 removeVnodes 全部清除

    • 当只有旧节点是文本节点时,清除其节点文本内容

    if(oldCh && ch && (oldCh !== ch)) {
        updateChildren(elm, oldCh, ch);
    } else if(ch) {
        if(oldVnode.text) nodeOps.setTextContent(elm, '');
        addVnodes(elm, null, ch, 0, ch.length - 1);
    } else if(oldCh) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
    } else if(oldVnode.text) {
        nodeOps.setTextContent(elm, '');
    }
    

    7 updateChildren

    7.1 定义新旧节点两边的索引:oldStartIdxnewStartIdxoldEndIdxnewEndIdx

    • 同时定义指向这几个索引对应的 Vnode 节点:oldStartVnodenewStartVnodeoldEndVnodenewEndVnode

    7.2 while循环:oldStartIdxnewStartIdxoldEndIdxnewEndIdx 会逐渐向中间靠拢

    1 当 oldStartVnode 或者 oldEndVnode 不存在时,oldStartIdxoldEndIdx 继续向中间靠拢,并更新对应的 oldStartVnodeoldEndVnode 的指向
    2 将 oldStartIdxnewStartIdxoldEndIdxnewEndIdx 两两比对的过程

    一共会出现 2*2=4 种情况

    • oldStartVnodenewStartVnode 符合 sameVnode 时,直接 patchVnode,同时 oldStartIdxnewStartIdx 向后移一位

    • oldEndVnodenewEndVnode 符合 sameVnode 时,同样进行 patchVnode 操作并将 oldEndIdxnewEndIdx 向前移动一位

    • oldStartVnodenewEndVnode 符合 sameVnode 时,将 oldStartVnode.elm 直接移动到 oldEndVnode.elm 这个节点后面,然后 oldStartIdx 向后移动一位,newEndIdx 向前移动一位

    • oldEndVnodenewStartVnode 符合 sameVnode 时,将 oldEndVnode.elm 直接移动到 oldStartVnode.elm 这个节点后面,然后 oldEndIdx 向前移动一位,newStartIdx 向后移动一位

    3 其他情况下
    • 处理节点得到一个 keyindex 索引对应的 map 表 oldKeyToIdx

    • 可以根据某一个 key 值,快速地从 oldKeyToIdx 中获取相同 key 节点的索引 idxInOld,然后找到相同的节点

    • 如果没有找到相同的节点,则通过 createElm 创建一个新节点,并将 newStartIdx向后移一位

    • 如果找到了节点,同时也符合 sameVnode,则将这两个节点进行 patchVnode,将该位置的旧节点赋值为 undefined,同时将 newStartVnode.elm 插入到 oldStartVnode.elm 前面,newStartIdx 后移一位

    • 如果不符合 sameVnode,只能创建一个新节点插入到 parentElm 的子节点中,newStartIdx 后移一位

    7.3 当 while 循环结束后,如果 oldStartIdx > oldEndIdx,调用 addVnodes 将新节点多的节点插入

    7.4 如果满足 newStartIdx > newEndIdx,调用 removeVnodes 将多的旧节点批量删除

    function updateChildren(parentElm, oldCh, newCh) {
        // 新旧节点 两边的索引及节点
        let oldStartIdx = 0;
        let newStartIdx = 0;
        let oldEndIdx = oldCh.length - 1;
        let newEndIdx = newCh.length - 1;
        let oldStartVnode = oldCh[0];
        let newStartVnode = newCh[0];
        let oldEndVnode = oldCh[oldEndIdx];
        let newEndVnode = newCh[newEndIdx];
        let oldKeyToIdx, idxInOld, elmToMove, refElm;
    
        // 在while循环中,新旧节点两边的索引会逐渐向中间靠拢
        while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
            if(!oldStartVnode) {
                oldStartVnode = oldCh[++oldStartIdx];
            } else if(!oldEndVnode) {
                oldEndVnode = oldCh[--oldEndIdx];
            } else if(sameVnode(oldStartVnode, newStartVnode)) {
                patchVnode(oldStartVnode, newStartVnode);
                oldStartVnode = oldCh[++oldStartIdx];
                newStartVnode = newCh[++newStartIdx];
            } else if(sameVnode(oldEndVnode, newEndVnode)) {
                patchVnode(oldEndVnode, newEndVnode);
                oldEndVnode = oldCh[--oldEndIdx];
                newEndVnode = newCh[--newEndIdx];
            } else if(sameVnode(oldStartVnode, newEndVnode)) {
                patchVnode(oldStartVnode, newEndVnode);
                nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));// 第三个参数什么意思?第一个参数是什么
                oldStartVnode = oldCh[++oldStartIdx];
                newEndVnode = newCh[--newEndIdx];
            } else if(sameVnode(oldEndVnode, newStartVnode)) {
                patchVnode(oldEndVnode, newStartVnode);
                nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);//不懂too
                oldEndVnode = oldCh[--oldEndIdx];
                newStartVnode = newCh[++newStartIdx];
            } else {
                let elmToMove = oldCh[idxInOld];
                if(!oldKeyToIdx) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
                if(!idxInOld) {
                    createElm(newStartVnode, parentElm);
                    newStartVnode = newCh[++newStartIdx];
                } else {
                    elmToMove = oldCh[idxInOld];
                    if (sameVnode(elmToMove, newStartVnode)) {
                        patchVnode(elmToMove, newStartVnode);
                        oldCh[idxInOld] = undefined;
                        nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm);
                        newStartVnode = newCh[++newStartIdx];
                    } else {
                        createElm(newStartVnode, parentElm);
                        newStartVnode = newCh[++newStartIdx];
                    }
                }
            }
        }
    
        if(oldStartIdx > oldEndIdx) {
            refElm = (newCh[newEndIdx + 1]) ? newCh[newEndIdx + 1].elm : null;
            addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx);
        } else if(newStartIdx > newEndIdx) {
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
        }
    }
    // 产生key与index索引对应的一个map表
    /**
     * [
     *    {xx: xx, key: 'key0'},
     *    {xx: xx, key: 'key1'},
     *    {xx: xx, key: 'key2'}
     * ]
     * {
     *    key0: 0,
     *    key1: 1,
     *    key2: 2    
     * }
     */
    createKeyToOldIdx(children, beginIdx, endIdx) {
    	let i, key;
    	const map = {};
    	for(i = beginIdx; i <= endIdx; ++i) {
            key = children[i].key;
            if(isDef(key)) map[key] = i;
    	}
    	return map;
    
    }
    
  • 相关阅读:
    HTTP-接触
    什么是虚拟机-粗略学习
    jQuery中的动画理论干货
    jQuery-中的事件
    熟悉又陌生的快捷方式
    jQuery中的DOM操作
    jQuery与javascript库
    jQuery-选择器(2)
    jest操作 Elasticsearch
    配置 Kibana
  • 原文地址:https://www.cnblogs.com/pleaseAnswer/p/14331296.html
Copyright © 2011-2022 走看看