React 中,用到的几种浅比较方式及其比较成本科普

开发 前端
这篇文章主要给大家介绍几种 React 在 Diff 过程中用到的比较方式,以及当这几种方式大量执行时,执行所要花费的时间。

开发中的绝大多数时候,我们并不需要关注 React 项目的性能问题。虽然我们在前面几个章节中,也花了几篇文章来分析如何优化 React 的性能体验,但是这些知识点在开发过程中能用到的机会其实比较少。面试的时候用得比较多。

但是,当你的项目遇到性能瓶颈,如何优化性能就变得非常重要。当然,我们前面几篇文章已经把性能优化的方式和方法说得非常清晰了,大家可以回顾一下。这篇文章我们要分享的重点是,当我采用不同的方式优化之后,代码逻辑执行所要付出的代价到底如何。

例如,当我们得知 React 的 DIFF 是全量比较的时候,可能第一个反应就是觉得他性能差。但是具体性能差到什么程度呢?有没有一个具体的数据来支撑?不确定,这只是一种主观感受。优化之后的性能到底强不强呢,也不敢肯定。

因此,这篇文章主要给大家介绍几种 react 在 diff 过程中用到的比较方式,以及当这几种方式大量执行时,执行所要花费的时间。

一、对象直接比较

又称为全等比较,这是一种成本最低的比较方式。在 React 中,state 与 props 的比较都会用到这样的方式。

var prevProps = {}
var nextProps = {}

if (prevProps === nextProps) {
  ...
}

那么,这种比较方式的成本有多低呢?我们来写一个循环简单验证一下。分别看看比较一万次需要多长时间。

var markTime = performance.now()
var prev = {}
var next = {}
for(var i = 0; i <= 10000; i++) {
  if (prev === next) {
    console.log('相等')
  }
}
var endTime = performance.now()
console.log(endTime - markTime)

执行结果会有小范围波动,展示出来的结果都是取的多次执行的平均值,或者出现次数最多的执行结果。比如本案例执行,用时最短的是 0.3 ms,用时最长的是 0.8 ms。

可以看到,对比一万次,用时约  0.6ms。

对比一百万次,用时约 6.4ms。

通常情况下,我们项目的规模应该很难超过一万次,控制得好一点,一般都在 1000 次以内。多一点也应该在 5000 次以内,5000 次用这种方式的对比只需要 0.3ms 左右。

二、Object.is

Object.is 是一种与全等比较相似但不同的比较方式,他们的区别就在于处理带符号的 0 和 NaN 时结果不一样。

+0 === -0 // true
Object.is(+0, -0) // false

NaN === NaN // false
Object.is(NaN, NaN) // true

React 源码里为 Object.is 做了兼容处理,因此多了一点判断,所以他的性能上会比全等要差一些。

function is(x, y) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}

const objectIs =
  typeof Object.is === 'function' ? Object.is : is;

那么差多少呢?我们先写一个逻辑来看一下执行一万次比较需要多久。

function is(x, y) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}

const objectIs =
  typeof Object.is === 'function' ? Object.is : is;
var markTime = performance.now()
var prev = {}
var next = {}
for(var i = 0; i <= 10000; i++) {
  if (objectIs(prev, next)) {
    console.log('相等')
  }
}
var endTime = performance.now()
console.log(endTime - markTime)

执行结果如下,大概是 0.8ms。

执行一百万次,用时约 11.4ms。

那么我们的项目规模在 5000 次比较以内的话,用时估计在 0.4ms 左右,比全等比较多用了 0.1ms。

三、shallowEqual

这种浅比较的成本就稍微大一些,例如,当我们对子组件使用了 memo 包裹之后,那么在 diff 过程中,对于 props 的比较方式就会转变成这样方式,他们会遍历判断 props 第一层每一项子属性是否相等。

function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    const currentKey = keysA[i];
    if (
      !hasOwnProperty.call(objB, currentKey) ||
      !is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }

  return true;
}

首先,这种比较方式在 React 中出现的次数非常的少,只有我们手动新增了 memo 之后才会进行这种比较,因此,我们测试的时候,先以 1000 次为例看看结果。

我们定义两个数量稍微多一点的 props 对象,他们最有最后一项不相同,因此比较的次数会拉满。

var prev = {a:1, b: 1, c: 1, d: 1, e: 1, f: 1, g: 1}
var next = {a:1, b: 1, c: 1, d: 1, e: 1, f: 1, g: 2}

完整代码如下:

function is(x, y) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}

const objectIs =
  typeof Object.is === 'function' ? Object.is : is;


function shallowEqual(objA, objB) {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    const currentKey = keysA[i];
    if (
      !Object.hasOwnProperty.call(objB, currentKey) ||
      !is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }

  return true;
}

var markTime = performance.now()
var prev = {a:1, b: 1, c: 1, d: 1, e: 1, f: 1, g: 1}
var next = {a:1, b: 1, c: 1, d: 1, e: 1, f: 1, g: 2}

for(var i = 0; i <= 1000; i++) {
  if (shallowEqual(prev, next)) {
    console.log('相等')
  }
}
var endTime = performance.now()
console.log(endTime - markTime)

1000 次比较结果耗时如下,约为 1.4ms。

5000 次比较结果耗时如下,约为 3.6ms。

10000 次比较结果耗时如下,约为 6.6 ms。

这里我们可以做一个简单的调整,让对比耗时直接少一半。那就是把唯一的变化量,写到前面来,如图所示,耗时只用了 3.1ms。

运用到实践中,就是把 props 中的变量属性,尽量写在前面,能够大幅度提高对比性能。

四、总结

次数

全等

is

shallow

五千

0.3

0.4

3.6

一万

0.6

0.8

6.6

百万

6.4

11.4

162

因此我们从测试结果中看到,全量 diff 并不可怕,如果你对性能优化的理解非常到位,那么能你的项目中,全量 diff 所花费的时间只有 0.几ms,理论的极限性能就是只在你更新的组件里对比出差异,执行 re-render。

当然,由于对于 React 内部机制的理解程度不同,会导致一些差异,例如有些同学的项目中,会执行过多的冗余 re-render。从而导致在大型项目中性能体验可能出现问题。那么这种情况下,也不用担心,有一种超级笨办法,那就是在项目中,结合我们刚才在 shallowEqual 中提高的优化方案,无脑使用 useCallback 与 memo,你的项目性能就能得到明显的提高,当然,这个方案不够优雅但是管用。

可以看出,React 性能优化最重要的手段非常简单:就是控制当前渲染内容的节点规模。

责任编辑:姜华 来源: 这波能反杀
相关推荐

2022-04-14 09:01:39

React源码Flow

2019-07-31 07:36:27

无线技术工业物联网IIOT

2018-05-23 09:15:54

存储接口协议

2011-04-08 15:19:04

开发工具开发

2010-06-24 16:42:30

视频监控

2011-03-18 10:25:20

javac++Python

2011-04-08 15:58:02

开发工具开发

2009-10-26 13:16:46

2011-09-22 13:49:44

XML基准测试

2022-02-14 13:58:32

操作系统JSON格式鸿蒙

2014-05-29 11:09:52

无线通信技术

2023-05-07 07:56:53

Python方式

2022-11-03 15:22:15

数据结构Python

2015-12-16 13:48:14

云成本云计算

2023-03-07 16:12:32

2009-06-11 17:22:03

操作xml方式Java

2010-09-14 12:19:02

2009-07-23 09:25:28

SaaS成本

2019-12-16 08:00:00

ReactAngularVue

2019-08-29 09:00:55

开发Flutter框架
点赞
收藏

51CTO技术栈公众号