关于javascript:Vue虚拟DOM

45次阅读

共计 7295 个字符,预计需要花费 19 分钟才能阅读完成。

Vue- 虚构 DOM

一、模板转换成视图的过程

  1. Vue.js 通过编译将 template 模板转换成渲染函数 (h),执行渲染函数就能够失去一个虚构节点树。
  2. 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来批改视图。这个过程次要是将新旧虚构节点进行差别比照,而后依据比照后果进行 DOM 操作来更新视图。

二、Virtual DOM

1. 定义

Virtual DOM 其实就是一棵以 VNode 节点作为根底的树,用对象属性来形容节点,实际上它只是一层对实在 DOM 的形象。最终能够通过一系列操作使这棵树映射到实在环境上。

简略来说,能够把 Virtual DOM 了解为一个简略的 JS 对象,其中几个比拟重要的属性:

  • tag 属性即这个 vnode 的标签属性
  • data 属性蕴含了最初渲染成实在 dom 节点后,节点上的 class,attribute,style 以及绑定的事件
  • children 属性是 vnode 的子节点
  • text 属性是文本属性
  • elm 属性为这个 vnode 对应的实在 dom 节点
  • key 属性是 vnode 的标记,在 diff 过程中能够进步 diff 的效率,后文有解说

对于虚构 DOM,咱们来看一个简略的实例,就是下图所示的这个,具体的论述了模板 → 渲染函数 → 虚构 DOM 树 → 实在 DOM 的一个过程

2. 作用

虚构 DOM 的最终目标是将虚构节点渲染到视图上。然而如果间接应用虚构节点笼罩旧节点的话,会有很多不必要的 DOM 操作。例如,一个 ul 标签下很多个 li 标签,其中只有一个 li 有变动,这种状况下如果应用新的 ul 去代替旧的 ul, 因为这些不必要的 DOM 操作而造成了性能上的节约。

为了防止不必要的 DOM 操作,虚构 DOM 在虚构节点映射到视图的过程中,将虚构节点与上一次渲染视图所应用的旧虚构节点(oldVnode)做比照,找出真正须要更新的节点来进行 DOM 操作,从而防止操作其余无需改变的 DOM。

其实虚构 DOM 在 Vue.js 次要做了两件事:

** 提供与实在 DOM 节点所对应的虚构节点 vnode,
将虚构节点 vnode 和旧虚构节点 oldVnode 进行比照,而后更新视图 **

3. 劣势:

  • 具备跨平台的劣势: 因为 Virtual DOM 是以 JavaScript 对象为根底而不依赖实在平台环境,所以使它具备了跨平台的能力,比如说浏览器平台、Weex、Node 等。
  • 操作 DOM 慢,js 运行效率高咱们能够将 DOM 比照操作放在 JS 层,提高效率: 因为 DOM 操作的执行速度远不如 Javascript 的运算速度快,因而,把大量的 DOM 操作搬运到 Javascript 中,使用 patching 算法来计算出真正须要更新的节点,最大限度地缩小 DOM 操作,从而显著进步性能。
  • 晋升渲染性能: Virtual DOM 的劣势不在于单次的操作,而是在大量、频繁的数据更新下,可能对视图进行正当、高效的更新。

三、diff 算法

Vue 的 diff 算法是基于 snabbdom 革新过去的,仅在同级的 vnode 间做 diff,递归地进行同级 vnode 的 diff,最终实现整个 DOM 树的更新。因为跨层级的操作是非常少的,忽略不计,这样工夫复杂度就从 O(n3) 变成 O(n)。

diff 算法包含几个步骤:

  1. 用 JavaScript 对象构造示意 DOM 树的构造;而后用这个树构建一个真正的 DOM 树,插到文档当中
  2. 当状态变更的时候,从新结构一棵新的对象树。而后用新的树和旧的树进行比拟,记录两棵树差别
  3. 把所记录的差别利用到所构建的真正的 DOM 树上,视图就更新了

四、实现代码

1. template 模板转换成渲染函数 (h)

const root = document.getElementById('root');

const oldVnode = h('ul', { id: 'container'},
    h('li', { style: { backgroundColor: '#110000'}, key: 'A' }, 'A'),
    h('li', { style: { backgroundColor: '#440000'}, key: 'B' }, 'B'),
    h('li', { style: { backgroundColor: '#770000'}, key: 'C' }, 'C'),
);
const newVnode = h('ul', { id: 'newContainer'},
    h('li', { style: { backgroundColor: '#440000'}, key: 'B' }, 'B1'),
    h('li', { style: { backgroundColor: '#110000'}, key: 'A' }, 'A1'),
    h('li', { style: { backgroundColor: '#AA0000'}, key: 'C' }, 'C1'),
    // h('li', { style: { backgroundColor: '#AA0000'}, key: 'E' }, 'E1'),
);
mount(oldVnode, root);

setTimeout(() => {patch(oldVnode, newVnode);
}, 1000)

2. 通过渲染函数转化为 虚构 DOM 树

function h(type, props, ...children) {let key, newProps = {};
    if (props) {if (props.key) {
            key = props.key;
            delete props.key;
        }
        for (let item in props) {if (props.hasOwnProperty(item)) {newProps[item] = props[item];
            }
        }
    }
    return vnode(type, key, props, children.map(child => {
        // 解决文字节点
        if (typeof child == 'string' || typeof child == 'number') {return vnode(undefined, undefined, undefined, undefined, child);
        }
        return child;
    }))
}
// 生成 vnode 节点
function vnode(type, key, props = {}, children, text, domElement) {
    return {type, key, props, children, text, domElement}
}

3. 渲染节点

1. 把虚构 DOM 节点封装成一个实在 DOM 节点

function createDOMElementFromVnode(vnode) {
    let type = vnode.type;
    let children = vnode.children;
    if (type) {
        // 一般节点 eg:div,span
        vnode.domElement = document.createElement(vnode.type);
        updateProperties(vnode);
        if (Array.isArray(children)) {
            children.map(child => {return vnode.domElement.appendChild(createDOMElementFromVnode(child))
            })
        }
    } else {
        // 文本节点
        vnode.domElement = document.createTextNode(vnode.text);
    }
    return vnode.domElement;
}

2. 更新属性

function updateProperties(vnode, oldProps = {}) {
    let newProps = vnode.props;
    let domElement = vnode.domElement;

    let oldStyle = oldProps.style || {};
    let newStyle = newProps.style || {};

    // 遍历老属性 (款式和属性),查看是否新属性中存在
    for (let item in oldProps) {if (!newProps[item]) {delete domElement[item];
        }
    }
    for (let item in oldStyle) {if (!newStyle[item]) {domElement.style[item] = "";
        }
    }

    // 遍历新属性 款式独自赋值
    for (let item in newProps) {if (item == 'style') {let styleObj = newProps[item];
            for (let styleItem in styleObj) {domElement.style[styleItem] = styleObj[styleItem];
            }
        } else {domElement[item] = newProps[item];
        }
    }

}

3. 渲染节点

function mount(vnode, root) {root.appendChild(createDOMElementFromVnode(vnode));
}

4.patch 更新

1. 新老节点的更新

// 新老节点的更新
function patch(oldVnode, newVnode){
    // 节点不同 间接替换
    if(oldVnode.type != newVnode.type){return oldVnode.domElement.parentNode.replaceChild(createDOMElementFromVnode(newVnode),oldVnode.domElement);
    }
    // 如果新节点是文本节点,那么间接批改文本内容
    if(newVnode.text){return oldVnode.domElement.textContent = newVnode.text;}
    let domElement = newVnode.domElement = oldVnode.domElement;

    updateProperties(newVnode, oldVnode.props);

    let oldChildren = oldVnode.children;
    let newChildren = newVnode.children;

    if(oldChildren.length > 0 && newChildren.length>0){
        // 新老节点都存在
        updateChildren(domElement,oldChildren,newChildren);
    }else if(oldChildren.length > 0){
        // 老节点存在子节点,新的没有
        oldVnode.domElement.innerHTML = '';

    }else if(newChildren.length>0){
        // 老节点不存在子节点,新的有
        for(let i = 0;i< newChildren.length;i++){oldVnode.domElement.appendChild(createDOMElementFromVnode(newChildren[i]));
        }
    }
}

2. 新老节点都存在,比拟更新

// 新老节点都存在,比拟更新,次要是列表
function updateChildren(parentDOMElement,oldChildren,newChildren){
    let oldStartIndex = 0;
    let oldStartNode = oldChildren[0];
    let oldEndIndex = oldChildren.length-1;
    let oldEndNode = oldChildren[oldEndIndex];
    
    let newStartIndex = 0;
    let newStartNode = newChildren[0];
    let newEndIndex = newChildren.length-1;
    let newEndNode = newChildren[newEndIndex];

    let oldKeyToIndexMap = createKeyToIndexMap(oldVnode.children);

    while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){if(!oldStartNode){
            // 如果此节点不存在,间接下移
            oldStartNode = oldChildren[++oldStartIndex];
        }else if(!oldEndNode){oldEndNode = oldChildren[--oldEndIndex];

        }else if(isSameVnode(oldStartNode,newStartNode)){
            // 旧头节点 = 新头节点 
            patch(oldStartNode,newStartNode);
            oldStartNode = oldChildren[++oldStartIndex];
            newStartNode = newChildren[++newStartIndex];
        }else if(isSameVnode(oldEndNode,newEndNode)){
            // 旧尾节点 = 新尾节点
            patch(oldEndNode,newEndNode);
            oldEndNode = oldChildren[--oldEndIndex];
            newEndNode = newChildren[--newEndIndex];
        }else if(isSameVnode(oldStartNode,newEndNode)){
            // 旧头节点 = 新尾节点
            patch(oldStartNode,newEndNode);
            parentDOMElement.insertBefore(oldStartNode.domElement,oldEndNode.domElement.nextSibling);
            oldStartNode = oldChildren[++oldStartIndex];
            newEndNode = newChildren[--newEndIndex];
        }else if(isSameVnode(oldEndNode,newStartNode)){
            // 旧尾节点 = 新头节点
            patch(oldEndNode,newStartNode);
            parentDOMElement.insertBefore(oldEndNode.domElement,oldStartNode.domElement);
            oldEndNode = oldChildren[--oldEndIndex];
            newStartNode = newChildren[++newStartIndex];

        }else{let oldIndexByKey = oldKeyToIndexMap[newStartNode.key];

            if(oldIndexByKey == null){
                // 新元素间接创立
                parentDOMElement.insertBefore(createDOMElementFromVnode(newStartNode),oldStartNode.domElement);
            }else{let oldVnodeToMove = oldChildren[oldIndexByKey];
                // console.log(oldKeyToIndexMap,oldIndexByKey,oldVnodeToMove, newStartNode);
                if(oldVnodeToMove.type === newStartNode.type){patch(oldVnodeToMove,newStartNode);
                    oldChildren[oldIndexByKey] = undefined;
                    oldKeyToIndexMap = createKeyToIndexMap(oldVnode.children); // 避免 key 雷同
                    parentDOMElement.insertBefore(oldVnodeToMove.domElement,oldStartNode.domElement);
                }else{
                    // key 雷同,type 不同,重建
                    parentDOMElement.insertBefore(createDOMElementFromVnode(newStartNode),oldStartNode.domElement);
                }
            }
            newStartNode = newChildren[++newStartIndex];

        }

    }
    // 解决残余的新节点
    if(oldStartIndex > oldEndIndex){let beforeDOMElement = newChildren[newStartIndex+1] ? newChildren[newStartIndex+1].domElement:null;
        for(let i = newStartIndex; i<= newEndIndex;i++){parentDOMElement.insertBefore(createDOMElementFromVnode(newChildren[i]),beforeDOMElement);
        }
    }

    // 删除残余的旧节点
    if(newStartIndex > newEndIndex){for(let i = oldStartIndex; i<= oldEndIndex;i++){parentDOMElement.removeChild(oldChildren[i].domElement)
        }
    }


}
// 获取 key 对应的地位 index
function createKeyToIndexMap(children) {let map = {};
    for (let i = 0; i < children.length; i++) {if (children[i] && children[i].key) {map[children[i].key] = i;
        }
    }
    return map;
}


// 是否是雷同的节点 类型雷同并且 key 雷同 key 可能为 null
function isSameVnode(oldVnode, newVnode) {return oldVnode.key === newVnode.key && oldVnode.type === newVnode.type;}

正文完
 0