2022-09-15JavaScript00

不知道你有没有写过类似的代码,反正以前我是写过

function test() {
    let arr = [3, 2, 1]
    arr.forEach(async item => {
        const res = await fetch(item)
        console.log(res)
    })
    console.log('end')
}

function fetch(x) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(x)
        }, 500 * x)
    })
}

test()

我当时期望的打印顺序是

3
2
1
end

结果现实与我开了个玩笑,打印顺序居然是

end
1
2
3

为什么

其实原因很简单,那就是 forEach 只支持同步代码。

我们可以参考下 Polyfill 版本的 forEach,简化以后类似就是这样的伪代码

while (index < arr.length) {
    // 也就是我们传入的回调函数
    callback(item, index)
}

从上述代码中我们可以发现,forEach 只是简单的执行了下回调函数而已,并不会去处理异步的情况。并且你在 callback 中即使使用 break 也并不能结束遍历。

怎么解决?

一般来说解决的办法有两种。

第一种是使用 Promise.all 的方式

async function test() {
    let arr = [3, 2, 1]
    await Promise.all(
        arr.map(async item => {
            const res = await fetch(item)
            console.log(res)
        })
    )
    console.log('end')
}

这样可以生效的原因是 async 函数肯定会返回一个 Promise 对象,调用 map 以后返回值就是一个存放了 Promise 的数组了,这样我们把数组传入 Promise.all 中就可以解决问题了。但是这种方式其实并不能达成我们要的效果,如果你希望内部的 fetch 是顺序完成的,可以选择第二种方式。

另一种方法是使用 for...of

async function test() {
    let arr = [3, 2, 1]
    for (const item of arr) {
        const res = await fetch(item)
        console.log(res)
    }
    console.log('end')
}

这种方式相比 Promise.all 要简洁的多,并且也可以实现开头我想要的输出顺序。

但是这时候你是否又多了一个疑问?为啥 for...of 内部就能让 await 生效呢。

因为 for...of 内部处理的机制和 forEach 不同,forEach 是直接调用回调函数,for...of 是通过迭代器的方式去遍历。

async function test() {
    let arr = [3, 2, 1]
    const iterator = arr[Symbol.iterator]()
    let res = iterator.next()
    while (!res.done) {
        const value = res.value
        const res1 = await fetch(value)
        console.log(res1)
        res = iterator.next()
    }
    console.log('end')
}

以上代码等价于 for...of,可以看成 for...of 是以上代码的语法糖。

最后

以上就是本篇文章的全部内容了,如果你还有什么疑问欢迎在评论区与我互动。

2022-09-15计算机网络00

HTTP

HTTP 协议是个无状态协议,不会保存状态。

Post 和 Get 的区别

先引入副作用和幂等的概念。

副作用指对服务器上的资源做改变,搜索是无副作用的,注册是副作用的。

幂等指发送 M 和 N 次请求(两者不相同且都大于 1),服务器上资源的状态一致,比如注册 10 个和 11 个帐号是不幂等的,对文章进行更改 10 次和 11 次是幂等的。

在规范的应用场景上说,Get 多用于无副作用,幂等的场景,例如搜索关键字。Post 多用于副作用,不幂等的场景,例如注册。

在技术上说:

  • Get 请求能缓存,Post 不能
  • Post 相对 Get 安全一点点,因为Get 请求都包含在 URL 里,且会被浏览器保存历史纪录,Post 不会,但是在抓包的情况下都是一样的。
  • Post 可以通过 request body来传输比 Get 更多的数据,Get 没有这个技术
  • URL有长度限制,会影响 Get 请求,但是这个长度限制是浏览器规定的,不是 RFC 规定的
  • Post 支持更多的编码类型且不对数据类型限制

常见状态码

2XX 成功

200 OK,表示从客户端发来的请求在服务器端被正确处理 204 No content,表示请求成功,但响应报文不含实体的主体部分 205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容 206 Partial Content,进行范围请求

3XX 重定向

301 moved permanently,永久性重定向,表示资源已被分配了新的 URL 302 found,临时性重定向,表示资源临时被分配了新的 URL 303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源 304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况 307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求

4XX 客户端错误

400 bad request,请求报文存在语法错误 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息 403 forbidden,表示对请求资源的访问被服务器拒绝 404 not found,表示在服务器上没有找到请求的资源

5XX 服务器错误

500 internal sever error,表示服务器端在执行请求时发生了错误 501 Not Implemented,表示服务器不支持当前请求所需要的某个功能 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

2022-09-15计算机网络00

TCP协议

头部

TCP 头部比 UDP 头部复杂的多

222.png 对于 TCP 头部来说,以下几个字段是很重要的

  • Sequence number,这个序号保证了 TCP 传输的报文都是有序的,对端可以通过序号顺序的拼接报文
  • Acknowledgement Number,这个序号表示数据接收端期望接收的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到
  • Window Size,窗口大小,表示还能接收多少字节的数据,用于流量控制
  • 标识符
    • URG=1:该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。
    • ACK=1:该字段为一表示确认号字段有效。此外,TCP 还规定在连接建立后传送的所有报文段都必须把 ACK 置为一。
    • PSH=1:该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。
    • RST=1:该字段为一表示当前 TCP 连接出现严重问题,可能需要重新建立 TCP 连接,也可以用于拒绝非法的报文段和拒绝连接请求。
    • SYN=1:当SYN=1,ACK=0时,表示当前报文段是一个连接请求报文。当SYN=1,ACK=1时,表示当前报文段是一个同意建立连接的应答报文。
    • FIN=1:该字段为一表示此报文段是一个释放连接的请求报文。

状态机

HTTP 是无连接的,所以作为下层的 TCP 协议也是无连接的,虽然看似 TCP 将两端连接了起来,但是其实只是两端共同维护了一个状态

333.png

TCP 的状态机是很复杂的,并且与建立断开连接时的握手息息相关,接下来就来详细描述下两种握手。

在这之前需要了解一个重要的性能指标 RTT。该指标表示发送端发送数据到接收到对端数据所需的往返时间。

建立连接三次握手

444.png

在 TCP 协议中,主动发起请求的一端为客户端,被动连接的一端称为服务端。不管是客户端还是服务端,TCP 连接建立完后都能发送和接收数据,所以 TCP 也是一个全双工的协议。

起初,两端都为 CLOSED 状态。在通信开始前,双方都会创建 TCB。 服务器创建完 TCB 后遍进入 LISTEN 状态,此时开始等待客户端发送数据。

第一次握手

客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x 表示客户端的数据通信初始序号。

第二次握手

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

PS:第三次握手可以包含数据,通过 TCP 快速打开(TFO)技术。其实只要涉及到握手的协议,都可以使用类似 TFO 的方式,客户端和服务端存储相同 cookie,下次握手时发出 cookie 达到减少 RTT 的目的。

你是否有疑惑明明两次握手就可以建立起连接,为什么还需要第三次应答?

因为这是为了防止失效的连接请求报文段被服务端接收,从而产生错误。

可以想象如下场景。客户端发送了一个连接请求 A,但是因为网络原因造成了超时,这时 TCP 会启动超时重传的机制再次发送一个连接请求 B。此时请求顺利到达服务端,服务端应答完就建立了请求。如果连接请求 A 在两端关闭后终于抵达了服务端,那么这时服务端会认为客户端又需要建立 TCP 连接,从而应答了该请求并进入 ESTABLISHED 状态。此时客户端其实是 CLOSED 状态,那么就会导致服务端一直等待,造成资源的浪费。

PS:在建立连接中,任意一端掉线,TCP 都会重发 SYN 包,一般会重试五次,在建立连接中可能会遇到 SYN FLOOD 攻击。遇到这种情况你可以选择调低重试次数或者干脆在不能处理的情况下拒绝请求。

断开链接四次握手

5555.png

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

第一次握手

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

第二次握手

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。

第三次握手

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。

第四次握手

A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态?

为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭。

ARQ 协议

ARQ 协议也就是超时重传机制。通过确认和超时机制保证了数据的正确送达,ARQ 协议包含停止等待 ARQ 和连续 ARQ

停止等待 ARQ
正常传输过程

只要 A 向 B 发送一段报文,都要停止发送并启动一个定时器,等待对端回应,在定时器时间内接收到对端应答就取消定时器并发送下一段报文。

报文丢失或出错

在报文传输的过程中可能会出现丢包。这时候超过定时器设定的时间就会再次发送丢包的数据直到对端响应,所以需要每次都备份发送的数据。

即使报文正常的传输到对端,也可能出现在传输过程中报文出错的问题。这时候对端会抛弃该报文并等待 A 端重传。

PS:一般定时器设定的时间都会大于一个 RTT 的平均时间。

ACK 超时或丢失

对端传输的应答也可能出现丢失或超时的情况。那么超过定时器时间 A 端照样会重传报文。这时候 B 端收到相同序号的报文会丢弃该报文并重传应答,直到 A 端发送下一个序号的报文。

在超时的情况下也可能出现应答很迟到达,这时 A 端会判断该序号是否已经接收过,如果接收过只需要丢弃应答即可。

这个协议的缺点就是传输效率低,在良好的网络环境下每次发送报文都得等待对端的 ACK 。

连续 ARQ

在连续 ARQ 中,发送端拥有一个发送窗口,可以在没有收到应答的情况下持续发送窗口内的数据,这样相比停止等待 ARQ 协议来说减少了等待时间,提高了效率。

累计确认

连续 ARQ 中,接收端会持续不断收到报文。如果和停止等待 ARQ 中接收一个报文就发送一个应答一样,就太浪费资源了。通过累计确认,可以在收到多个报文以后统一回复一个应答报文。报文中的 ACK 可以用来告诉发送端这个序号之前的数据已经全部接收到了,下次请发送这个序号 + 1的数据。

但是累计确认也有一个弊端。在连续接收报文时,可能会遇到接收到序号 5 的报文后,并未接到序号 6 的报文,然而序号 7 以后的报文已经接收。遇到这种情况时,ACK 只能回复 6,这样会造成发送端重复发送数据,这种情况下可以通过 Sack 来解决,这个会在下文说到。

滑动窗口

在上面小节中讲到了发送窗口。在 TCP 中,两端都维护着窗口:分别为发送端窗口和接收端窗口。

发送端窗口包含已发送但未收到应答的数据和可以发送但是未发送的数据。

666.png

发送端窗口是由接收窗口剩余大小决定的。接收方会把当前接收窗口的剩余大小写入应答报文,发送端收到应答后根据该值和当前网络拥塞情况设置发送窗口的大小,所以发送窗口的大小是不断变化的。

当发送端接收到应答报文后,会随之将窗口进行滑动

滑动窗口实现了流量控制。接收方通过报文告知发送方还可以发送多少数据,从而保证接收方能够来得及接收数据。

Zero 窗口

在发送报文的过程中,可能会遇到对端出现零窗口的情况。在该情况下,发送端会停止发送数据,并启动 persistent timer 。该定时器会定时发送请求给对端,让对端告知窗口大小。在重试次数超过一定次数后,可能会中断 TCP 链接。

拥塞处理

拥塞处理和流量控制不同,后者是作用于接收方,保证接收方来得及接受数据。而前者是作用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的情况。

拥塞处理包括了四个算法,分别为:慢开始,拥塞避免,快速重传,快速恢复。

慢开始算法

慢开始算法,顾名思义,就是在传输开始时将发送窗口慢慢指数级扩大,从而避免一开始就传输大量数据导致网络拥塞。

慢开始算法步骤具体如下

连接初始设置拥塞窗口(Congestion Window) 为 1 MSS(一个分段的最大数据量) 每过一个 RTT 就将窗口大小乘二 指数级增长肯定不能没有限制的,所以有一个阈值限制,当窗口大小大于阈值时就会启动拥塞避免算法。

拥塞避免算法

拥塞避免算法相比简单点,每过一个 RTT 窗口大小只加一,这样能够避免指数级增长导致网络拥塞,慢慢将大小调整到最佳值。

在传输过程中可能定时器超时的情况,这时候 TCP 会认为网络拥塞了,会马上进行以下步骤:

将阈值设为当前拥塞窗口的一半 将拥塞窗口设为 1 MSS 启动拥塞避免算法 #快速重传 快速重传一般和快恢复一起出现。一旦接收端收到的报文出现失序的情况,接收端只会回复最后一个顺序正确的报文序号(没有 Sack 的情况下)。如果收到三个重复的 ACK,无需等待定时器超时再重发而是启动快速重传。具体算法分为两种:

TCP Taho 实现如下

将阈值设为当前拥塞窗口的一半 将拥塞窗口设为 1 MSS 重新开始慢开始算法

TCP Reno 实现如下

拥塞窗口减半 将阈值设为当前拥塞窗口 进入快恢复阶段(重发对端需要的包,一旦收到一个新的 ACK 答复就退出该阶段) 使用拥塞避免算法 #TCP New Ren 改进后的快恢复 TCP New Reno 算法改进了之前 TCP Reno 算法的缺陷。在之前,快恢复中只要收到一个新的 ACK 包,就会退出快恢复。

在 TCP New Reno 中,TCP 发送方先记下三个重复 ACK 的分段的最大序号。

假如我有一个分段数据是 1 ~ 10 这十个序号的报文,其中丢失了序号为 3 和 7 的报文,那么该分段的最大序号就是 10。发送端只会收到 ACK 序号为 3 的应答。这时候重发序号为 3 的报文,接收方顺利接收并会发送 ACK 序号为 7 的应答。这时候 TCP 知道对端是有多个包未收到,会继续发送序号为 7 的报文,接收方顺利接收并会发送 ACK 序号为 11 的应答,这时发送端认为这个分段接收端已经顺利接收,接下来会退出快恢复阶段。

2022-09-15计算机网络00

UDP协议

面向报文

UDP 是一个面向报文(报文可以理解为一段段的数据)的协议。意思就是 UDP 只是报文的搬运工,不会对报文进行任何拆分和拼接操作。

具体来说

  • 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

不可靠性

  1. UDP 是无连接的,也就是说通信不需要建立和断开连接。
  2. UDP 也是不可靠的。协议收到什么数据就传递什么数据,并且也不会备份数据,对方能不能收到是不关心的
  3. UDP 没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。

高效

因为 UDP 没有 TCP 那么复杂,需要保证数据不丢失且有序到达。所以 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的。

2019-06-01-43739.png

头部包含了以下几个数据

  1. 两个十六位的端口号,分别为源端口(可选字段)和目标端口
  2. 整个数据报文的长度
  3. 整个数据报文的检验和(IPv4 可选 字段),该字段用于发现头部信息和数据中的错误

传输方式

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

2022-09-15React00

组件更新流程(一)

这是我的剖析 React 源码的第一篇文章。这篇文章开始将会带着大家学习组件更新过程相关的内容,尽可能的脱离源码来了解原理,降低大家的学习难度。

组件更新流程中你能学到什么?

文章分为三部分,在这部分的文章中你可以学习到如下内容:

setState 背后的批量更新如何实现 Fiber 是什么?有什么用? 如何调度任务 在另外的两篇文章中你可以学习到如何调和组件及渲染组件的过程。

setState 背后的批量更新如何实现

想必大家都知道大部分情况下多次 setState 不会触发多次渲染,并且 state 的值也不是实时的,这样的做法能够减少不必要的性能消耗。

handleClick () {
  // 初始化 `count` 为 0
  console.log(this.state.count) // -> 0
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log(this.state.count) // -> 0
  this.setState({ count: this.state.count + 1 })
  console.log(this.state.count) // -> 0
}

那么这个行为是如何实现的呢?答案是批量更新。接下来我们就来学习批量更新是如何实现的。

其实这个背后的原理相当之简单。假如 handleClick 是通过点击事件触发的,那么 handleClick 其实差不多会被包装成这样:

isBatchingUpdates = true
try {
  handleClick()
} finally {
  isBatchingUpdates = false
  // 然后去更新
}

在执行 handleClick 之前,其实 React 就会默认这次触发事件的过程中如果有 setState 的话就应该批量更新。

当我们在 handleClick 内部执行 setState 时,更新状态的这部分代码首先会被丢进一个队列中等待后续的使用。然后继续处理更新的逻辑,毕竟触发 setState 肯定会触发一系列组件更新的流程。但是在这个流程中如果 React 发现需要批量更新 state 的话,就会立即中断更新流程。

也就是说,虽然我们在 handleClick 中调用了三次 setState,但是并不会走完三次的组件更新流程,只是把更新状态的逻辑丢到了一个队列中。当 handleClick 执行完毕之后会再执行一次组件更新的流程。

另外组件更新流程其实是有两个截然不同的分支的。一种就是触发更新以后一次完成全部的组件更新流程;另一种是触发更新以后分时间片段完成所有的组件更新,用户体验更好,这种方式被称之为任务调度。如果你想详细了解这一块的内容,可以阅读我之前 写的文章。

当然本文也会提及一部分调度相关的内容,毕竟这块也包含在组件更新流程中。但是在学习任务调度之前,我们需要先来学习下 fiber 相关的内容,因为这块内容是 React 实现各种这些新功能的基石。

Fiber 是什么?有什么用?

在了解 Fiber 之前,我们先来了解下为什么 React 官方要费那么大劲去重构 React。

在 React 15 版本的时候,我们如果有组件需要更新的话,那么就会递归向下遍历整个虚拟 DOM 树来判断需要更新的地方。这种递归的方式弊端在于无法中断,必须更新完所有组件才会停止。这样的弊端会造成如果我们需要更新一些庞大的组件,那么在更新的过程中可能就会长时间阻塞主线程,从而造成用户的交互、动画的更新等等都不能及时响应。

React 的组件更新过程简而言之就是在持续调用函数的一个过程,这样的一个过程会形成一个虚拟的调用栈。假如我们控制这个调用栈的执行,把整个更新任务拆解开来,尽可能地将更新任务放到浏览器空闲的时候去执行,那么就能解决以上的问题。

那么现在是时候介绍 Fiber 了。Fiber 重新实现了 React 的核心算法,带来了杀手锏增量更新功能。它有能力将整个更新任务拆分为一个个小的任务,并且能控制这些任务的执行。

这些功能主要是通过两个核心的技术来实现的:

  • 新的数据结构 fiber
  • 调度器

新的数据结构 fiber

在前文中我们说到了需要拆分更新任务,那么如何把控这个拆分的颗粒度呢?答案是 fiber。

我们可以把每个 fiber 认为是一个工作单元,执行更新任务的整个流程(不包括渲染)就是在反复寻找工作单元并运行它们,这样的方式就实现了拆分任务的功能。

拆分成工作单元的目的就是为了让我们能控制 stack frame(调用栈中的内容),可以随时随地去执行它们。由此使得我们在每运行一个工作单元后都可以按情况继续执行或者中断工作(中断的决定权在于调度算法)。

那么 fiber 这个数据结构到底长什么样呢?现在就让我们来一窥究竟。

fiber 内部其实存储了很多上下文信息,我们可以把它认为是改进版的虚拟 DOM,它同样也对应了组件实例及 DOM 元素。同时 fiber 也会组成 fiber tree,但是它的结构不再是一个树形,而是一个链表的结构。

以下是 fiber 中的一些重要属性:

{
  ...
  // 浏览器环境下指 DOM 节点
  stateNode: any,

  // 形成列表结构
  return: Fiber | null,
  child: Fiber | null,
  sibling: Fiber | null,

  // 更新相关
  pendingProps: any,  // 新的 props
  memoizedProps: any,  // 旧的 props
  // 存储 setState 中的第一个参数
  updateQueue: UpdateQueue<any> | null,
  memoizedState: any, // 旧的 state

  // 调度相关
  expirationTime: ExpirationTime,  // 任务过期时间

  // 大部分情况下每个 fiber 都有一个替身 fiber
  // 在更新过程中,所有的操作都会在替身上完成,当渲染完成后,
  // 替身会代替本身
  alternate: Fiber | null,

  // 先简单认为是更新 DOM 相关的内容
  effectTag: SideEffectTag, // 指这个节点需要进行的 DOM 操作
  // 以下三个属性也会形成一个链表
  nextEffect: Fiber | null, // 下一个需要进行 DOM 操作的节点
  firstEffect: Fiber | null, // 第一个需要进行 DOM 操作的节点
  lastEffect: Fiber | null, // 最后一个需要进行 DOM 操作的节点,同时也可用于恢复任务
  ....
}

总的来说,我们可以认为 fiber 就是一个工作单元的数据结构表现,当然它同样也是调用栈中的一个重要组成部分。

提示

Fiber 和 fiber 不是同一个概念。前者代表新的调和器,后者代表 fiber node,也可以认为是改进后的虚拟 DOM。

调度器简介

每次有新的更新任务发生的时候,调度器都会按照策略给这些任务分配一个优先级。比如说动画的更新优先级会高点,离屏元素的更新优先级会低点。

通过这个优先级我们可以获取一个该更新任务必须执行的截止时间,优先级越高那么截止时间就越近,反之亦然。这个截止时间是用来判断该任务是否已经过期,如果过期的话就会马上执行该任务。

然后调度器通过实现 requestIdleCallback 函数来做到在浏览器空闲的时候去执行这些更新任务。

这其中的实现原理略微复杂。简单来说,就是通过定时器的方式,来获取每一帧的结束时间。得到每一帧的结束时间以后我们就能判断当下距离结束时间的一个差值。

如果还未到结束时间,那么也就意味着我可以继续执行更新任务;如果已经过了结束时间,那么就意味着当前帧已经没有时间给我执行任务了,必须把执行权交还给浏览器,也就是打断任务的执行。

另外当开始执行更新任务(也就是寻找工作单元并执行的过程)时,如果有新的更新任务进来,那么调度器就会按照两者的优先级大小来进行决策。如果新的任务优先级小,那么当然继续当下的任务;如果新的任务优先级大,那么会打断任务并开始新的任务。

小结

当交互事件调用 setState 后,会触发批量更新,在整个交互事件回调执行完之前 state 都不会发生变更。

回调执行完毕后,开始更新任务,并触发调度。调度器会给这些更新任务一一设置优先级,并且在浏览器空闲的时候去执行他们,当然任务过期除外(会立刻触发更新,不再等待)。

如果在执行更新任务的时候,有新的任务进来,会判断两个任务的优先级高低。假如新任务优先级高,那么打断旧的任务,重新开始,否则继续执行任务。