简析React源码之ExpirationTime 前言 熟悉React的朋友都知道如果在组件内使用setState会创建一个Update从而导致页面重新渲染,那么理论上如果有多个setState就会导致多次渲染,那么问题来了,实际真的是这个样的吗?
演示Demo 首先我们先简单的建立一个demoBlock 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的原因。