-
Notifications
You must be signed in to change notification settings - Fork 51
跟着 Event loop 规范理解浏览器中的异步机制 #29
Comments
顾轶灵的回答: |
@zengyuangai |
@fi3ework 懂了 是我纠结了~ 就是在 microTask 把数据统一处理 对吧! |
应该是吧(我没用过 Vue),看了下文档,应该就是将回到推到了下一轮的 microTask 里,相当于
|
@fi3ework |
@zengyuangai 啊我看错了,不过我感觉是这样的
microTask 要在本轮的 UI 渲染前 全部执行完毕 |
@fi3ework 在取dom的值的时候 dom是不是会先重排/重绘取到最新DOM,也就是线程会暂时性切出去,再回去执行 |
@zengyuangai 我的理解是不会,在修改完 DOM 之后,DOM 的值变了但是 UI 还没来得及 re-render。而且 eventLoop 的模型是这样规范的了,不能临时切出去,只能在一轮的最后 re-render。 |
@fi3ework 那是不是 UI的渲染 与 dom的信息更新 两个事情 可以这么理解吗? |
@zengyuangai 是的,Vue 就是这么做的啊,在一轮的 microTask 中随便修改 DOM 多次,最后只会 render 一次,如果每次修改 DOM 都 render 那不就不对了吗 |
@fi3ework get到 谢谢你的耐心的解答 感恩~ |
@zengyuangai 共同学习😆 |
请问task、microtask下面具体的分类是在哪里看到的?我看到很多地方都这样写,可我在规范里面并没有找到。个人觉得是有问题的。microtask是H5的概念,拿到node里面是否合适?即便合适, |
|
@zhuanyongxigua
|
@fi3ework
所以,放在node或者浏览器中都不准确。 |
@zhuanyongxigua |
有个疑问,UI rendering 是属于 task 么?如果属于,属于何种 task sources ,又或者说,某些 task 不会排入 task queue ? |
例子不正确 |
关于UI rendering属于task有个疑问,一轮event loop的流程是task -> microTask -> UI render ,如果UI rendering是task的话,不就违背了一轮event loop只执行一个task的原则了吗? |
前言
我们都知道 JavaScript 是一门单线程语言,这意味着同一事件只能执行一个任务,结束了才能去执行下一个。如果前面的任务没有执行完,后面的任务就会一直等待。试想,有一个耗时很长的网络请求,如果所有任务都需要等待这个请求完成才能继续,显然是不合理的并且我们在浏览器中也没有体验过这种情况(除非你要同步请求 Ajax),究其原因,是 JavaScript 借助异步机制来实现了任务的调度。
我们先看一个面试题:
上面这个例子会输出什么?答案是:
说明并没有 catch 到丢出来的 error,这个例子可能理解起来费劲一点。
如果我换一个例子
稍微了解一点浏览器中异步机制的同学都能答出会输出 “A C B”,本文会通过分析 event loop 来对浏览器中的异步进行梳理,并搞清上面的问题。
调用栈
函数调用栈其实就是执行上下文栈(Execution Context Stack),每当调用一个函数时就会产生一个新的执行上下文,同时新产生的这个执行上下文就会被压入执行上下文栈中。
全局上下文最先入栈,并且在离开页面时开会出栈,JavaScript 引擎不断的执行上下文栈中栈顶的那个执行上下文,在它执行完毕后将它出栈,直到整个执行栈为空。关于执行栈有五点比较关键:
这里首先要明确一个问题,函数上下文执行栈是与 JavaScript 引擎(Engine)相关的概念,而异步/回调是与运行环境(Runtime)相关的概念。
如果执行栈与异步机制完全无关,我们写了无数遍的点击触发回调是如何做到的呢?是运行环境(浏览器/Node)来完成的, 在浏览器中,异步机制是借助 event loop 来实现的,event loop 是异步的一种实现机制。JavaScript 引擎只是“傻傻”的一直执行栈顶的函数,而运行环境负责管理在什么时候压入执行上下文栈什么函数来让引擎执行。
另外,从一个侧面可以反应出执行上下文栈与异步无关的 —— 执行上下文栈是写在 ECMA-262 的规范中,需要遵守它的是浏览器的 JavaScript 引擎,比如 V8、Quantum 等。event loop 的是写在 HTML 的规范中,需要遵守它的是各个浏览器,比如 Chrome、Firefox 等。
event loop
定义
我们通过 HTML5规范 的定义来看 event loop 的定义来看模型,本章节所有引用的部分都是翻译自规范。
本文只关注浏览器部分,所以忽略 Web Worker。JavaScript 引擎并不是独立运行的,它需要运行在宿主环境中, 所以其实用户代理(user agent)在这个情境下更好的翻译应该是运行环境或者宿主环境,也就是浏览器。
关于 unit of related similar-origin browsing contexts,节选一部分规范的介绍:
简而言之就是一个浏览器环境(unit of related similar-origin browsing contexts.),只能有一个事件循环(event loop)。
event loop 又是干什么的呢?
可以看到,一个页面只有一个 event loop,但是一个 event loop 可以有多个 task queues。
来自相同的 task source 的 task 将会被排入相同的 task queue,但是规范说来自不同 task sources 的 tasks 可能会被排入到不同的 task queues 中,也就是说一个 task queue 中可能排列着来自不同 task sources 的 tasks,但是具体什么 task source 对应什么 task queue,规范并没有具体说明。
但是规范对 task source 进行了分类:
一般我们看个各个文章中对于 task queue 的描述都是只有一个,不论是网络,用户时间内还是计时器都会被 Web APIs 排入到用一个 task queue 中,但事实上规范中明确表示了是有多个 task queues,并举例说明了这样设计的意义:
接着看。
这句话很关键,是用户代理(宿主环境/运行环境/浏览器)来控制任务的调度,这里就引出了下一章的 Web APIs。
接下来我么来看看 event loop 是如何执行 task 的。
处理模型
我们可以形象的理解 event loop 为如下形式的存在:
event loop 会在整个页面存在时不停的将 task queues 中的函数拿出来执行,具体的规则如下:
microtask
规范引出了 microtask,
规范只介绍了 solitary callback microtasks,compound microtasks 可以先忽略掉。
microtasks 检查点
整个流程如下图:
task & microTask
以下是 task 和 microTask 的分类,规范中有明确的写道,比如 MutationObserver,在这里引用 Promise A+ 规范翻译 中的分类:
task
task 主要包含:
microtask
microtask 主要包含:
Web APIs
在上一章讲讲到了用户代理(宿主环境/运行环境/浏览器)来控制任务的调度,task queues 只是一个队列,它并不知道什么时候有新的任务推入,也不知道什么时候任务出队。event loop 会根据规则不断将任务出队,那谁来将任务入队呢?答案是 Web APIs。
我们都知道 JavaScript 的执行是单线程的,但是浏览器并不是单线程的,Web APIs 就是一些额外的线程,它们通常由 C++ 来实现,用来处理非同步事件比如 DOM 事件,http 请求,setTimeout 等。他们是浏览器实现并发的入口,对于 Node.JavaScript 来说,就是一些 C++ 的 APIs。
WebAPIs 提供了多线程来执行异步函数,在回调发生的时候,它们会将回调函数和推入任务队列中并传递返回值。
流程
至此,我们已经了解了执行上下文栈,event loop 及 WebAPIs,它们的关系可以用下图来表示(图片来自网络,原始出处已无法考证),一轮 event loop 的文字版流程如下:
首先执行一个 task,如果整个第一轮 event loop,那么整体的 script 就是一个 task,同步执行的代码会直接放进 call stack(调用栈)中,诸如 setTimeout、fetch、ajax 或者事件的回调函数会由 Web APIs 进行管理,然后 call stack 继续执行栈顶的函数。当网络请求获取到了响应或者 timer 的时间到了,Web APIs 就会将对应的回调函数推入对应的 task queues 中。event loop 不断执行,一旦 event loop 中的 current task 为 null,它就回去扫 task queues 有没有 task,然后按照一定规则拿出 task queues 中一个最早入队的回调函数(比如上面提到的以 75% 的几率优先执行鼠标键盘的回调函数所在的队列,但是具体规则我还没找到),取出的回调函数放入上下文执行栈就开始同步执行了,执行完之后检查 event loop 中的 microtask queue 中的 microtask,按照规则将它们全部同步执行掉,最后完成 UI 的重渲染,然后再执行下一轮的 event loop...
应用
setTimeout 的不准确性
了解了上面 Web APIs,我们知道浏览器中有一个 Timers 的 Web API 用来管理 setTimeout 和 setInterval 等计时器,在同步执行了 setTimeout 后,浏览器并没有把你的回调函数挂在事件循环队列中。 它所做的是设定一个定时器。 当定时器到时后, 浏览器会把你的回调函数放在事件循环中, 这样, 在未来某个时刻的 tick 会摘下并执行这个回调。
但是如果定时器的任务队列中已经被添加了其他的任务,后面的回调就要等待。
这个例子中,打印出来的时间戳就不会等于 300,虽然两个 setTimeout 的函数都会在时间到了时被 Web API 排入任务队列,然后 event loop 取出第一个 setTimeout 的回调开始执行,但是这个回调函数会同步阻塞一段时间,导致只有它执行完毕 event loop 才能执行第二个 setTimeout 的回调函数。
进入调用栈的时机
例1
回到最开始的那个问题,整个过程是这样的:执行到
setTimeout
时先同步地将回调函数注册给 Web APIs 的 timer,要清楚此时 setTimeout 的回调函数此时根本没有入调用栈甚至连 task queue 都没有进入,所以 try 的这个代码块就执行结束了,没有抛出任何 error,catch 也被直接跳过,同步执行完毕。等到 timer 的计时到了(要注意并不一定是下一个 event loop,因为 setTimeout 在每个浏览器中的最短时间是不确定的,在 Chrome 中执行几次也会发现每次时间都不同,0 ms ~ 2 ms 都有),会将 setTimeout 中的回调放入 task queue 中,此时 event loop 中的 current task 为 null,就将这个回调函数设为 current task 并开始同步执行,此时调用栈中只有一个全局上下文,try catch 已经结束了,就会直接将这个 error 丢出。
例2
正确答案是立即输出 “0 1 2 3 4”,setTime 的第一个参数接受的是一个函数或者字符串,这里第一个参数是一个立即执行函数,返回值为 undefined,并且在立即执行的过程中就输出了 "0 1 2 3 4",timer 没有接收任何回调函数,就与 event loop 跟无关了。
例3
是阮老师推特上的一道题,首先 Promise 构造函数中的对象同步执行(不了解 Promise 的同学可以先看下 这篇文章),碰到
resolve(1)
,将当前 Promise 标记为 resolve,但是注意它 then 的回调函数还没有被注册,因为还没有执行到 a 处。继续执行又碰到一个 Promise,然后也立刻被 resolved 了,并且执行它的 then 注册,将第二个 then 的回调函数推入空的 microtaskQueue 中。继续执行输出一个 4,然后 a 处的 then 现在才开始注册,将第一个 Promise 的 then 回调函数推入 microtaskQueue 中。继续执行输出一个 3。现在 task queue 中的任务已经执行完毕,到了 microtask checkpoint flag,发现有两个 microtask,按照添加的顺序执行,第一个输出一个 2,第二个输出一个 1,最后再更新一下 UI 然后这一轮 event loop 就结束了,最终的输出是"4 3 2 1"Vue
笔者本人并没有使用过 Vue,但是稍微知道一点 Vue 的 DOM 更新中有批量更新,缓冲在同一事件循环中的数据变化,即 DOM 只会被修改一次。
关于这点 顾轶灵 大佬在知乎上有过 回答:
在 event loop 那章的规范中明确的写到,在 event loop 的一轮中会按照 task -> microTask -> UI render 的顺序。用户的代码可能会多次修改数据,而这些修改中后面的修改可能会覆盖掉前面的修改,再加上 DOM 的操作是很昂贵的,一定要尽量减少,所以要将用户的修改 thunk 起来然后只修改一次 DOM,所以需要使用 microTask 在 UI 更新渲染前执行,就算有多次修改,也会只修改一次 DOM,然后进行渲染。
其实用什么具体的 API 不是最关键的,重要的是使用 microTask 在 在 UI render 前进行 thunk。
参考
The text was updated successfully, but these errors were encountered: