likes
comments
collection
share

JS是单线程执行,react fiber为什么可以随时暂停又恢复执行?

作者站长头像
站长
· 阅读数 1

浏览器在执行JS代码的时候,是单线程执行的。具体是由JS引擎的主线程不断从任务队列取出任务并执行,即:JS引擎每次从任务队列取出一个任务,执行完成后再去取出下一个任务执行。但是react fiber架构最重要的一个变革就是允许 React 将渲染工作分割成多个小的任务单元,这些任务可以被暂停、中断或重新安排优先级。既然JS是单线程执行,而每个任务执行完立马执行下一个任务,那 fiber 的暂停恢复又是如何做到的呢?了解了生成器(Generator)的底层实现机制,这个问题就简单了。

react fiber

React Fiber 是 React 框架的一个重写版本,于 16 版本中被引入,它主要目标是增强 React 在渲染大型应用程序和动画等场景下的性能。Fiber 允许 React 将渲染工作分割成多个小的任务单元,这些任务可以被暂停、中断或重新安排优先级。这种方式使得主线程更加平滑,提高了应用的响应能力,同时,它还引入了任务调度器,可以根据任务的优先级进行智能调度。紧急更新(如用户输入)可以获得更高的优先级,而不紧急的工作(如离屏渲染)可以延后处理。

生成器&协程

function* genDemo() {
  console.log("开始执行第一段")
  yield 'generator 1'

  console.log("开始执行第二段")
  yield 'generator 2'

  console.log("开始执行第三段")
  yield 'generator 3'

  console.log("执行结束")
  return 'generator 4'
}

console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')

执行上面这段代码,观察输出结果,发现函数genDemo并不是一次执行完的,全局代码和genDemo函数交替执行。其实这就是生成器函数的特性,可以暂停执行,也可以恢复执行。生成器函数的具体使用方式:

  1. 在生成器函数内部执行一段代码,如果遇到yield关键字,JavaScript引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
  2. 外部函数可以通过next方法恢复函数的执行。

之所以函数可以暂停恢复执行,正是因为有协程的存在。协程是一种比线程更加轻量级的存在。可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是A协程,要启动B协程,那么A协程就需要将主线程的控制权交给B协程,这就体现在A协程暂停执行,B协程恢复执行;同样,也可以从B协程中启动A协程。通常,如果从A协程启动B协程,就把A协程称为B协程的父协程。

正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

JS是单线程执行,react fiber为什么可以随时暂停又恢复执行?

fiber&协程

react比较两个虚拟DOM的过程是在一个递归函数里执行的,其核心算法是reconciliation,即平时笼统称呼的diff算法。通常情况下,这个比较过程执行得很快,不过当虚拟DOM比较复杂的时候,执行比较函数就有可能占据主线程比较久的时间,这样就会导致其他任务的等待,造成页面卡顿。为了解决这个问题,React团队重写了reconciliation算法,新的算法称为Fiber reconciler,之前老的算法称为Stack reconciler。而此处的Fiber正是上面提到的协程。

拓展 async/await原理

其实async/await技术背后的秘密就是Promise和生成器应用,往低层说就是微任务和协程应用。 async是一个通过异步执行并隐式返回 Promise 作为结果的函数。

async function foo() {
  console.log(1)
  let a = await 100
  console.log(a)
  console.log(2)
}
console.log(0)
foo()
console.log(3)

JS是单线程执行,react fiber为什么可以随时暂停又恢复执行? 结合上图,来一起分析下async/await的执行流程。

首先,执行console.log(0)这个语句,打印出来0。

紧接着就是执行foo函数,由于foo函数是被async标记过的,所以当进入该函数的时候,JavaScript引擎会保存当前的调用栈等信息,然后执行foo函数中的console.log(1)语句,并打印出1。接下来就执行到foo函数中的await 100这个语句了,这里是分析的重点,因为在执行await 100这个语句时,JavaScript引擎在背后默默做了一些事情

,它会默认创建一个Promise对象:

let promise_ = new Promise((resolve, reject) => {
  resolve(100)
})

在这个promise_对象创建的过程中,可以看到在executor函数中调用了resolve函数,JavaScript引擎会将该任务提交给微任务队列

然后JavaScript引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将promise_对象返回给父协程。

主线程的控制权已经交给父协程了,这时候父协程要做的一件事是调用promise_.then来监控promise状态的改变。

接下来继续执行父协程的流程,这里执行console.log(3),并打印出来3。随后父协程将执行结束,在结束之前,会进入微任务的检查点,然后执行微任务队列,微任务队列中有resolve(100)的任务等待执行,执行到这里的时候,会触发promise_.then中的回调函数,如下所示:

promise_.then((value) => {
  //回调函数被激活后
  //将主线程控制权交给foo协程,并将vaule值传给协程
})

该回调函数被激活以后,会将主线程的控制权交给foo函数的协程,并同时将value值传给该协程。

foo协程激活之后,会把刚才的value值赋给了变量a,然后foo协程继续执行后续语句,执行完成之后,将控制权归还给父协程。

以上就是await/async的执行流程。正是因为async和await在背后做了大量的工作,所以我们才能用同步的方式写出异步代码来。