在计算机里,并发「concurrent」一词,最早是用来表示多个任务同时进行。但是由于早期的计算机能力有限,单核计算机同一时间,只能运行一个任务。因此,为了做到看上去多个应用是在同时运行的,单核计算机就快速的在不同的应用中来回切换,它执行完A应用的一个任务,就执行B应用的任务,只要切换得足够快,对于用户而言,A应用与B应用就是在同时运行。因此,对于单核CPU来说,多个任务同时执行这种情况并不存在。
后来的主流计算机已经可以做到多个任务同时执行了,但是并发一词已经有了自己专属的场景,于是我们把真正的多个任务同时执行又重新取了一个名字,并行「parallel」而并发则保留了它原本在单核CPU上的的含义:多个任务切换执行。为了知道下一个任务到底应该是谁执行了,那么单核CPU上必定会设计一个调度模式,用来确定任务的优先级。因此,并发的另外一个角度的解读,就是多个任务对同一执行资源的竞争。
一、React的并发
在页面使用JS操作DOM渲染页面的过程中,也是同样的道理,他不存在有两个任务能同时执行的情况。不过,React设计了一种机制,来模拟渲染资源的竞争。React设计了一个调度器,Scheduler,来调度任务的优先级。但是在争取谁更先渲染这个事情,在浏览器的渲染原理里,他经不起推敲。为什么呢?因为浏览器的底层渲染机制有收集逻辑,他会合并所有的渲染指令
既然这样,那React的并发又是怎么回事呢?还有更诡异的事情,React的渲染指令,是通过setState来触发,我们知道,多个setState指令,React也会将他们合并批处理setLoading(false);setList([]);//等价于setState({loading:false,list:[]})既然如此,并发体现在什么地方呢?也不存在渲染资源的竞争啊?我们看不到任务的切换执行,也看不到不同任务对渲染资源的竞争。所以真相就是…大多数情况下,React确实并不存在任何并发现象。因此,资源竞争只会发生在渲染能力不够用的时候。
一次渲染包括两个部分,一个部分是JS逻辑,我们需要在JS逻辑中明确具体的DOM操作是什么。第二个部分是渲染引擎执行渲染任务。很明显,对于React而言,他无法改变渲染引擎的逻辑。那么也就意味着,React的并发只会发生在第一个部分:JS逻辑中。因此,react还设计了第二步骤,Reconciler。当我们通过setState触发一个渲染任务时,react需要在Reconciler中,利用diff算法找出来哪些DOM需要被更改。如果多个setState指令合并之后,我们发现diff过程超出了一帧的时间,这个时候就有可能会存在渲染资源的竞争。Reconciler收集操作DOM优先级可中断因此,只有在短时间之内页面需要多次渲染,才会存在资源竞争的情况。这个时候我们才会考虑并发的存在。
我们还需要进一步思考。刚才我们已经分析出,只有在短时间之内多次渲染,并且造成了页面卡顿,我们才会考虑并发。说明此时我们想要使用并发来解决的问题就是让页面不卡顿。因此,在多次渲染的前提下,多个任务的竞争结果就一定是渲染任务总量减少了,才会不卡顿。所以我们要做的事情就是,找出级更低的任务,即使他掉帧,只要不影响页面卡顿,我们都可以接受。在React的底层设计中,setState是一个任务,但是这个任务会影响哪些UI发生变化,它就可能会对应个Fiber,每一个Fiber的执行都是一个小任务,我们可以把一个任务看成一个函数。一旦一个任务开始执行之后,React会将其拆分为更小的子任务,来提高渲染的效率。