虚拟DOM学习与总结

虚拟DOM

  虚拟DOM简而言之就是,用JS去按照DOM结构来实现的树形结构对象,一般称之为虚拟节点(VNode)

        优点:解决浏览器性能问题 ,真实DOM频繁排版与重绘的效率是相当低的,虚拟DOM进行频繁修改,然后一次性比较并修改真实DOM中需要改的部分(注意!),最后并在真实DOM中进行排版与重绘,减少过多DOM节点排版与重绘损耗。

例子1:

<div>我是文本</div>
let VNode = {
    tag:'div',
    children:'我是文本'      
}

例子2:

<div class="container" style="color:yellow"></div>
let VNode = {
    tag:'div',
    data:{
        class:'container',
        style:{
            color:'yellow'
        }
    },
    children:''
}

例子3:

<div class="container"> 
    <h1 style="color:red">标题</h1> 
    <span style="color:grey">内容</span>
    <span></span>
<div> 
let VNode = { 
    tag: 'div', 
    data:{ 
        class:'container' 
    }, 
    children:[ 
        { 
            tag:'h1', 
            data:null, 
            children:{ 
                data: { 
                    style:{ 
                        color:'red' 
                    } 
                }, 
                children: '标题' 
            } 
        }, 
        { 
            tag:'span', 
            data:null, 
            children:{ 
                data: { 
                    style:{ 
                        color:'grey' 
                    } 
                }, 
                children: '内容' 
            } 
        },
        {
            tag:'span',
            data:null,
            children:''
        }
    ] 
}

 看完了例子,聪明的你一定知道了什么是虚拟dom。

 snabbdom

 先看一眼github上的例子

 snabbdom有几个核心函数,h函数,render函数和patch函数。

 h函数

 用于创建VNode(virtual node虚拟节点),追踪dom变化的。

 React中通过babel将JSX转换为h函数的形式,Vue中通过vue-loader将模板转换为h函数。

假设在vue中我们有如下模板

<template>
    <div>
        <h1></h1>
    </div>
</template>

用h函数来创建与之相符的VNode:

const VNode = h('div',null,h('h1'))

得到的VNode对象如下:

const VNode = { 
  tag: 'div', 
  data: null, 
  children: { 
    tag: 'span', 
    data: null, 
    children: null 
  } 
}

 什么是虚拟DOM的挂载

虚拟DOM挂载:将虚拟DOM转化为真实DOM的过程

主要用到如下原生属性或原生方法:

  • 创建标签:document.createElement(tag)

  • 创建文本:document.createTextNode(text);

  • 追加节点:parentElement.appendChild(element)

什么是虚拟DOM的更新

虚拟DOM更新:当节点对应得vnode发生改变时,比较新旧vnode的异同,从而更新真实的DOM节点。

let prevVNode = { 
    //... 
} 
let nextVNode = { 
    //... 
} 

//挂载 
render(prevVNode,container) 

//更新 
setTimeout(function(){ 
    render(nextVNode,container) 
},2000)

我们在更新的时候,又分为两种情况:

  1. prevVNode和nextVNode都有,执行比较操作

  2. 有prevVNode没有nextVNode,删除prevVNode对应的DOM即可

function render(vNode,container){ 
    const prevVNode = container.vNode; 
    //之前没有-挂载 
    if(prevVNode === null || prevVNode === undefined){ 
        if(vNode){ 
            mount(vNode,container); 
            container.vNode = vNode; 
        } 
    } 
    //之前有-更新 
    else{ 
        //之前有,现在也有 
        if(vNode){ 
            //比较 
        } 
        //以前有,现在没有,删除 
        else{ 
            //删除原有节点 
        } 
    } 
}

 render函数

 将VNode转化为真实DOM

 接收两个参数:

  • 虚拟节点
  • 挂载的容器
function render(VNode,container){ 
    //... 
}

 最终render代码

function render(vNode,container){ 
    const prevVNode = container.vNode; 
    //之前没有-挂载 
    if(prevVNode === null || prevVNode === undefined){ 
        if(vNode){ 
            mount(vNode,container); 
            container.vNode = vNode; 
        } 
    } 
    //之前有-更新 
    else{ 
        //之前有,现在也有 
        if(vNode){ 
            patch(prevVNode,vNode,container); 
            container.vNode = vNode; 
        } 
        //以前有,现在没有,删除 
        else{ 
            removeChild(container,prevVNode.el); 
            container.vNode = null; 
        } 
    } 
}

patch函数

 想了半天没想到怎么描述,我个人的理解就是,挂载更新,就是prevVNode 和 nextVNode 是如何进行对比的

我们现在将VNode只分为了两类:

  1. 元素节点

  2. 文本节点

那么 prevVNode 和 nextVNode 可能出现的情况只会有以下三种:

  1. 二者类型不同

  2. 二者都是文本节点

  3. 二者都是元素节点,且标签相同

当二者类型不同时,只需删除原节点,挂载新节点即可:

function patch (prevVNode, nextVNode, container) { 
    removeChild(container, prevVNode.el); 
    mount(nextVNode, container); 
}

当二者都是文本节点时,只需修改文本即可

function patch (prevVNode, nextVNode, container) { 
    const el = (nextVNode.el = prevVNode.el) 
    if(nextVNode.children !== prevVNode.children){ 
        el.nodeValue = nextVNode.children; 
    } 
}

当二者都是元素节点且标签相同时,此时比较麻烦,考虑是一个patchElement函数用于处理此种情况

function patch (prevVNode, nextVNode, container) { 
    patchElement(prevVNode, nextVNode, container) 
}

最终 patch 函数的代码如下:

function patch (prevVNode, nextVNode, container) { 
    // 类型不同,直接替换 
    if ((prevVNode.tag || nextVNode.tag) && prevVNode.tag !== nextVNode.tag) { 
        removeChild(container, prevVNode.el); 
        mount(nextVNode, container);  
    } 
    // 都是文本 
    else if(!prevVNode.tag && !nextVNode.tag){ 
        const el = (nextVNode.el = prevVNode.el) 
        if(nextVNode.children !== prevVNode.children){ 
            el.nodeValue = nextVNode.children; 
        } 
    } 
    // 都是相同类型的元素 
    else { 
        patchElement(prevVNode, nextVNode, container) 
    } 
}

比较相同tag的VNode(patchElement)

因为tag相同,所以patchElement函数的功能主要有两个:

  1. 检查prevVNode和nextVNode对应的元素属性是否一致(style、class、event等),不一致更新

  2. 比较prevVNode和nextVNode对应的子节点(children)

关于元素属性的比较与挂载阶段的逻辑基本一致,就不在此继续展开,我们主要考虑如何对子节点进行比较

子节点可能出现的情况有三种:

  1. 没有子节点

  2. 一个子节点

  3. 多个子节点

所以关于prevVNode和nextVNode子节点的比较,共有9种情况:

  1. 旧:单个子节点 && 新:单个子节点

  2. 旧:单个子节点 && 新:没有子节点

  3. 旧:单个子节点 && 新:多个子节点

  4. 旧:没有子节点 && 新:单个子节点

  5. 旧:没有子节点 && 新:没有子节点

  6. 旧:没有子节点 && 新:多个子节点

  7. 旧:多个子节点 && 新:单个子节点

  8. 旧:多个子节点 && 新:没有子节点

  9. 旧:多个子节点 && 新:多个子节点

前8中情况都比较简单,这里简单概括一下:

1.旧:单个子节点 && 新:单个子节点

都为单个子节点,递归调用patch函数

2.旧:单个子节点 && 新:没有子节点

删除旧子节点对应的DOM

3.旧:单个子节点 && 新:多个子节点

删除旧子节点对应的DOM,并将多个新子节点依次递归调用mount函数进行挂载即可

4.旧:没有子节点 && 新:单个子节点

直接调用mount函数疆新单个子节点进行挂载即可

5.旧:没有子节点 && 新:没有子节点

什么也不做

6.旧:没有子节点 && 新:多个子节点

将多个新子节点依次递归调用mount函数进行挂载即可

7.旧:多个子节点 && 新:单个子节点

删除多个旧子节点对应的DOM,递归调用mount函数对单个新子节点进行挂载即可

8.旧:多个子节点 && 新:没有子节点

删除多个旧子节点对应的DOM即可

9.旧:多个子节点 && 新:多个子节点

对于新旧子节点均为多个子节点的情况,是VNode更新阶段最复杂的情况,无论是React还是Vue都有不同的实现方案,这些实现方案也就是我们常说的Diff算法。

今天先不涉及比较复杂的Diff算法,关于Diff算法的内容,留到日后进行讲解,我们先通过最简单的方式来实现多个新旧子节点的更新(性能最差的做法)。

遍历旧的子节点,将其全部移除:

for (let i = 0; i < prevChildren.length; i++) { 
    removeChild(container,prevChildren[i].el) 
}

遍历新的子节点,将其全部挂载

for (let i = 0; i < nextChildren.length; i++) { 
    mount(nextChildren[i], container) 
}

最终的代码如下:

export const patchElement = function (prevVNode, nextVNode, container) { 

    const el = (nextVNode.el = prevVNode.el); 

    const prevData = prevVNode.data; 
    const nextData = nextVNode.data; 

    if (nextData) { 
        for (let key in nextData) { 
            let prevValue = prevData[key]; 
            let nextValue = nextData[key]; 
            patchData(el, key, prevValue, nextValue); 
        } 
    } 
    if (prevData) { 
        for (let key in prevData) { 
            let prevValue = prevData[key]; 
            if (prevValue && !nextData.hasOwnProperty(key)) { 
                patchData(el, key, prevValue, null); 
            } 
        } 
    } 
    //比较子节点 
    patchChildren( 
        prevVNode.children, 
        nextVNode.children, 
        el 
    ) 
} 


function patchChildren(prevChildren, nextChildren, container) { 
    //旧:单个子节点 
    if(prevChildren && !Array.isArray(prevChildren)){ 
        //新:单个子节点 
        if(nextChildren && !Array.isArray(nextChildren)){ 
            patch(prevChildren,nextChildren,container) 
        } 
        //新:没有子节点 
        else if(!nextChildren){ 
            removeChild(container,prevChildren.el) 
        } 
        //新:多个子节点 
        else{ 
            removeChild(container,prevChildren.el) 
            for(let i = 0; i<nextChildren.length; i++){ 
                mount(nextChildren[i], container) 
            } 
        } 
    } 
    //旧:没有子节点 
    else if(!prevChildren){ 
        //新:单个子节点 
        if(nextChildren && !Array.isArray(nextChildren)){ 
            mount(nextChildren, container)  
        } 
        //新:没有子节点 
        else if(!nextChildren){ 
            //什么都不做 
        } 
        //新:多个子节点 
        else{ 
            for (let i = 0; i < nextChildren.length; i++) { 
                mount(nextChildren[i], container) 
            } 
        } 
    } 
    //旧:多个子节点 
    else { 
        //新:单个子节点 
        if(nextChildren && !Array.isArray(nextChildren)){ 
            for(let i = 0; i<prevChildren.length; i++){ 
                removeChild(container,prevChildren[i].el) 
            } 
            mount(nextChildren,container)    
        } 
        //新:没有子节点 
        else if(!nextChildren){ 
            for(let i = 0; i<prevChildren.length; i++){ 
                removeChild(container,prevChildren[i].el) 
            } 
        } 
        //新:多个子节点 
        else{ 
            // 遍历旧的子节点,将其全部移除 
            for (let i = 0; i < prevChildren.length; i++) { 
                removeChild(container,prevChildren[i].el) 
            } 
            // 遍历新的子节点,将其全部添加 
            for (let i = 0; i < nextChildren.length; i++) { 
                mount(nextChildren[i], container) 
            }  
        } 
    } 

}

此文参考:

冰山工作室 http://www.bingshangroup.com/blog2/action2/jspool%EF%BC%9A%E9%99%88%E5%85%B6%E4%B8%B0/VNode2.html

原文地址:https://www.cnblogs.com/cqy1125/p/11661337.html