随着Vue
和React
的风声水起,伴随着诸多框架的成长,虚拟DOM
渐渐成了咱们常常议论和讨论的话题。什么是虚拟DOM
,虚拟DOM
是如何渲染的,那么Vue
的虚拟Dom
和React
的虚拟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
是那么的简陋,可是足能够说明状况啦,像Vue
和React
当须要对页面进行渲染更新的时候,则是对比的就是虚拟DOM
更新先后的差别只对有差别的部分进行更新,大大减小了对DOM
的操做。这里也就是咱们常常所说的DIFF
算法。app
经过上述描述能够总结得出,因为原生DOM
节点中的属性和方法过于复杂,操做时过于影响性能,因此使用Object
来描述页面中的HTML
结构,以达到对性能的提高。框架
如何建立虚拟DOM
若是熟悉Vue
或React
的朋友可能会知道一点,首先说下Vue
,在使用中Vue
中的虚拟DOM
是使用template
完成的,也就是平时咱们项目中书写最多的模板,Vue
经过vue-loader
对其进行编译处理最后造成咱们所须要的虚拟DOM
,然而在React
中则是否是这样的,React
是没有template
的,React
则是使用的是JSX
对进行编译,最后产生虚拟DOM,不管是Vue
仍是React
最终的想要获得的就是虚拟DOM
。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, // 保存子节点内容 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
则须要一个特定的方法,在Vue
和React
中会在HTML
有一个id
为app
的真实DOM
节点,最终渲染的时候被替换成了虚拟DOM
节点生成的真是的DOM
节点,接下来就按照这个思路继续实现一下,在Vue
和React
都有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
对象和旧的虚拟DOM
的JavaScript
对象做对比,记录着两棵树的差别。把差异反映到真实的DOM
结构上最后操做真正的DOM
的时候只操做有差别的部分就能够了。
下次再见,如有哪里有错误请大佬们及时指出,文章中如有错误请在评论区留言,我会尽快作出改正。