document.body.innerHTML = `<button id="btn" type="button">btn</button>`;const button = document.getElementById('btn')button.addEventListener('click',()=>{Promise.resolve().then(()=>console.log('promise resolved 1'))console.log('listener 1')})button.addEventListener('click',()=>{Promise.resolve().then(()=>console.log('promise resolved 2'))console.log('listener 2')})// 1. 手动点击按钮// button.click() // 2. 解开这句注释,用JS触发点击行为
A. listener1 -> promise resolved 1 -> listener2 -> promise resolved 2 B. listener1 -> listener2 -> promise resolved 1 -> promise resolved 2
被动接受知识
人工点击按钮

从上图中我们可以看到,一次点击事件之后,浏览器会调用Function Call进入JS引擎,执行listener1,输出
listener1
。弹栈时发现JS调用栈为空,这时候就会执行Microtasks队列中的所有 Microtask,输出
promise resolved 1
。接着浏览器调用Function Call进入JS引擎,执行listener2,输出
listener 2
。弹栈时发现JS调用栈为空,这时候就会执行Microtasks队列中的所有Microtask,输出 promise resolved 2
。

从上图中我们可以看到,浏览器运行JS代码时,调用了button.click 这个函数
进入事件处理,执行listener1,输出
listener1
。弹栈时发现JS调用栈非空(button.click函数还在运行)
执行listener2,输出
listener 2
。弹栈时发现JS调用栈为空,这时候就会执行Microtasks队列中的所Microtask,输出 promise resolved 1
、promise resolved 2
。
探索工具
Chrome Devtool中的Performance是一个sample profiler (采样分析仪),即它的运行机制是每1ms暂停一下vm,将当前的调用栈记录下来,最后利用这部分信息做出可视化。
使CPU变慢:在Devtool中打开 "CPU 6x slowdown" 在要探索的函数中执行一段比较长的for循环占用CPU时间(如上面的 heavy)
主动探索V8源码
人工点击按钮

从上图中我们可以看到,一次点击事件之后,Blink(Blink是一个渲染引擎,Chrome的Renderer进程中的主线程大部分时间会在Blink和V8两者切换)会调用v8.callFunction进入V8引擎,执行listener1,输出
listener1
。返回 Blink 时发现V8调用栈为空,这时候就会执行V8.RunMicrotasks执行 Microtasks队列中的所有Microtask,输出
promise resolved 1
。Blink调用v8.callFunction进入 V8 引擎,执行listener2,输出
listener 2
。返回Blink时发现V8调用栈为空,这时候就会执行Microtasks队列中的所有 Microtask,输出
promise resolved 2
。
注意,chrome://tracing 中的v8.xxx
小写v开头的为 Blink 的调用,V8.xxx
大写的V才是真正的V8引擎。
详细源码
tracing工具还有一个非常好用的功能,点击下图中的放大镜,就可以直接打开 Chromium Code Search 查看Chromium的源码。这个工具也自带搜索功能,可以查看函数的声明、定义以及调用。

下面源码的探索基于commit e8b6574c 的Chromium,并且为了简化隐藏了无关的代码,用...替代
v8.callFunction的调用,我们点击可以找到这个这个函数调用,是在Blink中调用V8的入口。
v8::MaybeLocal<v8::Value> V8ScriptRunner::CallFunction(v8::Local<v8::Function> function,ExecutionContext* context,v8::Local<v8::Value> receiver,int argc,v8::Local<v8::Value> args[],v8::Isolate* isolate){...TRACE_EVENT0("v8", "v8.callFunction"); // 这就是我们在 tracing 中看到的 v8.callFunction...v8::MicrotaskQueue* microtask_queue = ToMicrotaskQueue(context); // 拿到 microtask 队列...v8::MicrotasksScope microtasks_scope(isolate, microtask_queue,v8::MicrotasksScope::kRunMicrotasks); // 这个 scope 很可疑,这里构造之后在这个函数后面并没有使用...probe::CallFunction probe(context, function, depth);v8::MaybeLocal<v8::Value> result =function->Call(isolate->GetCurrentContext(), receiver, argc, args); // 函数调用CHECK(!isolate->IsDead());...}
v8::MicrotasksScope的变量很可疑,在创建之后并没有在后续的函数里面使用,所以我们来看一下他的声明和定义。
/*** This scope is used to control microtasks when MicrotasksPolicy::kScoped* is used on Isolate. In this mode every non-primitive call to V8 should be* done inside some MicrotasksScope.* Microtasks are executed when topmost MicrotasksScope marked as kRunMicrotasks* exits.* kDoNotRunMicrotasks should be used to annotate calls not intended to trigger* microtasks.*/class V8_EXPORT V8_NODISCARD MicrotasksScope {public:enum Type { kRunMicrotasks, kDoNotRunMicrotasks };MicrotasksScope(Isolate* isolate, Type type);MicrotasksScope(Isolate* isolate, MicrotaskQueue* microtask_queue, Type type);~MicrotasksScope(); // 注意这个析构函数...
MicrotasksPolicy::kScoped这个策略被使用的时候,我们在后面会拎出来讲,这里大家先默认Blink是设置了这个策略)。
microtasks_scope创建之后并没有在后续的函数里面使用,而析构函数会在变量被销毁时执行。我们继续来看
v8::MicrotasksScope的定义。
MicrotasksScope::~MicrotasksScope() {if (run_) {microtask_queue_->DecrementMicrotasksScopeDepth(); // 这里将函数调用栈减少一层if (MicrotasksPolicy::kScoped == microtask_queue_->microtasks_policy() && // 这里检查策略是否是 MicrotasksPolicy::kScoped!isolate_->has_scheduled_exception()) {DCHECK_IMPLIES(isolate_->has_scheduled_exception(),isolate_->scheduled_exception() ==i::ReadOnlyRoots(isolate_).termination_exception());microtask_queue_->PerformCheckpoint(reinterpret_cast<Isolate*>(isolate_)); // 这一步尝试执行 Microtasks 队列}}...}
(https://source.chromium.org/chromium/chromium/src/+/master:v8/src/execution/microtask-queue.cc;l=113;drc=6dd90e4f92a8eb811114180ccc74d99b2c46ee7d)
void MicrotaskQueue::PerformCheckpoint(v8::Isolate* v8_isolate) {if (!IsRunningMicrotasks() && !GetMicrotasksScopeDepth() && // 注意,这一步检查了调用栈是否为空!HasMicrotasksSuppressions()) {Isolate* isolate = reinterpret_cast<Isolate*>(v8_isolate);RunMicrotasks(isolate); // 执行队列中的Microtasksisolate->ClearKeptObjects();}}...int MicrotaskQueue::RunMicrotasks(Isolate* isolate) {...{TRACE_EVENT_CALL_STATS_SCOPED(isolate, "v8", "V8.RunMicrotasks"); // 我们在 tracing 里面也可以看到这个输出maybe_result = Execution::TryRunMicrotasks(isolate, this,&maybe_exception);processed_microtask_count =static_cast<int>(finished_microtask_count_ - base_count);}...}

观察到的现象即是 "当浏览器 JS 引擎调用栈弹空的时候,才会执行 Microtasks 队列"
所以现在我如果问你,是不是Macrotasks(宏任务)执行完才会执行 Microtasks呢? 答案显然是否定的,如同这个例子,我们的Macrotask是处理点击输入,而Microtasks在其中被执行了两次。
JS触发点击事件

用JS触发点击事件其实也是同理的,同样是使用V8::MicrotasksScope
的析构函数来进行调用,只是前面几次都因为调用栈非空(GetMicrotasksScopeDepth
),所以等到最后面才执行。
V8::MicrotasksPolicy
那是不是所有使用V8引擎的应用Microtasks的运行时机都是一样的呢?答案是否定的,Microtasks的运行时机是由V8::MicrotasksPolicy
来决定的。
(https://source.chromium.org/chromium/chromium/src/+/master:v8/include/v8.h;l=7406;drc=6dd90e4f92a8eb811114180ccc74d99b2c46ee7d)
/*** Policy for running microtasks:* - explicit: microtasks are invoked with the* Isolate::PerformMicrotaskCheckpoint() method;* - scoped: microtasks invocation is controlled by MicrotasksScope objects;* - auto: microtasks are invoked when the script call depth decrements* to zero.*/enum class MicrotasksPolicy { kExplicit, kScoped, kAuto };
由上面的源码注释我们可以知道
explicit
模式下,由应用自己主动调用才会运行Microtasks。目前Node是使用了这种策略。scoped
模式下,由MicrotasksScope
控制,但作用域失效时,在其析构函数中运行Microtasks。目前Blink是使用这种策略,如下面的代码段为Blink设置 MicrotasksPolicy。auto
模式为V8的默认值,当调用栈为空的时候就会执行Microtasks
static void InitializeV8Common(v8::Isolate* isolate) {...isolate->SetMicrotasksPolicy(v8::MicrotasksPolicy::kScoped);...}
探索工具
chrome://tracing/是一个structural profiler或叫CPU profiler,与Chrome Devtool performance的sample profiler不同,他是由代码中主动的去埋点打印出来的,所以每一次函数调用都会被记录下来,不会像sample profiler一样漏掉采样时刻之间的状态。
使用方法如下,首先进入chrome://tracing点击左上角的Record,勾选住你想 profile的组件。

然后去到你的demo页执行你想要探索的操作,回到tracing页面点Stop,接着在Processes里面筛选掉其他Tab(标签页)的信息。
最后使用键盘w(放大)s(缩小)a(左移)d(右移)探索
强烈建议大家学会使用这个工具,本文例子的profile结果文件也会文章最后给到大家,大家有兴趣可以导入试一试。
总结

快乐工作,快乐生活
Happy work , Happy life




