zoukankan      html  css  js  c++  java
  • 虚拟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

  • 相关阅读:
    天气预报FLEX版本
    关于“ORA01000: 超出打开游标的最大数”
    WIN7(x64) IIS7.5 404.17错误:请求的内容似乎是脚本,因而将无法由静态文件处理程序来处理。
    解决GDI+中“内存不足”问题
    Stack Overflow Exception
    清洁的Javascript
    设置SQL Server数据库中某些表为只读的多种方法
    程序员肿么了?为何总被认为是“屌丝”
    jquery datepicker 显示12个月份
    apache2.4配置虚拟主机随记
  • 原文地址:https://www.cnblogs.com/cqy1125/p/11661337.html
Copyright © 2011-2022 走看看