zoukankan      html  css  js  c++  java
  • 虚拟dom与diff算法

    1.虚拟dom

    dom就是html文件里内容,一个页面由多个dom组成

    <ul class="lists">
        <li class="item">li1</li>
        <li class="item">li2</li>
    </ul>

    而对应的虚拟dom是

    tag: 'ul',
    attrs: {
        className: 'lists'
    },
    children: [
        {
            tag: 'li',
            attrs: {
                className: 'item'
            },
            children: ['li1']
        },
        {
            tag: 'li',
            attrs: {
                className: 'item'
            },
            children: ['li2']
        }
    ]

    tag表示标签名,attrs就是dom的属性,每个dom如果有children的话,就会在children中以数组的形式展示,数组的每一项就又是一个虚拟dom结构。

    为什么要使用虚拟dom呢?

    举个最简单的列子

    使用jq的时候,使用append插入函数

    要是后续改了某个值,要重新append.是整个dom发生的替换,并不是修改的那一项

    并且单单一个空白的div底下的标签就有那么多

    var div = document.createElement('div')
    var item,
        result = ''
    for (item in div) {
        result += ' | ' + item
    }
    console.log(result)

    密密麻麻的属性,更何况这还只是一级属性,可想而知直接操作dom的方式是有多么费时,dom操作是费时的,
    但是Js作为一门语言,运行速度是非常快的,我们如果在Js层做dom对比,尽量减少不必要的dom操作,而不是每一次都全部翻修,我们的效率就会大大增加。
    而vdom就可以完美解决这个问题。

    要了解如何使用vdom,我们可以借助现有的vdom实现库,来了解其API,进而了解如何将vdom运用于开发中。

    这里我们选择一个Vue2中使用的虚拟dom库 snabbdom

    主要有两个函数:

    h函数

    可以看到 h 函数,有三个参数

    • 标签选择器
    • 属性
    • 子节点

    比如说第一个h函数生成的vnode,就是一个ul标签,绑定了className为lists,

    第一个children为带有className的li,li里是一个文本节点li1,

    第一个children为带有className的li,li里是一个文本节点li2,

    patch函数

    patch 分为两种情况

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

    第一次patch渲染的时候,是将生成的vnode往空容器里丢 可以对比之前的Jquery第一次渲染表格的时候,将table html append到容器中去

    • 第二种是更新节点的时候,newVnodeoldVnode替换
      btn.addEventListener('click',function() {
        var newVnode = h('ul#list',{},[
          h('li.item',{},'大冰哥'),
          h('li.item',{},'伦哥'),
          h('li.item',{},'孔祥宇'),
          h('li.item',{},'小老弟'),
        ])
        patch(vnode, newVnode)
      })
      复制代码

    这里的patch就会将的vonde和之前的vnode进行比对,只修改改动的地方,没动的地方保持不变,这里的核心就是涉及的diff算法

    我们可以清楚的看到,相对于之前的JQuery整个页面dom全部替换的情况,用vdom的pathc函数只修改了我们相对老的vnode变动的地方,没改动的地方就没用动(从页面的闪烁可以看出来)

     

    总结

    vdom的核心api

    • h('标签名', '属性', [子元素])
    • h('标签名', '属性', '文本')
    • patch(container, vnode)  //初次
    • patch(oldVnode,newVnode) // 修改

    2.diff算法

    什么是diff算法

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

    比如你在git提交代码的时候使用的 git diff 命令,再或者是网上的一些代码比对工具如svn上的,vue的key后续会说

    而我们的虚拟dom,核心就是diff算法,我们前面讲过,找出有必要更新的节点更新,没有更新的节点就不要动。

    这其中的核心就是如何找出哪些更新哪些不更新,这个过程就需要diff算法来完成

    patch(container, vnode)

    这个patch的过程是将一个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)

    这个patch过程就是比较差异的过程,我们这里就只模拟最简单的场景

    // 简化流程 假设跟标签相同的两个虚拟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)
        }
      })
    }

    这里面的点就也递归,这里只是简单的拿tag来判断更新条件,其实实际的比这复杂很多很多;

    replace函数实际的操作就是将newVnode新生成的真实dom将老的dom替换掉,这里涉及更多的是原生dom操作,就不在赘述了。

    Vue 中的 key
    为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。理想的 key 值是每项都有的唯一 id。
    我们在使用的使用经常会使用index(即数组的下标)来作为key,但其实这是不推荐的一种使用方法
    要是出现如下情况:
    在第二条加了一条数据
    之前的数据                         之后的数据
    
    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

    这样一来,追加数据以后,除了第一条数据能够就地复用,后三条都要重新渲染,这显然不是我们想要的结果。

    所以我们需要使用key来给每个节点做一个唯一标识,Vue的Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点,所以一句话,key的作用主要是为了高效的更新虚拟DOM

    原文链接:https://juejin.cn/post/6844903767473651720#heading-5

  • 相关阅读:
    获取滚动条卷入高度以及获取内联和外联的方法
    async
    使一个div元素上下左右居中
    .NetCore/ .NetFramework 机制
    Asp.netCore 是用的Socket 吗?
    Asp.netCore 的Startup 不继承接口
    月球
    JWT
    虚数的作用
    C# mailKit 发邮件 简单代码
  • 原文地址:https://www.cnblogs.com/ssszjh/p/15697217.html
Copyright © 2011-2022 走看看