Code前端首页关于Code前端联系我们

Virtual DOM 介绍及区分算法

terry 2年前 (2023-09-27) 阅读数 68 #数据结构与算法

什么是 Virtual Dom

我们知道我们正常的页面是由很多个 Dom 组成的,那么 Virtual Dom 到底是什么呢?简单来说,就是一个真正的dom节点。使用JavaScript来模拟一下这个,对比一下Js层的Dom变化。

以下是大家都应该熟悉的传统dom结。 ?属性,如果每个dom都有children,那么它在children中表现为一个数组,数组中的每一项都是另一个虚拟dom结构。

这里之所以使用Js来实现虚拟dom,是因为在前端领域,Js是唯一图灵完备的语言;所谓图灵完备的语言,是指能够进行复杂的逻辑运算,实现各种逻辑算法的语言。

为什么要使用虚拟DOM

有人可能会问DOM很好。当我们第一次学习前端的时候,肯定会接触到JQuery。 JQuery 是一个典型的操作 DOM 的框架工具库。我们使用 JQuery 来设计场景。我们解释了虚拟 dom 的用途和价值。

这个有一个需求场景

var data = [
      {
        name: '张三',
        age: '20',
        address: '杭州'
      },
      {
        name: '李四',
        age: '22',
        address: '北京'
      },
      {
        name: '隔壁老王',
        age: '24',
        address: "西溪水岸"
      }
    ]
复制代码

现在我们想把这个数据渲染成一个表格,点击页面上的一个按钮来替换部分数据。我们为此使用 Jquery。

  <div id="container"></div>
  <button id="btn-change">change</button>

  <script ></script>
  <script>
    var data = [
      {
        name: '张三',
        age: '20',
        address: '杭州'
      },
      {
        name: '李四',
        age: '22',
        address: '北京'
      },
      {
        name: '隔壁老王',
        age: '24',
        address: "西溪水岸"
      }
    ]

    function render(data) {
      var $container = $('#container')

      //清空现有内容
      $container.html('')

      // 拼接 table
      var $table = $('<table>')
      $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'))
      data.forEach(function (item) {
        $table.append($('<tr><td>'+ item.name +'</td><td>'+item.age+'</td><td>'+item.address+'</td></tr>'))
      })
      
      // 渲染到页面
      $container.append($table)
    }

    $('#btn-change').click(function () {
      data[1].age = 30
      data[2].address = '上海'
      render(data)
    })
    
    // 初始化时候渲染
    render(data)
复制代码

您可以看到我们已经替换了数据第二项年龄及其❓第三件衣服。点击更改按钮:虚拟DOM和Diff算法入门

vdom解决的问题从图中可以看到,我们只更改了表中的部分数据,但是table的所有节点都闪烁了,说明整个桌子又被更换了。

这个常识性的 JQuery 操作对于网页来说可能是一个巨大的实时性能杀手。由于这会更改 DOM 中不需要更改的节点,因此如果您仍然不确定这有多严重,可以继续阅读。

下面的代码非常容易使用。创建一个空的div标签,检查里面的属性并打印出来

    var div = document.createElement('div')
    var item ,result = ''
    for (item in div) {
      result += ' | ' + item
    }
    console.log(result)
复制代码
虚拟DOM和Diff算法入门

密密麻麻的属性,别说只是一级属性,可想而知管理起来是多么耗时直接 DOM 。 DOM 工作很耗时,但 JS 作为一种语言工作得非常快。我们在Js层做DOM对比的时候,应该尽量减少不必要的DOM。我们的效率大大提高,而不是每次都翻新一切。 vdom可以完美解决这个问题。

如何使用虚拟域

要了解如何使用vdom,我们可以通过现有的vdom应用库来了解其API,进而了解如何在开发中使用vdom。

这里我们选择Vue2中使用的虚拟域库snapdom。下图是其github主页的截图示例: 虚拟DOM和Diff算法入门

仔细观察,我们发现这个snapdom官方案例的主要内容是两个函数——h函数和函数

h函数

可以看到函数h有3个参数例如第一个h函数生成的vnode是div,点击事件绑定 someFn,没有 。 时尚地,sapn是一个文本节点它是粗体,第二个孩子直接是一个文本节点。三个孩子与herfa联系。布局函数

patch分为两种情况

  • 第一种是第一次渲染抛出patch容器❓❓❓空容器。
       var vnode = h('ul#list',{},[
        h('li.item',{},'大冰哥'),
        h('li.item',{},'伦哥'),
        h('li.item',{},'阿孔')
      ])
    
      patch(container, vnode) // vnode 将 container 节点替换
    复制代码

第一次渲染时,生成的vnode会被扔进一个空容器中。你可以将它与以前的Jquery进行比较。第一次渲染表格时,将表格的html添加到容器中

  • 第二次是更新节点时newVnode替换为旧的Vnode,此处补丁与之前的vnode并且只改变了改变的部分,未改变的部分保持不变。核心就是这里包含的diff算法虚拟DOM和Diff算法入门

    与之前的JQuery相比我们可以清楚地看到。要替换整个页面 dom,请使用 vdom 函数 pathc 只对我们相对较旧的 vnode 进行更改,而保留未更改的部分不变(如页面闪烁所示)

    使用 vdom 之前的 Jq -重做

    vdom核心apihh和h功能的案例。我们已经有了基本的了解。为了巩固对此的理解,我们继续下来用snabbdom重做我们之前的JQuery案例

    直接从代码开始

    <div id="container"></div>
      <button id="btn-change">change</button>
      <script ></script>
      <script ></script>
      <script ></script>
      <script ></script>
      <script ></script>
      <script ></script>
      <script>
        let container = document.getElementById('container')
        let btn = document.getElementById('btn-change')
        let snabbdom = window.snabbdom
        let patch = snabbdom.init([
          snabbdom_class,
          snabbdom_props,
          snabbdom_style,
          snabbdom_eventlisteners
        ])
        let h = snabbdom.h
        let data = [
          {
            name: '张三',
            age: '20',
            address: '杭州'
          },
          {
            name: '李四',
            age: '22',
            address: '北京'
          },
          {
            name: '隔壁老王',
            age: '24',
            address: "西溪水岸"
          }
        ]
        data.unshift({
          name: '姓名',
          age: '年龄',
          address: '地址'
        })
        let vnode
        function render(data) {
          // 创建虚拟table节点 第三个参数,也就是虚拟table的孩子 应该是虚拟的 行节点
          let newVnode = h('table', {}, data.map(item => {
            let tds = [] // 列,作为虚拟行的子项
            let i
            for(i in item) {
              if (item.hasOwnProperty(i)) {
                tds.push(h('td', {}, item[i]+''))
              }
            }
            return h('tr', {}, tds) // 虚拟行节点的 孩子 应该是虚拟的 列节点
          }))
    
          if (vnode) {
            patch(vnode,newVnode)
          } else {
            // 初次渲染
            patch(container,newVnode)
          }
          vnode = newVnode
        }
    
        btn.addEventListener('click', function(){
          data[1].age = 30,
          data[3].name = '一个女孩',
          render(data)
        })
        
        // 初始化时候渲染
        render(data)
      </script>
    </body>
    复制代码

    代码有点长。其实内容就是我们前面讲的。代码主要做了以下几件事

    • 介绍snabbdom core文件,第一次加载时初始化h函数和patch函数
    • ,渲染本质其实是patch(container, newVnode)
    • 然后点击change生成新的vnode,然后patch(vnode ,newVnode)

    函数render

    这里重点解释参数
    const list = [
        {
            id: 1,
            name: 'test1',
        },
        {
            id: 2,
            name: 'test2',
        },
        {
            id: 3,
            name: 'test3',
        },
        {
            id: 4,
            name: '添加到最后的一条数据',
        },
    ]
    复制代码

    ,当生成第三个参数newV时,第三个表的孩子是行节点。

  • tr线路节点也是v节点。当它们恢复时,它们还使用功能h。第三个参数是列td。 vnode
  • td列中的第三个vnode参数直接是文本节点。迭代每个项目并将其推入 tds 数组。
    虚拟DOM和Diff算法入门

到这里,大家应该对vdom有了一个大概的了解了。其实,与其说vdom快,不如说相对于覆盖dom的Jquer,不保证慢

总结

vdom核心api

  • h('tag) name' , 'property', [子元素])
  • h('tagname', 'property', 'text')
  • patch(container, vnode)
  • patch(oldVnode, newVnode) Diff算法简介介绍

    什么是 diff 算法

    在日常工作中,我们其实很多时候都会用到 diff 算法

    比如你在提交代码到 git 的时候会用到git♾❀ diff 或者一些代码对比的时候会用到它网上找的工具,我们虚拟域的核心是diff-算法。正如我们之前所说,在需要更新的节点上查找更新,而不更新不需要的节点。移动。其核心是如何找出哪些已更新,哪些未更新。需要 diff 算法来完成这个过程。与算法的差异。在Snabbdom中,差异主要体现在补丁。我们先看一下两种情况patch(container, vnode)patch(vnode, newVnode)

    ,所以空间有限(实际上空间有限)。我在这里简单解释一下,因为完成的diff算法的东西太多了。有兴趣的可以查看snabbdom的源码

    patch(container, vnode)

    虚拟DOM和Diff算法入门我们知道打补丁的过程就是在一个空容器中添加一个vnode(vdom)来创建一个真正的dom。基本代码流程如下:

    function creatElement(vnode) {
      let tag = vnode.tag
      let attrs = vnode.attrs || {}
      let children = vnode.children || []
      // 无标签 直接跳出
      if (!tag) {
        return null
      }
      // 创建元素
      let elem = document.createElement(tag)
      // 添加属性
      for(let attrName in attrs) {
        if (attrs.hasOwnProperty(attrName)) {
          elem.setAttribute(arrtName, arrts[attrName])
        }
      }
      // 递归创建子元素
      children.forEach((childVnode) => {
        elem.appendChild(createElement(childVnode))
      })
    
      return elem
    }
    复制代码

    简化代码 非常简单,任何人都可以理解。重要的一点是本身递归调用子节点。终止条件是tagnull

    patch(vnode, newVnode)是比较过程。这里我们只模拟最简单的场景

    虚拟DOM和Diff算法入门第三项变化,添加第四项

    // 简化流程 假设跟标签相同的两个虚拟dom
    function updateChildren (vnode, newVnode) {
      let children = vnode.children || []
      let newChildren = newVnode.children || []
    
      // 遍历现有的孩子
      children.forEach((oldChild, index) => {
        let newChild = newChildren[index]
        if (newChild === null) {
          return
        }
        // 两者tag一样,值得比较
        if (oldChild.tag === newChild.tag) {
          // 递归继续比较子项
          updateChildren(oldchild, newChild)
        } else {
          // 两者tag不一样
          replaceNode(oldChild, newChild)
        }
      })
    }
    复制代码

    这里Point也是递归的。这里我们简单的使用标签来设置更新条件。事实上,事情比这复杂得多;而函数replace的实际作用是 newVnode 生成的真实dom替换旧dom。涉及到的本地操作比较多,就不详细说了。

    现在大家应该已经了解差异的基本概念了。同样,为了便于理解,diff 算法过程被大大简化。实际的微分算法的复杂度远高于上述,比如

    • 添加和删除节点
    • 重新组织和优化这个过程
    • 节点属性样式事件的变化等将算法优化为极端等

    如果您有兴趣,可以获取更多信息。

    总结

    本文包含的知识是一个介绍。通过阅读本文,让不了解virtual dom的同学对virtual dom有一个很好的了解,并对diff算法有一个大概的了解。如果能够达到这样的效果,我想这篇文章将会非常有价值。想要进一步了解virtual dom或者diff算法的同学可以阅读snabbdompatch.js源码来加深学习。

    Vue额外密钥

    在写文章的时候,遇到了绑定vue密钥的问题。在这里我利用这份热情以及 virtual dom 和 diff 算法来了解 Vue 的关键

    Vue。关键

    首先来自Vue官网的解释:

    当Vue.js使用❙ v-for更新渲染元素列表时,默认采用“适当重用”策略。当数据项的顺序更改时,Vue 不会移动 DOM 元素来匹配数据项的顺序,它只是重用那里的每个元素,并确保它渲染在特定索引处渲染的每个元素。

    这里的就地回收策略是回收未发生变化的元素,其余的必须按顺序重新定位。

    为了让 Vue 跟踪每个节点的身份,从而重用和重新排序现有元素,您必须为每个项目提供唯一的 key 属性。 key 的理想值是每个项目的唯一 ID。

    我们经常使用索引(即数组下标)作为key,但这实际上不是推荐的❙用法。请参阅以下示例:

    这是数组数据。 ,没有问题,因为 索引 收集了末尾 1

    。但如果插入的数据是插入到中间而不是末尾,

    const list = [
        {
            id: 1,
            name: 'test1',
        },
        {
            id: 4,
            name: '不甘落后跑到第二的的一条数据',
        }
        {
            id: 2,
            name: 'test2',
        },
        {
            id: 3,
            name: 'test3',
        },
    ]
    复制代码

    这时候就会出现这种情况:

    之前的数据                         之后的数据
    
    key: 0  index: 0 name: test1     key: 0  index: 0 name: test1
    key: 1  index: 1 name: test2     key: 1  index: 1 name: 不甘落后跑到第二的的一条数据
    key: 2  index: 2 name: test3     key: 2  index: 2 name: test2
                                     key: 3  index: 3 name: test3
    复制代码

    这样数据插入后,除了前面的部分数据可以被复用之外。 最后三块要重新渲染,这显然不是我们想要的结果。

    使用独特钥匙来改善:

    这次我们绑定钥匙钥匙钥匙钥匙 作为每个新 ID 的补充。 紧随其后的 ID 为 4 的数据 是新添加的,其他数据复用了之前的 dom,因为这里是通过唯一 key 关联的,不关联重排序。渲染。

    所以我们需要使用一个key来唯一标识每个节点。 Vue Diff算法可以正确识别这个节点,并找到正确的位置插入新节点。一句话key的作用主要是有效更新虚拟DOM

    灵魂画师上线:

    可以看到当我们的旧数据转换为新数据时[a,b,c,d] ] --> [a, e ,b,c,d]

    如果我们没有使用正确的key,除了a数据可以重用外,接下来的4个数据都必须重用 -渲染为

    。而如果使用输入正确的key时,只需要改变一件事,就是新的数据e,其他的按照箭头所示继续。 虚拟DOM和Diff算法入门

    作者:noobakong
    链接:https://juejin.im/post/5c4a76b4e51d4526e57da225
    来源:版权所有来源:掘金。商业转载请联系作者获取授权。非商业转载请注明出处。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门