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对应的地位indexfunction 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可能为nullfunction isSameVnode(oldVnode, newVnode) {    return oldVnode.key === newVnode.key && oldVnode.type === newVnode.type;}