前言
曾经的我格局只有一个Task,直到突然有一天我我破茧而出,有了更高视角俯瞰这些Task,一切也变得更有趣。此时用一句古诗形容最为应景:不识庐山真面目,只缘身在此山中。但也可能是跳脱三界外,仍在五行中,哈哈哈哈哈。
不一样的角度再看Event Loop
在真正开始之前React,可能要重新理解一下 Javascript的Event Loop。由于平时开发者主要打交道的是main thread(主线程),也就是平时所说的Javascript是单线程(main thread)的,在同一时间只有一个任务在执行,这里要区分开并发和并行(concurrency and parallelism)是两个不同的概念,并发是通过时间切片(time slice)来实现的。见下图:
即浏览器引擎将所有要执行的代码逻辑作为Task进行调度执行,这里的Task可以是<script>、点击事件触发回调函数、setTimeout的定时任务,这些都是开发者发起的任务,同时还有一些浏览器发起的任务比如将dom在浏览器上绘制出来(layout+paint+commit),垃圾回收任务(GC)。也就是说一定要有一个认知:基于时间切片执行的Task不仅有开发者直接触发的Task,还有浏览器自身发起的Task,如果一个任务执行时间过长(Long Task),用户可能就会感觉到卡顿,没有响应,所以我们要避免平时写的代码执行时间不能过长;当一个任务执行完毕,也就是将执行的控制权重新交给了event loop,或者说交给了主线程(main thread),其在React源码中的说法叫做yield to main thread,也就是将控制权让渡给主线程,让其有机会调度绘制任务,以此将上一个任务对dom的修改及时绘制出来;试想一下如果两个任务task1和task2(比如通过click或setTimeout或MessageChannel发起),task1和task1都对同一个p标签的内容进行修改,两个task连续执行,而没有及时让主线程执行渲染绘制任务,那么task2对p标签修改将覆盖task1对p标签的修改,从用户的角度就是他们只能观察到task2对p标签的修改结果,而看不到task1对p标签的修改。
这里重新回顾一下事件循环单次执行逻辑:在一次事件循环中先执行第一个处于等待中(pending)的Task(在执行Task过程中可能产生新的task和microtask),然后执行Task中产生的所有microtasks,而微任务本身可以产生新的微任务,一直执行直到没有新的微任务产生,Task执行结束,然后将执行控制权交给主线程。主线程在执行下一个用户触发的Task之前,会先检查是否有需要渲染绘制dom,如果有则会调度一个dom渲染绘制任务,然后才是进入下一个循环。微任务是对既有task调度机制的一种补充,在一个Task中可能多次刷新微任务队列,典型的案例Dom点击事件在冒泡过程中触发的微任务;下图阐明了Task是由Function Call和Run Microtasks两部分组成的.
task与microtasks关系:
task与microtasks关系:
单次事件循环:task(microtasks) + render task
react中useLayoutEffect会在浏览器执行绘制任务前面的task中同步执行,所以如果useLayoutEffect的执行耗时过长导致页面刷新频率降低,帧数FPS变小,同游戏中的帧数;而useEffect是通过重新调度一个Task异步执行,即在浏览器绘制任务结束之后执行,所以就不会造成帧数的变小,所以官方推荐非必要不要使用useLayyoutEffect而使用useEffect.
总结
此篇文章介绍了Event loop机制,这对于理解react的并发渲染至关重要(renderRootConcurrent), 在react的并发渲染中每一个更新都有一个与之对应的task通过react sheduler进行调度,以此实现了将Long task(render + commit)变成短task, 这里说的太抽象,具体实现机制见下一篇react sheduler,由于内容过长故此分为两篇文章。
资料
- Using microtasks in JavaScript with queueMicrotask()
- In depth: Microtasks and the JavaScript runtime environment
- Tasks, microtasks, queues and schedules
- Optimize long tasks
强烈建议将以上资料都仔细至少看一遍