最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 「大白话」虚拟DOM和DOM diff

    正文概述 掘金(SwordQiu)   2020-12-13   610

    前言

    程序员向来推崇简单好用的设计思想,这也催生了各种优秀框架的繁荣,我们能够享受到开箱即用的便利都来源于程序员们不屑的努力。

    现如今谈及前端框架都对react和vue如雷贯耳,由于这些优秀好用框架的盛行,它们共同基于虚拟DOM来渲染浏览器的思想成为目前前端领域中比较重要的知识,这部分知识聚集了顶尖大神们共同思考的智慧,在这样的背景下,虚拟DOM和DOM diff算法就成为面试常考题。

    刚开始听到虚拟DOM时,我一直觉得这是个高大上的名词(前端领域总是有各种各样高大上的名词,其实互联网都有这个毛病),直到接触后才知道原来这是非常基础的东西通过各种方法变身,我不由得更加敬佩框架开创者们的聪明脑袋。

    话不多说,我们直接来谈谈什么是虚拟dom,它有什么优缺点

    虚拟DOM是什么

    虚拟DOM是一个能代表 DOM 树的对象,通常含有标签名、标签上的属性、事件监听和子元素们,以及其他属性

    我们直接看react的虚拟dom的真实呈现

    const vNode={
       key:null,
       props:{ 
         children:[
         {type: 'span', ...},{type: 'span', ...}//子元素们
         ]
         className:'red',//标签属性
         onClick:()=>{} //事件
       },
       ref:null,
       type:'div', //标签名
       ...
    }
    

    再来看一段Vue的

    const vNode = {
      tag: "div", // 标签名 or 组件名
      data: {
        class: "red", // 标签上的属性
        on: {
          click: () => {} // 事件
        }
      },
      children: [ // 子元素们
        { tag: "span", ... },
        { tag: "span", ... }
      ],
      ...
    }
    

    可以看得出来,虚拟DOM是一个JS对象,里面有各种数据解构,Vue跟React的虚拟DOM几乎一样。

    创建虚拟DOM

    上面的虚拟dom要怎样来创建出来呢?

    react使用了一个叫creatElement的函数,它是这样写的

    createElement('div',{className:'red',onClick:()=> {}},[
        createElement('span', {}, 'span1'),
        createElement('span', {}, 'span2')
      ]
    )
    

    如果真的要这样写代码,那可真的丑的没边了,于是大神们想到使用编译器来编译上面的代码,让它变得更简单,于是就有了JSX

    <div className="red" onClick={fn}>
        <span>span1</span>
        <span>span2</span>
    </div>
    

    上面的JSX会被babel编译,最后成为原始createlement的形式。

    创建了虚拟DOM,怎样渲染呢

    回顾一下dom是怎样的呢?

    <div class='dom'>
      <ul>
        <li></li>
        <li></li>
      <ul/>
    </div>
    

    上面就是一个dom树,那么我们来结合react的虚拟dom来分析一下dom结构树。

    首先,上面的dom中,外层的是一个div的type,class(className)为dom,里面有个children,type是ul...

    如果用js对象来表示呢?

    {
       type:'div', //标签名
       props:{ 
         children:[
           {type: 'ul', 
              props:{
                 children:[{type:'li'..}]}
           }//子元素们
         ]
         className:'dom',//标签属性
       }, 
       ...
    }
    

    上面的代码我们需要注意的点是标签名typechildrenclassName,现在我们好像知道了要插入的节点信息,是不是可以尝试写一个函数复用?

    function render(type,className,children){
       let node=document.createNode(type);//创建外层div
       node.style.className=className //设置外层div的className
       let childNode=children.map((c)=>{
         return render(c.type,c.className,c.children)
         ...
       }) //遍历它的children属性
       node.append(childNode) //最后插入到node节点中
       return node
    }
    document.querySelector('#root').append(render('div','dom',[]))
    //最后把整个创建的node插入到浏览器已有的根节点(id为root)中
    

    但是因为元素的内容太多了,我们的参数也会很多,所以我们就使用一个对象来包裹所有跟元素相关的属性,并且把要root节点也放到函数参数内,这样就只需要传入两个参数,一个是JS对象(包含要插入节点的信息),一个是需要插入的节点。

    这个函数是这样调用的。

    render({type:'div',className:'dom'...},rootNode)
    

    再来对比一下React渲染函数

    //JSX
    const App:FC=()=>{ 
       return <div className='demo'>这是一个简单的demo</div>
       // 翻译成return createElement('div',{className:'demo'},...)
       //createElement会返回JS对象---虚拟dom
    }
    
    ReactDOM.render(
        <App /> 
        //这里的App就是执行App() 获得一个JS对象---虚拟dom
      ,
      document.getElementById("root")
    );
    // render函数执行后的参数是虚拟dom对象和要插入在哪个节点内
    //render是渲染用的函数
    

    那么我们可以得出,JSX语法是用来得到一个JS对象的,这个JS对象就是虚拟DOM,最后的这个JS对象会随着ReactDOM.render变成被渲染成浏览器节点

    虚拟DOM更快?

    江湖流言,虚拟DOM比普通DOM操作更快,很多前端都深信不疑,但是存在一个逻辑问题,虚拟DOM是用来对DOM进行操作(增删改查)的,怎么就会比原生DOM操作要快呢?

    而且各大浏览器厂商也不傻,如果真的存在这种情况,那么都使用虚拟DOM来优化渲染引擎不是更棒的方案吗?为什么浏览器厂商不用呢,显然虚拟DOM更快是一种片面理解。

    结合这个问题,我们来谈一谈虚拟DOM的优点--结合DOM diff算法

    虚拟DOM的优点

    • 1、减少次数

    例如添加节点,我们可以通过innerHTML或者append插入,但是实践证明,假设插入1000个节点,使用append方式和innerHTML字符串的方式效率并不高,那么虚拟DOM就是通过把节点情况都存入数组,然后再一次性渲染出来,这样能够有效将原本的1000次合并成一次,在减少次数的情况下,浏览器渲染效率提高了,优化变好了。

    var arr = [];
    var a = '<a href="javascript=;">添加</a>';
    for (var j = 0; j < 100; j++) {
       arr.push(a); //把a都丢入数组中
    	}
    div.innerHTML = arr.join('') //把数组变成字符串 然后让innerHtml获取,得到添加元素的效果
    

    使用数组的方式可以减少开辟字符串空间,上面的代码就是使用数组的方式增加100次子节点。

    • 2、减少范围

    DOMdiff算法可以把多余的操作省略掉,比如已经在页面中渲染的,那么DOMdiff不回将他重复执行渲染,而是把多出来的部分给渲染出来

    • 3、跨平台

    虚拟DOM支持小程序、IOS应用、安卓应用等多平台,因为本身它就是一个JS对象,自然支持各依赖环境平台的语法。

    虚拟DOM的缺点

    需要额外创建函数,也就是上面所说的很难写的createElement或者Vueh函数,不过这种方式已经被克服,React采用JSX语法,而Vue则通过模板语法。

    //JSX
    const App:FC=()=>{ 
       return <div className='demo'>这是一个简单的demo</div>
       // 翻译成return createElement('div',{className:'dom'},...)
    }
    
    ReactDOM.render(
        <App /> 
        //这里的App就是执行App() 获得一个JS对象---虚拟dom
      ,
      document.getElementById("root")
    );
    // render函数执行后的参数是虚拟dom对象和要插入在哪个节点内
    //render是渲染用的函数
    

    DOM diff是什么

    DOM diff算法就是针对虚拟DOM的不同做的对比算法。

    它本质上就是一个函数,我们称之为patch函数。它有两个参数,分别是旧的虚拟DOM和新的虚拟DOM

    patches=patch(oldVnode,newVnode)
    

    pathes就是调用patch函数后产生的不同的比较结果,这个结果会用来做DOM操作,它长得类似这个

    [
      {type: 'INSERT', vNode: ... },
      {type: 'TEXT',  vNode: ... },
      {type: 'PROPS', propsPatch: [...]}
    ]
    

    它是如何做比较的呢,来看下面的一段代码

    <div :class="x">
        <span v-if="y">{string1}</span>
        <span>{string2}</span>
    </div>
    

    它的数结构可以画图表示「大白话」虚拟DOM和DOM diff

    在这个树上,所有结构都是可以变化的,包括textNode,现在我们假设divclass变成red,于是整个patch函数就会开始遍历,遍历时,会对整个遍历结果进行记录。

    上面的比较结果会变成下面的形式 「大白话」虚拟DOM和DOM diff

    下面我们再变化,将第一个span删除。 「大白话」虚拟DOM和DOM diff 跟我们的预期不一样,它会认为是第一个span更新了,第二个span删除了。这是因为Vnodechildren是一个数组。

    const arr=[1,2,3]
    arr.splice(1,1) //删除2
    arr // [1,3]
    

    diff算法会认为上面的操作是删除了第三个数,把第二个数2改成了3。

    那么有什么办法可以让diff算法也知道人类的思想呢?答案就是使用key(react跟vue都需要使用单独的key,这个key在同属于一个父节点的各兄弟节点上需要不同。)

    //jsx语法
    <div>
    {[{id:1,value:1},{id:2,value:2},{id:3,value:3}].map((v)=>{
       return <span key={v.id}/>
    })}
    </div>
    

    有了这个keydiff算法就可以知道原来我们删的是这个div

    key解决了什么问题

    codesandbox.io/s/vue-templ… 打开上面的链接,我们依次进行操作

    一、把文本框的内容都修改成这样 「大白话」虚拟DOM和DOM diff

    二、删除第二个,我们预想的结果 「大白话」虚拟DOM和DOM diff

    三、实际的结果 「大白话」虚拟DOM和DOM diff

    为什么会出现这样的情况?这是因为diff算法认为我们是把第三个删除了,把第二个文本框里面的value给修改了。

    当我们使用一个不同的key绑定在各个节点上时,此问题就会解除,因为diff知道了我们人类的意图。这也是key为什么会存在于vDOM上的原因。

    解决方法: codesandbox.io/s/vue-templ…

    Vue官方文档解释 cn.vuejs.org/v2/api/#key

    总结

    虚拟DOM是什么?虚拟DOM就是一个能代表 DOM 树的对象,通常含有标签名、标签上的属性、事件监听和子元素们,以及其他属性

    虚拟DOM的优点是减少不必要的操作(减少范围),减少DOM操作的次数

    虚拟DOM的缺点React和Vue分别采用各自的语法以弥补其缺陷

    Dom diff就是为了对DOM进行动态化操作所做的算法处理,其主要是通过旧的虚拟dom跟新的虚拟dom做层级对比,进行挨个对比之后,形成一套优化的算法,这种算法的好处是无需我们关心内层,只需识别我们对数据的操作,再返回一个patch对象,通过这个对象来进行DOM操作,减少不必要的性能耗时,例如避免重复操作等。

    我们还应当关注虚拟DOM的key问题,在写代码时,最好使用不一样的key来避免一些bug,这个key最好不要是index,而是一个不重复id。


    下载网 » 「大白话」虚拟DOM和DOM diff

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元