简析React源码之ExpirationTime

前言

熟悉React的朋友都知道如果在组件内使用setState会创建一个Update从而导致页面重新渲染,那么理论上如果有多个setState就会导致多次渲染,那么问题来了,实际真的是这个样的吗?

演示Demo

首先我们先简单的建立一个demo

Block
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, { Component } from 'react';

class App extends Component {
static state = {
name: 1
}
constructor(props) {
super(props)
this.state = {
number: 10000
}
}
componentDidMount() {
this.setState({
number: 10001
})
this.setState({
number: 10002
})
this.setState({
number: 10003
})
}
render() {
const { number } = this.state;
console.log('rendering')
return (
<div>
我的编号是:{number}
</div>
)
}
}

export default App;

可见在这个demo中componentDidMount中连续执行了三次setState,那么加上初始化渲染执行的一次render函数,理论上在控制台中应该会打印四次rendering,但是事实真的是这样吗?

显示效果图

<图1> 控制台

纳尼!怎么只打印了两次呢?emmmm,这里就引出了React中一个概念了,ExpirationTime即到期时间。

源码分析

本章代码主要来源于ReactFiberExpirationTime.js文件。

示例demo代码

Block
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<!-- 示例demo代码 -->
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

// import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';

const MAX_SIGNED_31_BIT_INT = 1073741823

// type ExpirationTime = number;

const NoWork = 0;
const Never = 1;
const Sync = MAX_SIGNED_31_BIT_INT;

const UNIT_SIZE = 10
const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1;

// 1 unit of expiration time represents 10ms.
function msToExpirationTime(ms) {
// Always add an offset so that we don't clash with the magic number for NoWork.
return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0)
}

function expirationTimeToMs(expirationTime) {
return (MAGIC_NUMBER_OFFSET - expirationTime) * UNIT_SIZE;
}

//
function ceiling(num, precision) {
return (((num / precision) | 0) + 1) * precision
}

function computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs) {
return (
MAGIC_NUMBER_OFFSET -
ceiling(
MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
)
}

const LOW_PRIORITY_EXPIRATION = 5000
const LOW_PRIORITY_BATCH_SIZE = 250

function computeAsyncExpiration(currentTime) {
return computeExpirationBucket(
currentTime,
LOW_PRIORITY_EXPIRATION,
LOW_PRIORITY_BATCH_SIZE,
)
}
// (1073741822-(((1073732322 / 25) | 0) + 1) * 25)

const HIGH_PRIORITY_EXPIRATION = 500
const HIGH_PRIORITY_BATCH_SIZE = 100

function computeInteractiveExpiration(currentTime) {
return computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
)
}
// (1073741822-(((1073731872 / 10) | 0) + 1) * 10)

console.log(computeInteractiveExpiration(10000))
console.log(computeAsyncExpiration(10000))
console.log('=======')
console.log(computeInteractiveExpiration(10002))
console.log(computeAsyncExpiration(10010))
console.log('=======')
console.log(computeInteractiveExpiration(10004))
console.log(computeAsyncExpiration(10020))
console.log('=======')
console.log(computeInteractiveExpiration(10006))
console.log(computeAsyncExpiration(10030))

显示效果图

<图2> 执行结果
  • 我们这里主要执行的是computeInteractiveExpiration函数与computeAsyncExpiration,他们在React中对应的是两类ExpirationTime,一个是Interactive(交互式,例如:由事件触发)另一个是async(异步式,例如:setState )

  • 这里特别强调一下,computeInteractiveExpiration与computeAsyncExpiration几乎无异,两者之只不过在传递参数上大小略有不同,computeInteractiveExpiration传递的是HIGH_PRIORITY_EXPIRATION(500)跟HIGH_PRIORITY_BATCH_SIZE(100),而computeAsyncExpiration传递的则是LOW_PRIORITY_EXPIRATION(5000)跟LOW_PRIORITY_BATCH_SIZE(250),根据以上代码中的公式可以得知传递的参数越大得到的expirationTime也会越大,也就是说 交互的优先级高于异步式,computeInteractiveExpiration的更新优先级高于computeAsyncExpiration。

  • 根据图二可以得知,在computeInteractiveExpiration函数中区间在10以内的获取的ExpirationTime相同,computeAsyncExpiration函数中区间为25以内获取的ExpirationTime相同。如果不同的Update获取到的ExpirationTime相同,那么React则会认为把它整合为同一个Update一次性更新,避免因为不必要的重复渲染导致性能问题。这也就是为什么在控制台中只打印出了两次rendering的原因。