<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          探索v8源碼:事件循環(huán) Microtasks (微任務(wù))

          共 9944字,需瀏覽 20分鐘

           ·

          2021-04-20 21:43

          點擊上方藍(lán)字,發(fā)現(xiàn)更多精彩

          導(dǎo)語


          Microtasks(微任務(wù))是事件循環(huán)中一類優(yōu)先級比較高的任務(wù),本文通過一個有趣的例子探索其運行時機。從兩年前被動接受知識 "當(dāng)瀏覽器JS引擎調(diào)用棧彈空的時候,才會執(zhí)行 Microtasks 隊列",到兩年后主動深入探索源碼后了解到的 "當(dāng) V8 執(zhí)行完調(diào)用要返回 Blink 時,由于 MicrotasksScope 作用域失效,在其析構(gòu)函數(shù)中檢查 JS 調(diào)用棧是否為空,如果為空就會運行 Microtasks。"。同時文章中介紹了用于探索瀏覽器運行原理的一些工具。





          一個有趣的例子

          剛學(xué)前端那會學(xué)習(xí)事件循環(huán),說事件循環(huán)存在的意義是由于 JavaScript 是單線程的,所以需要事件循環(huán)來防止JS阻塞,讓網(wǎng)絡(luò)請求等I/O操作不阻塞主線程。

          而 Microtasks 是一類優(yōu)先級比較高的任務(wù),我們不能像 Macrotasks(宏任務(wù)) 一樣插入 Macrotasks 隊列末端,等待多個事件循環(huán)后才執(zhí)行,而需要插入到 Microtasks 的隊列里面,在本輪事件循環(huán)中執(zhí)行。

          比如下面這個有趣的例子:

          1. document.body.innerHTML = `

          2.    <button id="btn" type="button">btn</button>

          3. `;

          4. const button = document.getElementById('btn')

          5. button.addEventListener('click',()=>{

          6.  Promise.resolve().then(()=>console.log('promise resolved 1'))

          7.  console.log('listener 1')

          8. })

          9. button.addEventListener('click',()=>{

          10.  Promise.resolve().then(()=>console.log('promise resolved 2'))

          11.  console.log('listener 2')

          12. })

          13. // 1. 手動點擊按鈕

          14. // button.click() // 2. 解開這句注釋,用JS觸發(fā)點擊行為

          當(dāng)我手動點擊按鈕的時候,大家覺得瀏覽器的輸出是下面的A還是B?

          • A. listener1 -> promise resolved 1 -> listener2 -> promise resolved 2

          • B. listener1 -> listener2 -> promise resolved 1 -> promise resolved 2

          大家可以在這里試一下

          https://codesandbox.io/s/naughty-morning-9hnr3?fontsize=14&hidenavigation=1&theme=dark

          當(dāng)我將上面代碼中的最后一行注釋打開,使用JS觸發(fā)點擊行為的時候,瀏覽器的輸出是A還是B?

          大家覺得上面1、2兩種情況的輸出順序是否一樣?

          答案非常有意思

          • 當(dāng)我們使用1. 手動點擊按鈕時,瀏覽器的輸出是A

          • 當(dāng)我們使用2. 用JS觸發(fā)點擊行為時,瀏覽器的輸出是B




          被動接受知識

          為什么會出現(xiàn)這種情況呢?這個 Microtasks 的運行時機有關(guān)。兩年前當(dāng)我?guī)е@個問題搜索資料并詢問大佬的時,大佬告訴我:

          當(dāng)瀏覽器JS引擎調(diào)用棧彈空的時候,才會執(zhí)行Microtasks隊列

          按照這個結(jié)論,我使用 Chrome Devtool 中的 Performance 做了一次探索。

          人工點擊按鈕

          人工點擊的時候輸出為 listener1 -> promise resolved 1 -> listener2 -> promise resolved 2 。

          • 從上圖中我們可以看到,一次點擊事件之后,瀏覽器會調(diào)用 Function Call 進(jìn)入JS引擎,執(zhí)行 listener1,輸出 listener1

          • 彈棧時發(fā)現(xiàn)JS調(diào)用棧為空,這時候就會執(zhí)行 Microtasks 隊列中的所有 Microtask,輸出 promise resolved1

          • 接著瀏覽器調(diào)用 Function Call 進(jìn)入JS引擎,執(zhí)行 listener2,輸出 listener2

          • 彈棧時發(fā)現(xiàn)JS調(diào)用棧為空,這時候就會執(zhí)行 Microtasks 隊列中的所有Microtask,輸出 promise resolved2


          JS觸發(fā)點擊事件

          在JS代碼中觸發(fā)點擊時輸出為 listener1 -> listener2 -> promise resolved 1 -> promise resolved 2

          • 從上圖中我們可以看到,瀏覽器運行JS代碼時,調(diào)用了 button.click 這個函數(shù)

          • 進(jìn)入事件處理,執(zhí)行 listener1,輸出 listener1

          • 彈棧時發(fā)現(xiàn)JS調(diào)用棧非空(button.click函數(shù)還在運行)

          • 執(zhí)行 listener2,輸出 listener2

          • 彈棧時發(fā)現(xiàn)JS調(diào)用棧為空,這時候就會執(zhí)行 Microtasks 隊列中的所有 Microtask,輸出 promise resolved1、 promise resolved2

          探索工具

          Chrome Devtool 中的 Performance 是一個 sample profiler (采樣分析儀),即它的運行機制是每1ms暫停一下vm,將當(dāng)前的調(diào)用棧記錄下來,最后利用這部分信息做出可視化。

          由于它是一種 sample 的機制,所以在兩個 sample 之間的運行狀態(tài)可能會被丟失,所以我們在使用這個工具的時候可以

          1. 使CPU變慢:在 Devtool 中打開 "CPU 6x slowdown"

          2. 在要探索的函數(shù)中執(zhí)行一段比較長的for循環(huán)占用CPU時間(如上面的 heavy)

          強烈建議大家學(xué)會使用這個工具,本文例子的 profile 結(jié)果文件也會文章最后給到大家,大家有興趣可以導(dǎo)入試一試。




          主動探索V8源碼

          兩年的時間過去了,在上周整理筆記的時候,我開始質(zhì)疑這一個知識,"當(dāng)瀏覽器 JS 引擎調(diào)用棧彈空的時候,才會執(zhí)行 Microtasks 隊列"。

          因為它其實是個表現(xiàn),我想知道瀏覽器和 JS 引擎到底是怎么實現(xiàn)這樣的機制的。

          因此我使用 chrome://tracing進(jìn)行探索,

          下面探索基于 Chrome Version 88.0.4324.192 (Official Build) (x86_64),不同瀏覽器的實現(xiàn)有不同

          人工點擊按鈕

          • 從上圖中我們可以看到,一次點擊事件之后,Blink(Blink是一個渲染引擎,Chrome 的 Renderer 進(jìn)程中的主線程大部分時間會在 Blink 和 V8 兩者切換)會調(diào)用 v8.callFunction 進(jìn)入 V8 引擎,執(zhí)行 listener1,輸出 listener1

          • 返回 Blink 時發(fā)現(xiàn) V8 調(diào)用棧為空,這時候就會執(zhí)行 V8.RunMicrotasks 執(zhí)行 Microtasks 隊列中的所有 Microtask,輸出 promise resolved1

          • Blink 調(diào)用 v8.callFunction 進(jìn)入 V8 引擎,執(zhí)行 listener2,輸出 listener2

          • 返回 Blink 時發(fā)現(xiàn) V8 調(diào)用棧為空,這時候就會執(zhí)行 Microtasks 隊列中的所有 Microtask,輸出 promise resolved2

          注意,chrome://tracing 中的 v8.xxx小寫v開頭的為 Blink 的調(diào)用, V8.xxx大寫的V才是真正的V8引擎。

          詳細(xì)源碼

          tracing 工具還有一個非常好用的功能,點擊下圖中的放大鏡,就可以直接打開 Chromium Code Search 查看 Chromium 的源碼。這個工具也自帶搜索功能,可以查看函數(shù)的聲明、定義以及調(diào)用。

          下面源碼的探索基于commit e8b6574c 的Chromium,并且為了簡化隱藏了無關(guān)的代碼,用...替代

          比如我們在上面的 tracing 里面看到有 v8.callFunction的調(diào)用,我們點擊可以找到這個這個函數(shù)調(diào)用,是在 Blink 中調(diào)用 V8 的入口。

          thirdparty/blink/renderer/bindings/core/v8/v8script_runner.cc

          1. v8::MaybeLocal<v8::Value> V8ScriptRunner::CallFunction(

          2.   v8::Local<v8::Function> function,

          3.   ExecutionContext* context,

          4.   v8::Local<v8::Value> receiver,

          5.   int argc,

          6.   v8::Local<v8::Value> args[],

          7.   v8::Isolate* isolate

          8. ){

          9.  ...

          10.  TRACE_EVENT0("v8", "v8.callFunction"); // 這就是我們在 tracing 中看到的 v8.callFunction

          11.  ...

          12.  v8::MicrotaskQueue* microtask_queue = ToMicrotaskQueue(context); // 拿到 microtask 隊列

          13.  ...

          14.  v8::MicrotasksScope microtasks_scope(isolate, microtask_queue,

          15.                                       v8::MicrotasksScope::kRunMicrotasks); // 這個 scope 很可疑,這里構(gòu)造之后在這個函數(shù)后面并沒有使用

          16.  ...

          17.  probe::CallFunction probe(context, function, depth);

          18.  v8::MaybeLocal<v8::Value> result =

          19.      function->Call(isolate->GetCurrentContext(), receiver, argc, args); // 函數(shù)調(diào)用

          20.  CHECK(!isolate->IsDead());

          21.  ...

          22. }

          這里類型為 v8::MicrotasksScope的變量很可疑,在創(chuàng)建之后并沒有在后續(xù)的函數(shù)里面使用,所以我們來看一下他的聲明和定義

          v8/include/v8.h

          1. /**

          2. * This scope is used to control microtasks when MicrotasksPolicy::kScoped

          3. * is used on Isolate. In this mode every non-primitive call to V8 should be

          4. * done inside some MicrotasksScope.

          5. * Microtasks are executed when topmost MicrotasksScope marked as kRunMicrotasks

          6. * exits.

          7. * kDoNotRunMicrotasks should be used to annotate calls not intended to trigger

          8. * microtasks.

          9. */

          10. class V8_EXPORT V8_NODISCARD MicrotasksScope {

          11. public:

          12.  enum Type { kRunMicrotasks, kDoNotRunMicrotasks };

          13.  MicrotasksScope(Isolate* isolate, Type type);

          14.  MicrotasksScope(Isolate* isolate, MicrotaskQueue* microtask_queue, Type type);

          15.  ~MicrotasksScope(); // 注意這個析構(gòu)函數(shù)

          16.  ...

          上面這段注釋告訴我們,這個類是用來控制 Microtasks 的(當(dāng) MicrotasksPolicy::kScoped 這個策略被使用的時候,我們在后面會拎出來講,這里大家先默認(rèn) Blink 是設(shè)置了這個策略)。

          這里的析構(gòu)函數(shù)非常的可疑,因為我們在前面一步發(fā)現(xiàn)變量 microtasks_scope創(chuàng)建之后并沒有在后續(xù)的函數(shù)里面使用,而析構(gòu)函數(shù)會在變量被銷毀時執(zhí)行。我們繼續(xù)來看 v8::MicrotasksScope 的定義

          v8/src/api/api.cc

          1. MicrotasksScope::~MicrotasksScope() {

          2.  if (run_) {

          3.    microtask_queue_->DecrementMicrotasksScopeDepth(); // 這里將函數(shù)調(diào)用棧減少一層

          4.    if (MicrotasksPolicy::kScoped == microtask_queue_->microtasks_policy() && // 這里檢查策略是否是 MicrotasksPolicy::kScoped

          5.        !isolate_->has_scheduled_exception()) {

          6.      DCHECK_IMPLIES(isolate_->has_scheduled_exception(),

          7.                     isolate_->scheduled_exception() ==

          8.                         i::ReadOnlyRoots(isolate_).termination_exception());

          9.      microtask_queue_->PerformCheckpoint(reinterpret_cast<Isolate*>(isolate_)); // 這一步嘗試執(zhí)行 Microtasks 隊列

          10.    }

          11.  }

          12.  ...

          13. }

          v8/src/execution/microtask-queue.cc

          1. void MicrotaskQueue::PerformCheckpoint(v8::Isolate* v8_isolate) {

          2.  if (!IsRunningMicrotasks() && !GetMicrotasksScopeDepth() && // 注意,這一步檢查了調(diào)用棧是否為空

          3.      !HasMicrotasksSuppressions()) {

          4.    Isolate* isolate = reinterpret_cast<Isolate*>(v8_isolate);

          5.    RunMicrotasks(isolate); // 執(zhí)行隊列中的Microtasks

          6.    isolate->ClearKeptObjects();

          7.  }

          8. }

          9. ...

          10. int MicrotaskQueue::RunMicrotasks(Isolate* isolate) {

          11.  ...

          12.    {

          13.      TRACE_EVENT_CALL_STATS_SCOPED(isolate, "v8", "V8.RunMicrotasks"); // 我們在 tracing 里面也可以看到這個輸出

          14.      maybe_result = Execution::TryRunMicrotasks(isolate, this,

          15.                                                 &maybe_exception);

          16.      processed_microtask_count =

          17.          static_cast<int>(finished_microtask_count_ - base_count);

          18.    }

          19.  ...

          20. }

          到這里我們就知道了 Microtasks 的運行時機了,當(dāng) V8 執(zhí)行完調(diào)用要返回 Blink 時,由于 MicrotasksScope 作用域失效,在其析構(gòu)函數(shù)中檢查 JS 調(diào)用棧是否為空,如果為空的話就會運行 Microtasks

          下圖為完整的調(diào)用路徑

          觀察到的現(xiàn)象即是 "當(dāng)瀏覽器 JS 引擎調(diào)用棧彈空的時候,才會執(zhí)行 Microtasks 隊列"

          所以現(xiàn)在我如果問你,是不是 Macrotasks(宏任務(wù))執(zhí)行完才會執(zhí)行 Microtasks 呢? 答案顯然是否定的,如同這個例子,我們的 Macrotask 是處理點擊輸入,而 Microtasks 在其中被執(zhí)行了兩次。

          JS觸發(fā)點擊事件

          用JS觸發(fā)點擊事件其實也是同理的,同樣是使用 V8::MicrotasksScope的析構(gòu)函數(shù)來進(jìn)行調(diào)用,只是前面幾次都因為調(diào)用棧非空( GetMicrotasksScopeDepth),所以等到最后面才執(zhí)行。

          V8::MicrotasksPolicy

          那是不是所有使用V8引擎的應(yīng)用 Microtasks 的運行時機都是一樣的呢?答案是否定的,Microtasks 的運行時機是由 V8::MicrotasksPolicy來決定的。

          v8/include/v8.h

          1. /**

          2. * Policy for running microtasks:

          3. *   - explicit: microtasks are invoked with the

          4. *               Isolate::PerformMicrotaskCheckpoint() method;

          5. *   - scoped: microtasks invocation is controlled by MicrotasksScope objects;

          6. *   - auto: microtasks are invoked when the script call depth decrements

          7. *           to zero.

          8. */

          9. enum class MicrotasksPolicy { kExplicit, kScoped, kAuto };

          由上面的源碼注釋我們可以知道

          • explicit模式下,由應(yīng)用自己主動調(diào)用才會運行 Microtasks。目前 Node 是使用了這種策略。

          • scoped模式下,由 MicrotasksScope控制,但作用域失效時,在其析構(gòu)函數(shù)中運行 Microtasks。目前 Blink 是使用這種策略,如下面的代碼段為 Blink 設(shè)置 MicrotasksPolicy。

          • auto模式為 V8 的默認(rèn)值,當(dāng)調(diào)用棧為空的時候就會執(zhí)行 Microtasks

          thirdparty/blink/renderer/bindings/core/v8/v8initializer.cc

          1. static void InitializeV8Common(v8::Isolate* isolate) {

          2.  ...

          3.  isolate->SetMicrotasksPolicy(v8::MicrotasksPolicy::kScoped);

          4.  ...

          5. }

          探索工具

          chrome://tracing/ 是一個 structural profiler 或叫 CPU profiler,與 Chrome Devtool performance 的 sample profiler 不同,他是由代碼中主動的去埋點打印出來的,所以每一次函數(shù)調(diào)用都會被記錄下來,不會像sample profiler一樣漏掉采樣時刻之間的狀態(tài)。

          使用方法如下,首先進(jìn)入 chrome://tracing 點擊右上角的 Record,勾選住你想 profile 的組件。

          然后去到你的 demo 頁執(zhí)行你想要探索的操作,回到 tracing 頁面點 Stop,接著在 Processes 里面篩選掉其他 Tab(標(biāo)簽頁)的信息。

          最后使用鍵盤 w(放大)s(縮小)a(左移)d(右移)探索

          強烈建議大家學(xué)會使用這個工具,本文例子的 profile 結(jié)果文件也會文章最后給到大家,大家有興趣可以導(dǎo)入試一試。




          總 結(jié)

          Event Loop(事件循環(huán))是前端工程師經(jīng)常討論到的話題,往深挖可以挖出 JS 如何實現(xiàn)異步、requestAnimationFrame、瀏覽器渲染機制、Macrotasks、Microtasks等等問題。

          本文主要探索了Microtasks的運行時機,我從兩年前被動接受知識 "當(dāng)瀏覽器JS引擎調(diào)用棧彈空的時候,才會執(zhí)行 Microtasks 隊列"

          到兩年后主動使用工具深入探索源碼后了解到的 "當(dāng) V8 執(zhí)行完調(diào)用要返回 Blink 時,由于 MicrotasksScope 作用域失效,在其析構(gòu)函數(shù)中檢查 JS 調(diào)用棧是否為空,如果為空就會運行 Microtasks。"

          這也是計算機最吸引我的地方,當(dāng)你每隔一段時間回來看一個東西的時候,都能夠更往深一步,發(fā)現(xiàn)到更神奇的原理,也可以夠感受到自己的進(jìn)步

          在探索的過程中還使用了一些工具,如 Chrome Devtool Performance、Chrome tracing、Chromium Code Search 等,希望感興趣的同學(xué),也可以使用這些工具,更深入的探索瀏覽器內(nèi)部原理。


          如果你喜歡我的文章,歡迎光臨我的博客 lzane.com


          最后


          • 歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個專業(yè)的技術(shù)人...

          點個在看支持我吧


          瀏覽 45
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  暗呦网一区二区三区 | 人人撸夜夜撸 | 国产人兽网站 | 人人艹人人搡 | 欧美老妇乱伦 |