虚拟DOM&& diff算法

95 阅读7分钟

react虚拟DOM的原理, 虚拟DOM是什么? 怎么理解虚拟DOM?

  1. 是什么? 顾名思义,虚拟DOM就是虚拟节点,使用js对象来模拟的DOM节点,存储在内存中,最终会根据这些虚拟的dom节点渲染出真实的DOM节点 本质上是一个Js对象

  2. 怎么做?

    js对象是如何模拟dom节点?

    比如我们使用jsx语法写出了一个列表

      <ul id="list">
        <li className="li">hello world</li>
      </ul>
    

    那么会被表示成一个如下的js对象

    vNode = {
        tageName: 'ul',
        props: {
            id: 'list
        },
        children: [
            {
                tagName: 'li',
                props: {
                    className: 'li',
                },
                children: ['hi']
            }
        ]
    }
    

    为什么写了这个标签就会被转换成这样的对象呢?

    因为jsx的语法会被转译成creaeElement函数调用,如下:

    React.createElement('ul', { id: 'list'}, 
      React.createElement('li', { class: 'li'}, 'hello world')
    )
    

    (这里是babel或者webpack的一些loader实现的)

    所以实际上,我们是最终通过creatElement这个方法来实现由标签转化成虚拟dom的。creatElement可以嵌套。

    如何将虚拟节点变成真实dom节点

    通过一个render方法

        function render(vdom) {
            //如果是字符串或者数字,创建一个文本节点
            if (typeof vdom === 'string' || typeof vdom === 'number') {
                return document.createTextNode(vdom);
            }
            
            //创建真实的dom
            const { tagName, props, children } = vdom;
            const element = document.createElement(tag);
    
            //设置该节点的属性
            setProps(element, props);
    
            //遍历子节点,并获取创建真实DOM,插入到当前节点
            children.map(render).forEach(element.appendChild.bind(element)); //递归的遍历过程
    
            //虚拟dom中缓存真实dom节点
            vdom.dom = element;
    
            //返回真实dom
            return element;
        }
    

    如果节点发生了变化,那么就会产生一个新的虚拟节点,但是我们不会把这个虚拟节点直接渲染到页面,而是经过diff算法得到一个patch(补丁)再更新到真实节点上。 这个过程会进行一个新旧虚拟dom的对比,然后得到一个最精简的dom的更改,这里可能就是一个局部的更改,比如就是更改某个文本而已,然后更新到真实dom上。

  3. 解决了什么问题 a. dom操作性能的问题,我们在以前用原生js或者jquery写代码的时候,会频繁地使用操作dom的方法,比如新增节点,删除节点等等,但是我们的写法可能性能很有问题,那么通过虚拟dom和diff算法,就可以减少很多不必要的dom操作,保证了性能不会太差。相当于规范了我们的操作,以最理想的方式去更新我们的dom。 (因为最终还是要操作真实的dom, 所以只能说保证让性能不太差,而不能保证一定不会有性能问题,但是可以让一些经验较少的程序员写出的代码性能不会有大问题)。

    b. 避免dom操作不方便的问题,因为要使用各种DOM的API, 需要记很多的API, 非常繁琐, 而现在我们只需要记住去setState,去更新数据就够了。

  4. 优点?(比其他的解决方案好在哪里?) a. 让react带来了跨平台的能力,因为虚拟节点除了可以渲染为真实的dom节点外,还可以渲染成其他的东西。就是拿到虚拟dom这个对象去渲染的时候,如果是在h5页面的话,div就被渲染成div, 如果在android平台的话,我们也可以把它渲染成android平台上的某个标签 view啊什么的。在ios上的话 就可以是其他的标签了。 所以说可以实现跨平台的能力。 b. 让dom操作的整体性能更改,减少不必要的dom操作,保证性能下线

  5. 缺点? 在性能要求极高的情况下还是要操作真实dom (不算什么缺点) react为虚拟dom创建了合成事件, react的事件并不是真实的dom事件, 所有react的事件都是绑定在了根元素上,那么所有的onClick其实并不是绑定在button或者div上面的,是直接绑定在根元素上的,自动实现了事件委托。 如果同时使用原生dom事件和合成事件,可能会出bug,比如在其中一个事件里面阻止冒泡了,但是另一个事件还是会触发,因为两个事件是不同的系统。 (这个是需要注意的。)

  6. 如何解决缺点? 不用react??

dom diff算法?

  1. 是什么? 就是用来对比两棵虚拟dom树的算法。当组件变化的时候,会render出一个新的虚拟DOM,diff算法对比新旧虚拟dom之后,得到一个patch, 然后react用patch来更新真实的dom。
  2. 怎么实现?

    a. 首先对比两棵树的根节点

     i. 如果根节点类型变了,比如div变成了span,那么直接认为整棵树都变了,不再进行子节点的对比。此时直接删除对应的真实DOM树,创建新的真实DOM树。
     ii. 如果根节点类型没有变,检查属性是否改变
         1. 没有变, 保留对应的真实节点
         2. 改变了, 更新改节点的属性,不重新创建节点 (如果是style属性发生了改变,react也只会更新改变的,比如style="margin:0; paring:0" 变成了style="margin:1px; padding: 0;", react 也只会更新改变的。将margin做更新 而不是重写整个style)
    

    b. 然后同时遍历两棵树的子节点,每个节点的对比过程同上。

    i. 情况一
     ```
     <ul>
         <li>a</li>
         <li>b</li>
     </ul>
     ```
     变成了
     ```
     <ul>
         <li>a</li>
         <li>b</li>
         <li>c</li>
     </ul>
     ```
     那么react会依次比较a-a b-b ,发现c是新增的,最终会创建真实的c节点插入到页面
     ii. 情况二
     ```
     <ul>
         <li>b</li>
         <li>c</li>
     </ul>
     ```
     变成
     ```
     <ul>
         <li>a</li>
         <li>b</li>
         <li>c</li>
     </ul>
     ```
     react 会先对比b - a 然后就删除了b文本,创建a文本, 然后对比c-b, 会删除c文本,创建b文本,最后发现多了一个c, 就会新创建c,添加到最后。
     (注意 不是变对比变删除新建的,而是会把操作汇总到patch里,再进行最终的DOM操作)
    
     其实你会发现只需要新增一个a ,保留b和c即可。 那为什么react做不到呢?
     因为需要你增加一个Key,这样react就可以做到了!
    
     ```
         <ul>
             <li key="b">b</li>
             <li key="c">c</li>
         </ul>
    
     ```
     变成
     ```
         <ul>
             <li key="a">a</li>
             <li key="b">b</li>
             <li key="c">c</li>
         </ul>
    
     ```
     加上key的节点,react会怎么对比呢?
     react先对比key,发现Key只新增了一个,那么久保留b和c,新增a

vue的dom diff算法源码分析之----双端交叉对比

重复下面的对比过程,直到两个数组中任一数组的头指针超过尾指针,循环结束

  1. 头头对比 对比两个数组的头部,如果找到,把新节点patch到旧节点,头指针后移
  2. 尾尾对比 对比两个数组的尾部,如果找到, 把新节点patch到旧节点,尾指针前移
  3. 旧尾新头对比 交叉对比,旧尾新头,如果找到,把新节点patch到旧节点,新尾指针前移,旧头指针后移
  4. 旧头新尾对比 交叉对比,旧头新尾, 如果找到, 把新节点patch到旧节点,新尾指针前移,旧头指针后移
  5. 利用key进行对比

react dom diff 与 vue 的dom diff 的区别

  1. react 是从左到右遍历的对比 vue 是双端交叉的对比
  2. vue 整体效率要比react更高, 举例说明:假设有N个子节点,我们只是要把最后子节点移到第一个,那么 a. react需要借助Map进行key搜索找到匹配项,然后复用节点 b. Vue会发现移动,直接复用节点