正在尝试彻底搞懂diff算法...(一)

大家好,我是你们的阿达西,一个又菜又爱玩的前端菜鸟。今天开始尝试一些更深入的东西,因为我发现侃侃而谈一些东西比起"额,这个...,没太了解过" 更吸引人,不懂得话那样太窘迫了,尴尬的想钻进地缝里面。本篇文章主要是在深入阅读前辈们的diff理解,进行整理的。

你了解虚拟DOM吗?给我说说vue里面的diff

前面觉得这种问题还是比较深层次的,我应该还要晚点才能接触到,所以没有去了解过,但是后来发现被问的概率太大了,就促使我来搞一搞这个了。

为什么要有虚拟DOM

我们都知道浏览器在渲染的时候要进行一系列的操作:

  • 解析HTML生成DOM树
  • 解析CSS生成cssOM树
  • 合成生成渲染树
  • 进行样式计算
  • 布局
  • 绘制
  • 合成

每次修改DOM都会触发这些步骤的一个或多个步骤。我们都知道DOM操作是很耗性能的,因为会触发重绘和回流,以及遍历节点等。所以此时virtual DOM就"铛铛铛"的产生了。虚拟DOM其实就是真实DOM的抽象,通过js对象来模拟真实的DOM树。 例如:

js 复制代码
<div class="dd">
    <p>123</p>
</div>

虚拟dom就是:

js 复制代码
var Vnode = {
    tag: 'div',
    props:{
       className:"dd"
    },
    children: [
        { tag: 'p', text: '123' }
    ]
};

这样的 DOM 结构就称之为 虚拟 DOM (Virtual DOM)

它的表达方式就是把每一个标签都转为一个对象,这个对象可以有三个属性:tagpropschildren

  • tag:必选。就是标签。也可以是组件,或者函数
  • props:非必选。就是这个标签上的属性和方法
  • children:非必选。就是这个标签的内容或者子节点,如果是文本节点就是字符串,如果有子节点就是数组。换句话说 如果判断 children 是字符串的话,就表示一定是文本节点,这个节点肯定没有子元素

虚拟DOM它可以减少浏览器的重绘回流,只更新需要更新的部分,这就牵扯到了虚拟DOM的diff算法了。

diff算法

diff算法就是找出新旧两棵虚拟DOM树上的不同。但是我们知道两棵树找不同还是很复杂的,引用大佬的话就是

我们知道,两棵树做 diff,复杂度是 O(n^3) 的,因为每个节点都要去和另一棵树的全部节点对比一次,这就是 n 了,如果找到有变化的节点,执行插入、删除、修改也是 n 的复杂度。所有的节点都是这样,再乘以 n,所以是 O(n * n * n) 的复杂度。

作者:zxg_神说要有光

链接:juejin.cn/post/711417...

我当时就在想一个可能比较幼稚的问题,为什么不能直接每个同层次的节点进行比较,而是一个节点要比较另一棵树所有的节点,这不纯纯浪费吗?为什么会是这种计算方式呢?有没有大佬能解惑一下。

一个小疑惑,咱们继续回到上面,所以diff算法就有了这样的约定:只进行同层次的比较,相同就继续往下比,不同的话就直接替换掉。但是这里就有一点问题了:如果仅仅只是第一个不同后面的都相同,那还要全部替换,这不是纯纯浪费吗? 例如:

所以,就有了以下的几种diff:

简单diff(多节点diff)

就是让每个节点都有一个唯一的标识(这也就是我们的v-for为什么需要key的原因了)通过key来判断是不是存在这个节点,如果存在的话就只需要移动节点就行了,不再需要重新创造个节点或者移除了。

它的核心就是

  1. 通过唯一标识判断是否存在这个这个节点,存在就表示可以复用,可能只需要移动即可。不存在就表示需要创建节点。
  2. 判断是否需要移动,就看该节点是不是在最后一个匹配到旧节点的位置,如果是的话就不需要移动,不是的话就需要对该节点进行移动了。
  3. 最后就是移除一些没有用到的结点
js 复制代码
const oldChildren = n1.children
const newChildren = n2.children

let lastIndex = 0

// 遍历新的 children
for (let i = 0; i < newChildren.length; i++) {
  const newVNode = newChildren[i]
  let j = 0
  let find = false

  // 遍历旧的 children
  for (j; j < oldChildren.length; j++) {
    const oldVNode = oldChildren[j]

    // 如果找到了具有相同 key 值的两个节点,则调用 patch 函数更新
    if (newVNode.key === oldVNode.key) {
      find = true
      patch(oldVNode, newVNode, container)

      if (j < lastIndex) {
        // 需要移动
        const prevVNode = newChildren[i - 1]
        if (prevVNode) {
          const anchor = prevVNode.el.nextSibling
          insert(newVNode.el, container, anchor)
        }
      } else {
        // 更新 lastIndex
        lastIndex = j
      }
      break
    }
  }

  if (!find) {
    const prevVNode = newChildren[i - 1]
    let anchor = null
    if (prevVNode) {
      anchor = prevVNode.el.nextSibling
    } else {
      anchor = container.firstChild
    }
    patch(null, newVNode, container, anchor)
  }
}

// 遍历旧的节点
for (let i = 0; i < oldChildren.length; i++) {
  const oldVNode = oldChildren[i]

  // 拿着旧 VNode 去新 children 中寻找相同的节点
  const has = newChildren.find(
    vnode => vnode.key === oldVNode.key
  )

  if (!has) {
    // 如果没有找到相同的节点,则移除
    unmount(oldVNode)
  }
}

根据这个过程,ABCD要变成BCDA的,就要移动三次,这其实也是很费时间的,所以我们必须还要对其进行优化...

优化

本篇文章先着重弄清楚diff算法的一个原理,下篇文章再进行一些优化的学习,以及学习v2,v3,以及react的diff算的不同之处。以上是我通过前辈们对diff算法的理解在进行的一个自我的总结和归纳,感谢大家的观看!!!

相关推荐
BillKu1 小时前
js前端对时间进行格式处理
前端
小满zs2 小时前
React-router v7 第三章(路由)
前端·react.js
Kx…………6 小时前
Day2:前端项目uniapp壁纸实战
前端·学习·uni-app·实战·项目
gqkmiss6 小时前
Git Cherry-pick:核心命令、实践详解
前端·git·前端框架·commit·cherry-pick
不想上班只想要钱6 小时前
vue面试题
前端·javascript·vue.js
拉不动的猪7 小时前
简单回顾下es6增数组方法
前端·javascript·面试
Alkaid:7 小时前
解决Long类型前端精度丢失和正常传回后端问题
java·前端·javascript·vue.js
Code额7 小时前
Vue 框架组件间通信方式
前端·vue.js·前端框架
玖玖passion7 小时前
即时响应之道:深入浅出Server-Sent Events
前端
无名之逆8 小时前
[特殊字符] Hyperlane:Rust 高性能 HTTP 服务器库,开启 Web 服务新纪元!
java·服务器·开发语言·前端·网络·http·rust