虚拟Dom详解 - (一)

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

虚拟Dom详解 - (二)node

什么是虚拟Dom

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

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

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算法。app

经过上述描述能够总结得出,因为原生DOM节点中的属性和方法过于复杂,操做时过于影响性能,因此使用Object来描述页面中的HTML结构,以达到对性能的提高。框架

如何建立虚拟DOM

若是熟悉VueReact的朋友可能会知道一点,首先说下Vue,在使用中Vue中的虚拟DOM是使用template完成的,也就是平时咱们项目中书写最多的模板,Vue经过vue-loader对其进行编译处理最后造成咱们所须要的虚拟DOM,然而在React中则是否是这样的,React是没有template的,React则是使用的是JSX对进行编译,最后产生虚拟DOM,不管是Vue仍是React最终的想要获得的就是虚拟DOMdom

若想要知道虚拟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,
    //  保存子节点内容
    children:text
  };
};

经过上面的代码能够简单的实现对虚拟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);
  }
};

最终使用:

const VNODEData = [
    "div",
    {id:"test"},
    [
      createElement("p",{
        key:1,
        style:{
          color:"red",
          background:"pink"
        }
      },"节点一"),
      createElement("p",{
        key:2,
        "@click":() => console.log("click me!!!")
      },"节点二"),
      createElement("p",{
        key:3,
        class:"active"
      },"节点三"),
      createElement("p",{key:4},"节点四"),
      createElement("p",{key:5},"节点五")
    ]
];
let VNODE = createElement(...VNODEData);
render(VNODE,document.getElementById("app"));

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

总结

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

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

相关文章
相关标签/搜索