zoukankan      html  css  js  c++  java
  • 虚拟Dom详解

    随着VueReact的风声水起,伴随着诸多框架的成长,虚拟DOM渐渐成了我们经常议论和讨论的话题。什么是虚拟DOM,虚拟DOM是如何渲染的,那么Vue的虚拟DomReact的虚拟DOM到底有什么区别等等等...一系列的话题都在不断的讨论中。为此也做了一些学习简单的侃一侃虚拟DOM到底是什么?

    虚拟Dom详解 - (二)

    什么是虚拟Dom

    虚拟Dom首次产生是React框架最先提出和使用的,其卓越的性能很快得到广大开发者的认可,继React之后vue2.0也在其核心引入了虚拟DOM的概念。在没有虚拟DOM的时候,我们在创建页面的时候一般都是使用HTML标签一个一个的去搭建我们的页面,既然有了DOM节点以后,为什么不直接使用原生DOM,那么原生DOM到底有什么弊端呢?原因是这个样子的,原生DOM中一个Node节点有N多的属性,一旦对DOM进行操作的时候会影响页面性能的核心问题主要在于DOM操作导致了页面的重绘或重排,为了减少由于重绘和重排对网页性能的影响,所以无论在什么项目中尽可能少的去操作DOM节点是性能优化的一大重点。

    所谓的虚拟DOM到底是什么?也就是通过JavaScript语言来描述一段HTML代码。其实使用JavaScript描述一段HTML代码是很简单的:

    HTML:

    <div class="" id="app">
      <p class="text">节点一</p>
    </div>
    

    JavaScript:

    const createElement = () => {
      return {
        "tag":"div",
        "prop":{
          "id":"app"
        },
        "children":[
          {
            "tag":"p",
            "prop":{
              "class":"text"
            },
            "children":["节点一"]
          }
        ]
      }
    }
    

    上面的代码中,只是简单的使用了JavaScript语言简单描述了一下HTML部分相对应的代码,此时我们只需要再写入一个创建DOM的方法,按照文档描述将创建好的DOM按照层级添加到里面页面中就好了。

    上述JavaScript中所描述的数据类型也就可以简单的理解为是虚拟DOM,虽然这个虚拟DOM是那么的简陋,但是足可以说明情况啦,像VueReact当需要对页面进行渲染更新的时候,则是对比的就是虚拟DOM更新前后的差异只对有差异的部分进行更新,大大减少了对DOM的操作。这里也就是我们经常所说的DIFF算法。

    通过上述描述可以总结得出,由于原生DOM节点中的属性和方法过于复杂,操作时过于影响性能,所以使用Object来描述页面中的HTML结构,以达到对性能的提升。

    如何创建虚拟DOM

    如果熟悉VueReact的朋友可能会知道一点,首先说下Vue,在使用中Vue中的虚拟DOM是使用template完成的,也就是平时我们项目中书写最多的模板,Vue通过vue-loader对其进行编译处理最后形成我们所需要的虚拟DOM,然而在React中则是不是这样的,React是没有template的,React则是使用的是JSX对进行编译,最后产生虚拟DOM,无论是Vue还是React最终的想要得到的就是虚拟DOM

    若想要知道虚拟DOM是如何创建的,那么就可简单的实现一下其创建过程,在上面中可以得到一个描述DOM节点的数据文本,我们可以根据其需要对其进行创建:

    const vnodeTypes = {
      //  HTML节点类型
      "HTML":"HTML",
      //  文本类型
      "TEXT":"TEXT",
      //  组件类型
      "COMPONENT":"COMPONENT"
    };
    const childTeyps = {
      //  为空
      "EMPTY":"EMPTY",
      //  单个
      "SINGLE":"SINGLE",
      //  多个
      "MULTIPLE":"MULTIPLE"
    };
    //  新建虚拟DOM
    //    所需创建标签名称
    //    标签属性
    //    标签子元素
    function createElement (tag,data,children = null){
      //  当前元素的标签类型
      let flag;
      //  子元素的标签类型
      let childrenFlag;
      if(typeof tag === "string"){
        //  如果是文本的则认为是,普通的HTML标签
        //  将其元素的flag设置成HTML类型
        flag = vnodeTypes.HTML;
      }else if(typeof tag === "function"){
        //  如果为函数,则认为其为组件
        flag = vnodeTypes.COMPONENT;
      }
      else {
        //  否则是文本类型
        flag = vnodeTypes.TEXT;
      };
      //  判断子元素情况
      if(children === null){
        //  如果 children 为空
        //  则子元素类型为空
        childrenFlag = childTeyps.EMPTY;
      }else if (Array.isArray(children)){
        //  如果 children 为数组
        //  获取子元素长度
        let len = children.length;
        //  如果长度存在
        if(len){
          //  则设置子元素类型为多个
          childrenFlag = childTeyps.MULTIPLE;
        }else{
          //  否则设置为空
          childrenFlag = childTeyps.EMPTY;
        }
      }else {
        //  如果存在并且不为空
        //  则设置为单个
        childrenFlag = childTeyps.SINGLE;
        //  创建文本类型方法,并将 children 的值转为字符串
        children = createTextVNode(children+"");
      }
    
      //  返回虚拟DOM
      return {
        flag, //  虚拟DOM类型
        tag,  //  标签
        data, //  虚拟DOM属性
        children, //  虚拟DOM子节点
        childrenFlag,  //  虚拟DOM子节点类型
        el:null   //  挂载元素的父级
      };
    };
    
    //  新建文本类型虚拟DOM
    function createTextVNode (text){
      return {
        //  节点类型设置为文本
        flag:vnodeTypes.TEXT,
        //  设置为没有标签
        tag:null,
        //  没有任何属性
        data:null,
        //  子元素类型设置为单个
        childrenFlag:childTeyps.EMPTY
      };
    };
    

    通过上面的代码可以简单的实现对虚拟DOM的创建,可以通过调用createElement并传入用来描述虚拟DOM的对象,就可以打印出已经创建好的虚拟DOM节点:

    const VNODEData = [
        "div",
        {id:"test"},
        [
            createElement("p",{},"节点一")
        ]
    ];
    let div = createElement(...VNODEData);
    console.log(div);
    

    结果:

    {
    	"flag": "HTML",
    	"tag": "div",
    	"data": {
    		"id": "test"
    	},
    	"children": [{
    		"flag": "HTML",
    		"tag": "p",
    		"data": {},
    		"children": {
    			"flag": "TEXT",
    			"tag": null,
    			"data": null,
    			"childrenFlag": "EMPTY"
    		},
    		"childrenFlag": "SINGLE"
    	}],
    	"childrenFlag": "MULTIPLE"
    }
    

    通过上述方法打印出来的则是按照传入的描述虚拟DOM的对象,已经创建好了一个虚拟DOM树,是不是一件很神奇的事情,其实仔细看下代码也没有什么特别重要的逻辑,只是该变了数据结构而已(可以这样理解,但是不能对外这么说,很丢人的,哈哈)。

    既然虚拟DOM节点已经出来了,下一步就是如何渲染出虚拟DOM了,渲染虚拟DOM则需要一个特定的方法,在VueReact中会在HTML有一个idapp的真实DOM节点,最终渲染的时候被替换成了虚拟DOM节点生成的真是的DOM节点,接下来就按照这个思路继续实现一下,在VueReact都有render函数,这里也就同样使用这个名称进行命名了,在开始之前,首先要确认一点的是,无论是首次渲染还是更新都是通过render函数来完成的,所以要对其进行判断,其余的就不多赘述了。

    //  渲染虚拟DOM
    //    虚拟DOM节点树
    //    承载DOM节点的容器,父元素
    function render(vnode,container) {
      //  首次渲染
      mount(vnode,container);
    };
    //  首次渲染
    function mount (vnode,container){
      //  所需渲染标签类型
      let {flag} = vnode;
      //  如果是节点
      if(flag === vnodeTypes.HTML){
        //  调用创建节点方法
        mountMethod.mountElement(vnode,container);
      } //  如果是文本
      else if(flag === vnodeTypes.TEXT){
        //  调用创建文本方法
        mountMethod.mountText(vnode,container);
      };
    };
    //  创建各种元素的方法
    const mountMethod = {
      //  创建HTML元素方法
      mountElement(vnode,container){
        //  属性,标签名,子元素,子元素类型
        let {tag,children,childrenFlag} = vnode;
        //  创建的真实节点
        let dom = document.createElement(tag);
        //  在VNode中保存真实DOM节点
        vnode.el = dom;
        //  如果不为空,表示有子元素存在
        if(childrenFlag !== childTeyps.EMPTY){
          //  如果为单个元素
          if(childrenFlag === childTeyps.SINGLE){
            //  把子元素传入,并把当前创建的DOM节点以父元素传入
            //  其实就是要把children挂载到 当前创建的元素中
            mount(children,dom);
          } //  如果为多个元素
          else if(childrenFlag === childTeyps.MULTIPLE){
            //  循环子节点,并创建
            children.forEach((el) => mount(el,dom));
          };
        };
        //  添加元素节点
        container.appendChild(dom);
      },
      //  创建文本元素方法
      mountText(vnode,container){
        //  创建真实文本节点
        let dom = document.createTextNode(vnode.children);
        //  保存dom
        vnode.el = dom;
        //  添加元素
        container.appendChild(dom);
      }
    };
    

    通过上面的代码,就可完成真实DOM的渲染工作了,虽然但是这也只是完成了其中的一小部分而已。但是很多东西没有添加进去,比如动态添加style样式,给元素绑定样式,添加class等等等,一系列的问题都还没有解决,现在工作也只是简单的初始化而已。其实想要完成上述的功能也不是很难,要知道刚刚所说的所有东西都是添加到DOM节点上的,我们只需要在DOM节点上做文章就可以了,改进mountElement方法:

    const mountMethod = {
      //  创建HTML元素方法
      mountElement(vnode,container){
        //  属性,标签名,子元素,子元素类型
        let {data,tag,children,childrenFlag} = vnode;
        //  创建的真实节点
        let dom = document.createElement(tag);
        //  添加属性   (✪ω✪)更新了这里哦
        data && domAttributeMethod.addData(dom,data);
        //  在VNode中保存真实DOM节点
        vnode.el = dom;
        //  如果不为空,表示有子元素存在
        if(childrenFlag !== childTeyps.EMPTY){
          //  如果为单个元素
          if(childrenFlag === childTeyps.SINGLE){
            //  把子元素传入,并把当前创建的DOM节点以父元素传入
            //  其实就是要把children挂载到 当前创建的元素中
            mount(children,dom);
          } //  如果为多个元素
          else if(childrenFlag === childTeyps.MULTIPLE){
            //  循环子节点,并创建
            children.forEach((el) => mount(el,dom));
          };
        };
        //  添加元素节点
        container.appendChild(dom);
      }
    };
    //  dom添加属性方法
    const domAttributeMethod = {
      addData (dom,data){
        //  挂载属性
        for(let key in data){
          //  dom节点,属性名,旧值(方便做更新),新值
          this.patchData(dom,key,null,data[key]);
        }
      },
      patchData (el,key,prv,next){
        switch(key){
          case "style":
            this.setStyle(el,key,prv,next);
            break;
          case "class":
            this.setClass(el,key,prv,next);
            break;
          default :
            this.defaultAttr(el,key,prv,next);
            break;
        }
      },
      setStyle(el,key,prv,next){
        for(let attr in next){
          el.style[attr] = next[attr];
        }
      },
      setClass(el,key,prv,next){
        el.setAttribute("class",next);
      },
      defaultAttr(el,key,prv,next){
        if(key[0] === "@"){
          this.addEvent(el,key,prv,next);
        }
        else {
          this.setAttribute(el,key,prv,next);
        }
      },
      addEvent(el,key,prv,next){
        if(next){
          el.addEventListener(key.slice(1),next);
        }
      },
      setAttribute(el,key,prv,next){
        el.setAttribute(key,next);
      }
    };
    

    以上就简单的实现了对虚拟DOM的创建以及属性的以及事件的挂载,算是有一个很大的跨越了,只是完成初始化是远远不够的,还需要对其进一步处理,so有时间的话会继续对虚拟`DOM`的更新进行说明。也就是其`DIFF`算法部分。单一职责,一篇博客只做一件事,哈哈

    总结

    虚拟DOM在目前流行的几大框架中都作为核心的一部分使用,可见其性能的高效,本文只是简单的做一个简单的剖析,说到头来其实虚拟DOM就是使用JavaScript对象来表示DOM树的信息和结构,这个JavaScript对象可以构建一个真正的DOM树。当状态变更的时候用修改后的新渲染的的JavaScript对象和旧的虚拟DOMJavaScript对象作对比,记录着两棵树的差异。把差别反映到真实的DOM结构上最后操作真正的DOM的时候只操作有差异的部分就可以了。

    下次再见,若有哪里有错误请大佬们及时指出,文章中若有错误请在评论区留言,我会尽快做出改正。

  • 相关阅读:
    Java对MongoDB的CRUD
    MongoDB数据库基本用法
    MySQL order by 排序结果不正确
    Linux shell 脚本 eq,ne,le,ge,lt,gt意义
    Linux shell 命令运行符 &,;,&& 区别
    Linux netstat 命令详解
    Linux ss 命令详解
    Linux sort 命令详解
    Linux sed 命令详解
    Linux xargs 命令详解
  • 原文地址:https://www.cnblogs.com/aaron---blog/p/11312820.html
Copyright © 2011-2022 走看看