blog
blog copied to clipboard
从Promise来看JavaScript中的Event Loop、Tasks和Microtasks
看到过下面这样一道题:
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()
为什么输出结果是1,2,3,5,4
而非1,2,3,4,5
?
比较难回答,但我们可以首先说一说可以从输出结果反推出的结论:
-
Promise.then
是异步执行的,而创建Promise实例(executor
)是同步执行的。 -
setTimeout
的异步和Promise.then
的异步看起来 “不太一样” ——至少是不在同一个队列中。
相关规范摘录
在解答问题前,我们必须先去了解相关的知识。(这部分相当枯燥,想看结论的同学可以跳到最后即可。)
Promise/A+
规范
要想找到原因,最自然的做法就是去看规范。我们首先去看看Promise的规范。
摘录promise.then
相关的部分如下:
promise.then(onFulfilled, onRejected)
2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.
规范要求,onFulfilled
必须在 执行上下文栈(execution context stack) 只包含 平台代码(platform code) 后才能执行。平台代码指 引擎,环境,Promise实现代码。实践上来说,这个要求保证了onFulfilled
的异步执行(以全新的栈),在then
被调用的这个事件循环之后。
规范的实现可以通过 macro-task 机制,比如setTimeout
和 setImmediate
,或者 micro-task 机制,比如MutationObserver
或者process.nextTick
。因为promise的实现被认为是平台代码,所以可以自己包涵一个task-scheduling
队列或者trampoline
。
通过对规范的翻译和解读,我们可以确定的是promise.then
是异步的,但它的实现又是平台相关的。要继续解答我们的疑问,必须理解下面几个概念:
- Event Loop,应该算是一个前置的概念,理解它才能理解浏览器的异步工作流程。
- macro-task 机制和 micro-task 机制,这组概念很新,之前根本没听过,但却是解决问题的核心。
Event Loop
规范
HTML5规范里有Event loops这一章节(读起来比较晦涩,只关注相关部分即可)。
- 每个浏览器环境,至多有一个event loop。
- 一个event loop可以有1个或多个task queue。
- 一个task queue是一列有序的task,用来做以下工作:
Events
task,Parsing
task,Callbacks
task,Using a resource
task,Reacting to DOM manipulation
task等。
每个task都有自己相关的document,比如一个task在某个element的上下文中进入队列,那么它的document就是这个element的document。
每个task定义时都有一个task source,从同一个task source来的task必须放到同一个task queue,从不同源来的则被添加到不同队列。
每个(task source对应的)task queue都保证自己队列的先进先出的执行顺序,但event loop的每个turn,是由浏览器决定从哪个task source挑选task。这允许浏览器为不同的task source设置不同的优先级,比如为用户交互设置更高优先级来使用户感觉流畅。
Jobs and Job Queues
规范
本来应该接着上面Event Loop的话题继续深入,讲macro-task和micro-task,但先不急,我们跳到ES2015规范,看看Jobs and Job Queues
这一新增的概念,它有点类似于上面提到的task queue
。
一个Job Queue
是一个先进先出的队列。一个ECMAScript实现必须至少包含以下两个Job Queue
:
Name | Purpose |
---|---|
ScriptJobs | Jobs that validate and evaluate ECMAScript Script and Module source text. See clauses 10 and 15. |
PromiseJobs | Jobs that are responses to the settlement of a Promise (see 25.4). |
单个Job Queue
中的PendingJob总是按序(先进先出)执行,但多个Job Queue
可能会交错执行。
跟随PromiseJobs到25.4章节,可以看到PerformPromiseThen ( promise, onFulfilled, onRejected, resultCapability ):
这里我们看到,promise.then
的执行其实是向PromiseJobs
添加Job。
event loop怎么处理tasks和microtasks?
好了,现在可以让我们真正来深入task(macro-task)和micro-task。
认真说,规范并没有包括macro-task 和 micro-task这部分概念的描述,但阅读一些大神的博文以及从规范相关概念推测,以下所提到的在我看来,是合理的解释。但是请看文章的同学辩证和批判地看。
首先,micro-task在ES2015规范中称为Job。 其次,macro-task代指task。
哇,所以我们可以结合前面的规范,来讲一讲Event Loop(事件循环)是怎么来处理task和microtask的了。
- 每个线程有自己的事件循环,所以每个web worker有自己的,所以它才可以独立执行。然而,所有同属一个origin的windows共享一个事件循环,所以它们可以同步交流。
- 事件循环不间断在跑,执行任何进入队列的task。
- 一个事件循环可以有多个task source,每个task source保证自己的任务列表的执行顺序,但由浏览器在(事件循环的)每轮中挑选某个task source的task。
- tasks are scheduled,所以浏览器可以从内部到JS/DOM,保证动作按序发生。在tasks之间,浏览器可能会render updates。从鼠标点击到事件回调需要schedule task,解析html,setTimeout这些都需要。
- microtasks are scheduled,经常是为需要直接在当前脚本执行完后立即发生的事,比如async某些动作但不必承担新开task的弊端。microtask queue在回调之后执行,只要没有其它JS在执行中,并且在每个task的结尾。microtask中添加的microtask也被添加到microtask queue的末尾并处理。microtask包括
mutation observer callbacks
和promise callbacks
。
结论
定位到开头的题目,流程如下:
- 当前task运行,执行代码。首先
setTimeout
的callback被添加到tasks queue中; - 实例化promise,输出
1
; promise resolved;输出2
; -
promise.then
的callback被添加到microtasks queue中; - 输出
3
; - 已到当前task的end,执行microtasks,输出
5
; - 执行下一个task,输出
4
。
有一个问题就是,I/O, UI render 究竟是不是当做一个task来执行,看有的文章说是,有的说是在每个task之间进行的
@cendylee 就我看到的资料来说,好像是在task之间进行的。如果有更详细可靠的资料进行补充或更正当然更好。
Promise的then原型方法注册的回调确实是在microtask中注册执行的,但是我很好奇,node实现的process.nextTick,看源码似乎并不是由microtask驱动的,为啥网上到处都说process.nextTick也是属于microtask的一部分呢?
@hyj1991 从源码来看,process.nextTick
属于 microtask。
https://github.com/nodejs/node/blob/v7.x/src/node.cc#L4381
inline int Start(Isolate* isolate, IsolateData* isolate_data,
int argc, const char* const* argv,
int exec_argc, const char* const* exec_argv) {
...
{
Environment::AsyncCallbackScope callback_scope(&env);
LoadEnvironment(&env);
}
https://github.com/nodejs/node/blob/v7.x/src/node.cc#L3406
void LoadEnvironment(Environment* env) {
...
Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
"bootstrap_node.js");
Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
https://github.com/nodejs/node/blob/v7.x/lib/internal/bootstrap_node.js#L12
function startup() {
...
NativeModule.require('internal/process/next_tick').setup();
https://github.com/nodejs/node/blob/v7.x/lib/internal/process/next_tick.js#L49
function scheduleMicrotasks() {
if (microtasksScheduled)
return;
nextTickQueue.push({
callback: runMicrotasksCallback,
domain: null
});
tickInfo[kLength]++;
microtasksScheduled = true;
}
...
function nextTick(callback) {
if (typeof callback !== 'function')
throw new TypeError('callback is not a function');
// on the way out, don't bother. it won't get fired anyway.
if (process._exiting)
return;
var args;
if (arguments.length > 1) {
args = new Array(arguments.length - 1);
for (var i = 1; i < arguments.length; i++)
args[i - 1] = arguments[i];
}
nextTickQueue.push({
callback,
domain: process.domain || null,
args
});
tickInfo[kLength]++;
}
可以看到,安排microtask就是向nextTickQueue
队列压入callback
,而nextTick
同样是向nextTickQueue
队列压入callback
,可见,process.nextTick
属于 microtask。
20170629 更新代码路径
很清晰的文章!
我也是有同样的疑惑:到底UI render是在哪执行的?
博主您给出的HTML规范链接里面的这部分内容我反复读了,但是Processing model
的第5点Update the rendering之后说的 run the resize steps
、 run the scroll steps
这些步骤是什么意思呢?不可能每次microtask执行完就resize和scroll吧,所以请问博主你看懂这块了吗?
@Ma63d 你给的链接里描述很清晰,microtask跟你说的这些无关。
链接描述,一个event loop的典型步骤:
- Select the oldest task...
- Set the event loop's currently running task to the task selected in the previous step.
- Run: Run the selected task.
- Set the event loop's currently running task back to null.
- Remove the task that was run in the run step above from its task queue.
- Microtasks: Perform a microtask checkpoint.
- Update the rendering
- If this is a worker event loop...
- Return to the first step of the event loop.
Update the rendering是event loop的一个步骤,Microtasks也是event loop的一个步骤,在执行完Microtasks,浏览器开始Update the rendering,至于run the resize steps
等是Update the rendering里面的内容,跟Microtasks无关。
@creeperyang 嗯,我当时的表示有误,我问的意思是执行完所有的microtask之后执行的Update the rendering,不是说单独执行一个之后立马执行run the resize steps/run the scroll steps。我当时描述不准确,不好意思把你引入到了莫名的关注点上面去了。
我当时一开始没看懂,为什么Update the rendering竟然去执行resize窗口和scroll窗口。不可能每次task执行完,然后清空完microtask队列之后就让屏幕resize/scroll一次吧。所以评论了一开始的内容。
最近仔细看了一下run the scroll steps不是scoll窗口,每次我们scoll的时候视口或者dom就已经立即scroll了,并把document或者dom加入到 pending scroll event targets中,而run the scroll steps具体做的则是遍历这些target,在target上触发scroll事件。 run the resize steps也是相似的,这个步骤是触发resize事件。
至于后续的media query, run CSS animations and send events等等也是相似的,都是触发事件,第10步和第11步则是执行我们熟悉的requestAnimationFrame回调和IntersectionObserver回调(第十步还是挺关键的)。
第十二步才是对UI执行render,这里应该就是重排、重绘和把更改后的样式真正render改到dom上面去。
SetTimeout 内的回调属于 macrotask, 会在下一个 Event Loop 中执行
定位到开头的题目,流程如下:
- 当前task运行,执行代码。首先setTimeout的callback被添加到tasks queue中;
- 实例化promise,输出 1; promise resolved;输出 2;
- promise.then的callback被添加到microtasks queue中;
- 输出 3;
- 已到当前task的end,执行microtasks,输出 5;
- 执行下一个task,输出4。
关于第2,3步,不是promise.resolve()的时候,promise.then的callback已经被添加到microtask queue了吗?然后输出2,然后跳到第四步输出3.
@keenwon
为了让第2,3步更清晰一点,可以写成下面这样。
// 1
setTimeout();
// 2
var promise = new Promise(executor);
// 3
promise.then(callback)
// 4
console.log(3)
其中,得到Promise
的实例promise的时候,exectuor
作为参数传给Promise
的构造函数同步执行。所以输出了数字1
和2
。
构造函数执行完后,我们得到了promise
(它是resolved
)。
调用promise.then
,callback
被添加到microtasks的队列中。
console.log(3)
执行完后,当前执行栈为空,则开始执行microtasks。
@mqliutie
你这例子不对吧?你的 setTimeout
在 new Promise
内部呀,大兄弟;setTimeout
不执行 then
也永远不执行,这是有明显的先后顺序的
学习学习
应该是每个浏览器至少有一个 event loop 吧
补充关于 同步/异步/阻塞/非阻塞 的理解。搬迁自已废弃的#15,因为觉得和 event loop
强相关,放到一起便于参照。
Synchronize / Asynchronize / Block / Non-block 一个从分布式系统角度的理解
这一段主要来自 知乎 怎样理解阻塞非阻塞与同步异步的区别? 严肃的答案 ,并参照了 stackoverflow 的相关问题。
同步与异步,阻塞与非阻塞是两组概念,但容易混淆,比如同步不代表阻塞,同步也可以是非阻塞的。
同步与异步
-
同步和异步关注的是 消息通信机制 (synchronous communication/ asynchronous communication)。
-
所谓同步,就是在发出调用时,
- 在没有得到结果之前,调用不返回。
- 一旦调用返回,即得到返回值。
换句话说,就是由 调用者主动等待 这个调用的结果。
-
异步则是相反,调用在发出之后,调用就直接返回,但没有返回结果。
换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
阻塞与非阻塞
-
阻塞和非阻塞关注的是 程序在等待调用结果(消息,返回值)时的状态。
-
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
-
非阻塞调用指在不能立刻得到结果之前,该调用 不会阻塞当前线程。
举例
以你打电话让书店老板查找某本书为例来讲:
- 同步通信机制:老板说,“你稍等,我查下”,然后查好后(可能会5秒或者1天)告诉你结果(返回结果)。
- 异步通信机制:老板说,“我查好后打电话你”,然后直接挂电话(无返回结果)。查好后打电话你(“回电”这种方式回调/通知你)。
- 阻塞式调用:打电话给老板时,你会一直把自己“挂起”,直到结果返回。
- 非阻塞式调用:打电话给老板时,不管老板有没有告诉你,你自己先一边玩了,但可能会每过几分钟跟老板check一下有没有返回结果。
总结:阻塞与非阻塞 与 是否同步异步无关(跟老板通过什么方式回答你无关)。 所以,也可以说, 同步/异步 针对的是 通信机制(被调用方怎么通知调用方),阻塞/非阻塞 针对的是 调用方在等待结果时的状态。
此外,同步/异步 和 阻塞/非阻塞 可以相互组合 (from 吴昌明 的评论):
- 同步阻塞:你打电话问老板有没有某书,老板去查,在老板给你结果之前,你一直拿着电话等待老板给你结果,你此时什么也干不了。
- 同步非阻塞:你打电话过去后,在老板给你结果之前,你拿着电话等待老板给你结果,但是你拿着电话等的时候可以干一些其他事,比如嗑瓜子。
- 异步阻塞:你打电话过去后,老板去查,你挂掉电话,等待老板给你打电话通知你,这是异步,你挂了电话后还是啥也干不了,只能一直等着老板给你打电话告诉你结果,这是阻塞。
- 异步非阻塞:你打电话过去后,你就挂了电话,然后你就想干嘛干嘛去。只用时不时去看看老板给你打电话没。
这个回答我觉得是很清晰,易于理解的解释,不过下面还是会列出一些其它角度的解释,方便对照吧。
Asynchronous vs synchronous execution, what does it really mean?
这是关于同步/异步执行的理解,几个高票答案总结下可知:
- 同步执行是每个task必须结束后才能继续下个task,前后task是顺序的,有依赖的。
- 异步执行是多个task可以并行,相互不依赖。
这也是一个角度,可参照。
是否可以这样认为,同步异步是两者的通信方式,CPU 同 I/O 或数据库。阻塞非阻塞是个体的工作方式,进程的单线程、多线程。单线程工作方式的进程就是阻塞的,多线程工作方式的进程就是非阻塞的。
NodeJS 应该是利用了事件循环既实现了两者间的异步通信,又解决了单线程的阻塞难题。(对比时分复用,将单线程按事件划分成虚拟的多线程,但是对CPU密集程序来说它还是阻塞的)
@szouc 阻塞/非阻塞和单/多线程没有对应关系,同步异步和线程也没有关系。
在Node.js中,JavaScript是单线程的,所以必然不能使用阻塞IO(阻塞就没法实现高并发)。
Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.
通过libuv,Node.js 在多平台支持了非阻塞IO:
- Network IO,依赖Linux上的epoll,OSX和BSD类OS上的kqueue,SunOS上的event ports以及Windows上的IOCP等内核事件通知机制。
- File IO,使用thread pool (线程池,多线程)。
@creeperyang 请教一下
- Promise.new代码在event loop中的task中调度
- 事件循环tick,promise.resolve被调用,then之后的程序添加到之后的microtasks队列中
- 在microtasks中,then代码执行,返回一个新的promise(等待事件)
- 事件循环tick,该Promise的resolve被调用, then之后的代码被添加到之后的microtask队列中
- 在microtasks中,then代码执行,返回一个值,之后的then代码被追加到当前的microtask队列之后。
- 继续在microtask中执行then代码
是这样吗?
@1msoft 你的意思是对的,可能描述有点问题。
- tasks 队列的第一个任务取出,开始执行。
- 执行直到当前 stack 为空,于是去检查 microtasks 队列。
- 依次执行 mocrotasks 队列的所有任务,直到 mocrotasks 队列为空,转 1 。
如上循环中,注意到第 2 步中执行时可以向 microtasks 队列压入 microtask。
(function test() {
// 1⃣️ task A 执行中
// 2⃣️ tasks 队列压入新的 task B
setTimeout(() => {
// microtasks 队列为空,于是检查 tasks 队列,取出 B并执行了
console.log(4)
}, 0)
new Promise(resolve => {
console.log(1)
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve()
}
// 3⃣️ task A 继续执行
console.log(2)
})
// 4⃣️ microtasks 队列压入 microtask a
.then(() => {
// 6⃣️ microtask a 执行中
console.log(5)
Promise.resolve(7)
// 7⃣️ microtasks 队列压入 microtask b
.then(v => console.log(v))
// microtask a 执行完毕
})
// 8⃣️ microtasks 队列压入 microtask c
// 这个 then 执行完后继续检查 microtasks 队列,并一次执行 b,c
.then(() => {
console.log(6)
})
// 5⃣️ task A 执行完毕,检查 microtasks 队列,发现非空,执行 microtasks 队列的第一个 microtask a
console.log(3)
})()
/**
1
2
3
5
7
6
4
*/
提出自己的一点理解: scheduleMicrotasks 处理的是emitPendingUnhandledRejections的问题,并不是处理普通的promise的resolve 和reject或者promise.then的逻辑。 process.nextTick是由于C++层的makecallback 函数触发的,进入_tickCallback 中,代码写死了执行顺序,首先执行所有process.nextTick中的回调队列,然后执行_runMicrotasks(),_runMicrotasks才是跟promise有关的部分。 严格说process.nextTick肯定不属于Microtasks,但是_runMicrotasks和nextTick是在同一个时机执行的。 这个执行的时机就是makecallback ,makecallback 又是macro-task队列中一个task ready情况下执行的。
normally _runMicrotasks() should be executed at the end of some phase of the current tick. _runMicrotasks()是每次执行的时候都把此时的所有settled promises中的then回调函数都运行一次吗? 按理说是这样的,但是我没有看c++_runMicrotasks实现的代码。
@kkshaq 昨天晚上看了下相关代码,有点变化,我今天看完再和你讨论下,谢谢。
先找到 require('internal/process/next_tick').setup()
,这是初始化 next_tick 功能。
// 调用 c++ 的 SetupNextTick
const tickInfo = process._setupNextTick(_tickCallback, _runMicrotasks);
// 设置 _runMicrotasks,对应 c++ 的 RunMicrotasks
_runMicrotasks = _runMicrotasks.runMicrotasks;
然后找到 c++ 中 SetupNextTick
做了哪些工作?
void SetupNextTick(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsFunction());
CHECK(args[1]->IsObject());
// _tickCallback 被作为 tick_callback_function
env->set_tick_callback_function(args[0].As<Function>());
// RunMicrotasks 可以被 JS 调用
env->SetMethod(args[1].As<Object>(), "runMicrotasks", RunMicrotasks);
// ...
// Values use to cross communicate with processNextTick. (返回给JS代码)
uint32_t* const fields = env->tick_info()->fields();
uint32_t const fields_count = env->tick_info()->fields_count();
// ...
}
// TickInfo
class TickInfo {
public:
inline uint32_t* fields();
inline int fields_count() const;
inline uint32_t index() const;
inline uint32_t length() const;
inline void set_index(uint32_t value);
private:
friend class Environment; // So we can call the constructor.
inline TickInfo();
enum Fields {
kIndex,
kLength,
kFieldsCount
};
uint32_t fields_[kFieldsCount];
DISALLOW_COPY_AND_ASSIGN(TickInfo);
};
最后详细看 _tickCallback
:
function _tickCallback() {
do {
while (tickInfo[kIndex] < tickInfo[kLength]) {
++tickInfo[kIndex];
const tock = nextTickQueue.shift();
const callback = tock.callback;
const args = tock.args;
// CHECK(Number.isSafeInteger(tock[async_id_symbol]))
// CHECK(tock[async_id_symbol] > 0)
// CHECK(Number.isSafeInteger(tock[trigger_id_symbol]))
// CHECK(tock[trigger_id_symbol] > 0)
nextTickEmitBefore(tock[async_id_symbol], tock[trigger_id_symbol]);
// emitDestroy() places the async_id_symbol into an asynchronous queue
// that calls the destroy callback in the future. It's called before
// calling tock.callback so destroy will be called even if the callback
// throws an exception that is handles by 'uncaughtException' or a
// domain.
// TODO(trevnorris): This is a bit of a hack. It relies on the fact
// that nextTick() doesn't allow the event loop to proceed, but if
// any async hooks are enabled during the callback's execution then
// this tock's after hook will be called, but not its destroy hook.
if (async_hook_fields[kDestroy] > 0)
emitDestroy(tock[async_id_symbol]);
// Using separate callback execution functions allows direct
// callback invocation with small numbers of arguments to avoid the
// performance hit associated with using `fn.apply()`
_combinedTickCallback(args, callback);
nextTickEmitAfter(tock[async_id_symbol]);
if (kMaxCallbacksPerLoop < tickInfo[kIndex])
tickDone();
}
tickDone();
_runMicrotasks();
emitPendingUnhandledRejections();
} while (tickInfo[kLength] !== 0);
}
可以看到,_tickCallback
执行时不断取 nextTickQueue
中元素并执行。执行完以后,执行 _runMicrotasks()
,也就是执行 microtasks 。从这个角度来说,nextTick 和 microtask 是同一层级的。
最后,我们注意到 next_tick 的初始化中,首先调用了 require('internal/process/promises')
的初始化,并且传了个函数 scheduleMicrotask
。
scheduleMicrotask
干嘛用的呢?在处理 PendingUnhandledRejection 时被调用,用来把 microTasksTickObject
放入 nextTickQueue
。
问下 UI render 在哪个阶段?
@rubyless https://github.com/creeperyang/blog/issues/21#issuecomment-284234856
在浏览器的 event loop 中, UI redner 发生在 microtasks 执行完成之后。
有个问题, 既然process.nextTick会压入microtask那么为什么下面的代码会先输出3而不是2呢
new Promise((res) => {
console.log(1)
res()
}).then(() => console.log(2))
process.nextTick(() => console.log(3))
@lastIndexOf 楼主在上面的回复里贴了process.nextTick()的源码了,nextTick不是event loop的一部分,更不是microtask,它有自己的队列,在当前循环中立即执行,执行到最后再_runMicrotasks()。
@zhanba 谢谢了
@zhanba 基本是这样的。
@lastIndexOf https://github.com/nodejs/node/issues/2736#issuecomment-138607657
function _tickCallback() {
do {
// 便利执行当前 nextTickQueue
while (tickInfo[kIndex] < tickInfo[kLength]) {
++tickInfo[kIndex];
// ......
_combinedTickCallback(args, callback);
// ......
if (kMaxCallbacksPerLoop < tickInfo[kIndex])
tickDone();
}
tickDone();
_runMicrotasks(); // 执行 v8 的 microtasks
emitPendingUnhandledRejections();
} while (tickInfo[kLength] !== 0);
}
再次回顾一下 _tickCallback
,可以看到,nextTick 被先处理了,然后 micro tasks 才被处理。
- 两个循环,可以保证 promise.then 里再次添加的 nextTick callback 在这个循环被处理。
- 内层的循环保证 nextTick callbacks 被首先处理。
-
_runMicrotasks()
调用 v8 的RunMicrotasks
,处理 v8 的 microtasks。而从实际效果来说,process.nextTick 被看作 microtask 没有问题。
请问ajax的回调是属于macrotask吗?
@silhouettesia 是的。
那用各种库封装的基于promise的ajax就变成了microtask?
@silhouettesia 当然不会。把 Ajax 封装成 promise 形式,只是 API 使用形式的变化,底层仍然基于事件注册和回调。
new Promise((res,rej)=>{
var req=new XMLHttpRequest();
req.open('open',url);
req.onreadystatechange=()=>{
if(req.readyState==4&&req.status==200){
res();
}
}
req.send();
}.then(()=>{
console.log('then');
});
如果在代码里有这样一段,这个ajax回调是在下一个事件循环的macrotask,如果在这段代码后面有很多microtask,即使ajax数据已经返回但这个回调必须等所有的microtask都执行完了进入下一事件循环才会resolve是吗?
还有一个问题是,新的fetch api是macrotask还是microtask呢?
@silhouettesia fetch/XMLHttpRequest
需要和服务器交互,必然是异步的,且是 macrotask 的。
很直观的经验:和服务器交互的时间可能很长,怎么会让当前 event loop 必须等到服务器返回才结束。
Promise
本身也是异步的,且不同实现可能会是 macrotask 或者 microtask,但一般是 microtask 的。
new Promise((resolve)=>{
// 1. 同步 resolve,但实际是当前执行栈(execution context stack)为空(准确说只有平台代码)后才会调用 then 注册的回调——microtask
resolve()
// 2. setTimeout 本身已经把 resolve 延迟到下个 event loop 执行了。
setTimeout(resolve)
}
@creeperyang 谢谢解答 还得再深入了解Promise的实现...
我想问下
Promise 本身也是异步的,且不同实现可能会是 macrotask 或者 microtask,但一般是 microtask 的。 到底在什么情况会是 macrotask的?
new Promise(function (resolve) {
setTimeout(resolve,0)
}).then(function() {
console.log('then')
});
是类似这种的吗
@channg 对。
@channg promise构造函数在stack里,虽然promise.then是microtask,setTimeout是macrotask,但是promise.then只有resolve或reject了才会触发(才会进入queue)。
@qingmingsang 所以我可以理解,从微观角度 promise.then是microtask 但是宏观上,那是与setTimeout处于同一个macrotask
@creeperyang 我理解的是event loop是有两个的队列,task和job,他们都遵循着先进先出,每执行完一个task后会去执行job,job执行完成后再去执行task,以此类推
@creeperyang 大概的一个伪代码就是
const job = [];
const task = [];
// 一些同步操作
something();
// event loop
job.forEach(v => v());
// job 执行完
task.forEach(v=> {
v();
job.forEach(v => v()); //在 loop 期间会有新任务加入也会执行,
})
执行策略的优化就是看哪些属于 job,哪些属于 task,然后根据业务来进行优化吧,尽可能的让视图渲染放到 job中,promise.then 是 job,setTimeout 是 task
<script type="text/javascript">
setTimeout(function(){
console.log(1);
})
Promise.resolve().then(function(){
console.log(2);
})
console.log(3);
document.write('<script type="text/javascript">console.log(4)<\/script>');
</script>
<script type="text/javascript">
document.write('<script type="text/javascript">console.log(5)<\/script>');
console.log(6)
</script>
结果是什么?
请教博主一个问题,烦请指教!当我反复查看html规范时候
An event loop has one or more task queues. A task queue is an ordered list of tasks, which are algorithms that are responsible for such work as:
- Events Dispatching an Event object at a particular EventTarget object is often done by a dedicated task. 2.Parsing The HTML parser tokenizing one or more bytes, and then processing any resulting tokens, is typically a task. ....
规范里面说到event loop有多个task queues.一个task queue就是任务的列表,它们负责以下工作。 看里很久有两个疑惑,恳请解答 1.dispatching an event 不是浏览器有专门的线程监听各种事件吗?然后相应的event handler 会进入task queues.为什么规范里说:是任务队列Dispatching an Event。因为任务队列里的任务都会被JavaScript engines执行,也就是说是JavaScript engines在触发事件? 2.html解析不是通过渲染引擎来解析的吗,怎么这里又说是JavaScript engines解析?在一次event loop中,如果页面需要更新。这时候会停止JavaScript引擎,启动渲染引擎来更新页面吗? 谢谢了 @creeperyang
new Promise(resolve => {
resolve(1);
console.log(4)
// new Promise(r => r(2)).then(e => console.log(e))
Promise.resolve(2).then(e => console.log(e))
Promise.resolve(5).then(e => console.log(e))
}).then(v => console.log(v))
console.log(3)
在 Promise 中, 如果还有 promise, 那么先得把内部的 promise 顺序执行完,然后执行外面的 promise? 我是根据得到的结果推理出来的, 暂时没看到对应的文档说明, issue 主怎么看?
@googya
// 执行第 1 步
const p = new Promise(resolve => {
resolve(1);
// 第 1.1 步,首先输出 4
console.log(4)
// 第 1.2 步,压入microtask 队列
Promise.resolve(2).then(e => console.log(e))
// 第 1.3 步,压入microtask 队列
Promise.resolve(5).then(e => console.log(e))
})
// 执行第 2 步,压入microtask 队列
p.then(v => console.log(v))
// 执行第 3 步
console.log(3)
所以输出结果:
4 --> 3,然后microtask队列支持输出 2 --> 5 --> 1
@creeperyang 步骤分解之后是更清楚一点。 我之前的疑问是, 为什么 1.2, 1.3 先于步骤2,只是因为 1.2 1.3 在构造器中, 先执行的吧
标题:从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理 作者:Lichun Dai
一篇从浏览器进程/线程角度来解释浏览器环境中JS运行机制的文章,通俗易懂,对理解浏览器环境的 event loop 很有帮助。
核心知识点:
-
现代浏览器(如Chrome)每个tab页对应一个独立进程,有独立的渲染引擎(浏览器内核)实例,内核一般包括以下线程:
+-------------------------------+ |+-----------------------------+| || GUI渲染线程 || || || |+-----------------------------+| | | |+-----------------------------+| || JS引擎线程 || || || |+-----------------------------+| | | |+-----------------------------+| || 事件触发线程 || || || |+-----------------------------+| | | |+-----------------------------+| || 定时器触发线程 || || || |+-----------------------------+| | | |+-----------------------------+| || 异步http请求线程 || || || |+-----------------------------+| | | +-------------------------------+
-
JS 线程用于解析和执行 JavaScript。图中显见:一个渲染引擎进程只有一个 JS 线程——JS 是单线程的。那么JS的异步能力是谁赋予的?JS引擎本身只能不断地去检索任务队列,取出开头的任务去执行(即所谓的event loop),那么
setTimeout
/事件回调/ajax请求回调 等异步能力仅仅靠 JS 线程本身是做不到的,上面的图正好可以让初学者们恍然大悟:setTimeout 等本质上是 JS 线程调用了其它线程,其它线程在条件达成时把任务塞入队列。
-
UI 线程和 JS 线程是互相阻塞的,所以不要让 JS 线程长时间运行,从而让 UI 线程被长时间挂起,使 UI 渲染掉帧甚至不响应。
我最近正好看到这个地方,颇有疑惑之处,首先,node的makecallback 有调用 env()->tick_callback_function()->Call;所以我刚开始感觉nextTick是在事件循环的回调中调用。但是实际上比如用setTimeout(fn,0)这个是标准的libuv timer然后回调执行fn,那应该是在执行完fn以后再执行nexttick,但实际不是,所以我比较同意是Microtasks来执行的,而且应该不是在同一个线程上,因为node启动platform的时候用了4个线程做线程池,然后再node::start里uv_run前面有个PumpMessageLoop更加重了我的猜测,就是这个 是microtasks来做的nexttick。而且如果我通过执行自己的embed函数 在函数中直接运行uv_run,不通过node::start里面的uv_run执行,nextTick是不会被执行的。
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
这篇文章里讲清楚了
楼主能说下 tick和loop是啥区别吗?
<script type="text/javascript"> setTimeout(function(){ console.log(1); }) Promise.resolve().then(function(){ console.log(2); }) console.log(3); document.write('<script type="text/javascript">console.log(4)<\/script>'); </script> <script type="text/javascript"> document.write('<script type="text/javascript">console.log(5)<\/script>'); console.log(6) </script>
结果是什么?
342561
楼主好,根据最近一次的新闻,WHATWG全权接管了HTML和DOM的标准制定权(链接)。 您在博文中HTML5标准的引用就有点过时了。而且根据最新标准制定Task Queue不是一个队列,它是一个任务集合(A task queue is a set of tasks)。 与时俱进一下。
单个Job Queue中的PendingJob总是按序(先进先出)执行,但多个Job Queue可能会交错执行。
终于解答了最近被一小段代码的困扰:
var c1 = (r) => {
console.log(r)
}
var c2 = () => {
console.log(3)
}
console.log('promise1')
var p3
var p1 = Promise.resolve()
.then(function() {
console.log(0)
p3 = Promise.resolve(6)
return p3
})
.then(c1)
console.log('promise2')
var p2 = Promise.resolve()
.then(function() {
console.log(1)
})
.then(function() {
console.log(2)
})
.then(function(){
console.log('before 3')
c2()
console.log('after 3')
}).then(function() {
console.log(5)
})
/**
log:
promise1
promise2
0
1
2
6
before 3
3
after 3
5
*/
在低版本Node中,输出结果如上,而高版本我用的V12,则6会在after 3后面输出。
但是规范里并没有说,多个Job Queue如何调度(可能有交叉调度),这也导致没法正确推导代码输出结果。哪位有了解多个Job Queue的调度方式,欢迎告知或提供学习资料,感谢!
浏览器 event loop 简易归纳:
关于 taskQueue
- 首先浏览器可以拥有不止一个 taskQueue。易于理解的一个解释是,不同 taskQueue 可以放不同类型的 task,比如处理鼠标/键盘事件的可以放一个 taskQueue,便于优先响应用户操作。(但不能饿死其它 taskQueue)。
- taskQueue 不是 queue,而是 set。每次取最先可以运行的 task,而不是队列头部的 task。
关于运行流程(已简化)
- 取一个 taskQueue,并且 taskQueue 有 runnable 的 task(没有则跳到 microtask 那一步);
- 从 taskQueue 取出当前要运行的 task 并执行;
- 执行完检查 microtask queue 是否不为空,不为空则 dequeue microtask 执行,直到 microtask queue 为空;
- 执行 UI 更新(Update the rendering):
- 理解"Rendering opportunities",比如 60HZ 刷新率下,那么1秒内最多有 60次 Rendering opportunities;如果浏览器没法满足60Hz,那么可能就会降级到30Hz(30次opportunities)而不是降几次;如果浏览器tab页不可见,那么甚至会降到4Hz。
- rendering 的顺序:处理autofocus-->resize-->scroll-->media query-->update animation-->fullscreen-->animation frame callback --> intersection observation --> mark painting timing
- 更新完UI后,看看是不是有机会触发 Idle Callback。
一些重要的执行顺序
Promise --> requestAnimationFrame --> Paint --> requestIdleCallback
一个有意思的实例
Vue.nextTick
为什么能保证在DOM更新后执行? 具体情况见异步更新队列。
- 从
watcher
代码来看,vue 的数据绑定机制可以监听 vm 数据的更新,但不会同步更新DOM,而是内部调用queueWatcher
,把数据对应的 watcher 塞入队列,并调用nextTick
(仅一次,保证队列后面会被处理,即更新DOM,而后面vm再变动,只是塞入队列); - 所以vm的更新也是调用
Vue.nextTick
,都是通过 Promise 或其它API 生成 microtask; - 因为microtask 队列的先后关系,vm 的更新导致的 DOM 更新被先处理,所以可以通过
Vue.nextTick
保证访问到最新的 DOM 状态; - 最后明确:DOM 更新,和屏幕上的UI更新不是一个概念。DOM 更新可以理解为 JS 的执行,真正的 Paint 时机仍然是浏览器按规则执行——最终符合上一节的执行顺序。
这两个执行结果bing'bu'y并不一样,大佬能解释一下原理吗
@spademan 我实际测了下,直接btn.click()
和 页面点击按钮输出不一样。
可以看到,btn.click()
时,两个 handler 显然合并在同一个 event loop 执行了;而在页面里点击按钮则不一样。这个是浏览器自身实现细节,我没有查到相关文档,估计要去看相关代码才能知道原因了。
我的猜测是:用户点击,是属于用户交互,浏览器认为在UI上需要立即反馈,于是每处理完一个handler,立即更新页面UI。(看看就行,别当真 😸 有官方解释可告知我)
@spademan 用户点击,事件是异步触发,点击之后
-
Promise.resolve()
, 然后往微任务队列里面放一个任务(log('M1')) - 打印 'L1'
- 这时候 call stack 是空闲的,会执行微任务队列里的任务,打印 'M1'
- 第二个事件处理程序也是同样如此
用程序触发 click, 事件是同步触发
-
btn.click()
进入 call stack -
Promise.resolve()
, 然后往微任务队列里面放一个任务 - 打印 'L1'
- 此时,call stack 并没有空闲,click 函数还在里面,所以不会执行微任务队列里的任务
- 执行第二个事件处理程序,
Promise.resolve()
,然后往微任务队列里面放一个任务(log('M2')) - 打印 'L2'
- 事件处理程序结束,
click
函数移出 call stack - 开始执行微任务队列里的任务,打印 'M1', 'M2'
不晓得对不对。。。。
![]()
这两个执行结果bing'bu'y并不一样,大佬能解释一下原理吗 @creeperyang @zlv2s 找到一个相对深入一点的解释 https://stackoverflow.com/questions/55709512/why-is-there-a-difference-in-the-task-microtask-execution-order-when-a-button-is