由哪一個log先輸出引出的event loop思考

這篇文章就再也不聊關於promise的各類好處和用法了,若是不瞭解請自行Google啦!javascript

我相信不少人在面試的時候遇到過這樣一道面試題:html

console.log(0)
let p = Promise.resolve()
setTimeout(()=>{
    console.log(4);
    setTimeout(()=>{
        console.log(5);
    },0);
},0);
p.then(data=>{
    console.log(2);
    setTimeout(()=>{
        console.log(3);
    },0);
})
console.log(6)
複製代碼

那麼你的答案是什麼呢? 粘貼到chrome的控制檯裏運行一下,結果以下vue

// 0
// 6
// 2
// 4
// 3
// 5
複製代碼

interesting的是,並非在全部瀏覽器裏都是這樣的打印順序的,例如,在safari 9.1.2中測試,輸出卻這樣的:java

// 0
// 6
// 4
// 2
// 5
// 3
複製代碼

再放到safari 10.0.1中卻又獲得了和chrome同樣的結果;node

固然,這只是這道面試題的一個簡單版本喲!git

那麼這道題到底在考察什麼呢?es6

其實,我相信不少同窗均可以一眼看出0和6會先輸出,可是setTimeout和promise哪一個先執行就有一丟丟小糾結了github

不再想爲這樣的執行順序所困擾?讓咱們先來了解一下js的event loop機制和promises的實現原理吧。web

咱們都知道promise是用來處理異步的,也知道js是單線程的,那麼js的異步是什麼呢? 這裏咱們先明確一批概念,是的沒看錯,一批面試

js

ECMAScript + DOM + BOM 咱們說js異步背後的「靠山」就是event loops。 其實這裏的異步準確的說應該叫瀏覽器的event loops或者說是javaScript運行環境的event loops,由於ECMAScript中沒有event loops, event loops是在HTML Standard定義的。

event loop

event loop也就是咱們常說的事件循環,能夠理解爲實現異步的一種方式,咱們來看看event loop在HTML Standard中的定義:

爲了協調事件,用戶交互,腳本,渲染,網絡等,用戶代理必須使用本節所述的event loop。

進程和線程

咱們知道javascript在最初設計時設計成了單線程,爲何不是多線程呢? 進程是操做系統分配資源和調度任務的基本單位,線程是創建在進程上的一次程序運行單位,一個進程上能夠有多個線程。

以瀏覽器爲例

  1. 用戶界面-包括地址欄、前進/後退按鈕、書籤菜單等
  2. 瀏覽器引擎-在用戶界面和呈現引擎之間傳送指令(瀏覽器的主進程)
  3. 渲染引擎,也被稱爲瀏覽器內核(瀏覽器渲染進程)
  4. 一個插件對應一個進程(第三方插件進程)
  5. GPU提升網頁瀏覽的體驗(GPU進程)

因而可知瀏覽器是多進程的,而且從咱們的角度來看咱們更加關心主進程,也就是瀏覽器渲染引擎

而單獨看渲染引擎,內部又是多線程的,包含兩個最爲重要的線程,即ui線程和js線程。並且ui線程和js線程是互斥的,由於JS運行結果會影響到ui線程的結果。

這裏也就回答了javascript爲何是單線程得問題,試想一下,若是多個線程同時操做DOM那豈不會很混亂?

固然,這裏所謂的單線程指的是主線程,也就是渲染引擎是單線程的,一樣的,在Node中主線程也是單線程的。

既然說js單線程指的是主線程是單線程的,那麼還有哪些其餘的線程呢?

  1. 瀏覽器事件觸發線程(用來控制事件循環,存放setTimeout、瀏覽器事件、ajax的回調函數)
  2. 定時觸發器線程(setTimeout定時器所在線程)
  3. 異步HTTP請求線程(ajax請求線程)

其餘線程

單線程特色是節約了內存,而且不須要在切換執行上下文。並且單線程不須要管其餘語言如java裏鎖的問題;

ps:這裏簡單說下鎖的概念。例以下課了你們都要去上廁所,廁所就一個,至關於全部人都要訪問同一個資源。那麼先進去的就要上鎖。而對於node來講。 下課了就一我的去廁所,因此免除了鎖的問題!

task (macrotask)

一個event loop有一個或者多個task隊列。

當用戶代理安排一個任務,必須將該任務增長到相應的event loop的一個tsak隊列中。

每個task都來源於指定的任務源,好比能夠爲鼠標、鍵盤事件提供一個task隊列,其餘事件又是一個單獨的隊列。能夠爲鼠標、鍵盤事件分配更多的時間,保證交互的流暢。

task也被稱爲macrotask,task隊列仍是比較好理解的,就是一個先進先出的隊列,由指定的任務源去提供任務。

哪些是task任務源呢?

規範在Generic task sources中有說起:

DOM操做任務源: 此任務源被用來相應dom操做,例如一個元素以非阻塞的方式插入文檔。

用戶交互任務源: 此任務源用於對用戶交互做出反應,例如鍵盤或鼠標輸入。響應用戶操做的事件(例如click)必須使用task隊列。

網絡任務源: 網絡任務源被用來響應網絡活動。

history traversal任務源: 當調用history.back()等相似的api時,將任務插進task隊列。

總之,task任務源很是寬泛,好比ajax的onload,click事件,基本上咱們常常綁定的各類事件都是task任務源,還有數據庫操做(IndexedDB ),須要注意的是setTimeout、setInterval、setImmediate也是task任務源。總結來講task任務源:

  1. setTimeout
  2. setInterval
  3. setImmediate (這是什麼東東?沒用過吧?沒用過很正常,由於它只兼容ie)
  4. MessageChannel
  5. I/O
  6. UI rendering

microtask

每個event loop都有一個microtask隊列,一個microtask會被排進microtask隊列而不是task隊列。

有兩種microtasks:分別是solitary callback microtasks和compound microtasks。規範值只覆蓋solitary callback microtasks。

若是在初期執行時,spin the event loop,microtasks有可能被移動到常規的task隊列,在這種狀況下,microtasks任務源會被task任務源所用。一般狀況,task任務源和microtasks是不相關的。

microtask 隊列和task 隊列有些類似,都是先進先出的隊列,由指定的任務源去提供任務,不一樣的是一個 event loop裏只有一個microtask 隊列。

HTML Standard沒有具體指明哪些是microtask任務源,一般認爲是microtask任務源有:

  1. process.nextTick
  2. promises.then
  3. Object.observe
  4. MutationObserver

執行棧

task和microtask都是推入棧中執行的 來看下面一段代碼:

function bar() {
    console.log('bar');
  }

  function foo() {
    console.log('foo');
    bar();
  }

  foo();
複製代碼

在規範的Processing model定義了event loop的循環過程: 一個event loop只要存在,就會不斷執行下邊的步驟:

  1. 在tasks隊列中選擇最老的一個task,用戶代理能夠選擇任何task隊列,若是沒有可選的任務,則跳到下邊的microtasks步驟。
  2. 將上邊選擇的task設置爲正在運行的task。
  3. Run: 運行被選擇的task。
  4. 將event loop的currently running task變爲null。
  5. 從task隊列裏移除前邊運行的task。
  6. Microtasks: 執行microtasks任務檢查點。(也就是執行microtasks隊列裏的任務)
  7. 更新渲染(Update the rendering)...
  8. 若是這是一個worker event loop,可是沒有任務在task隊列中,而且WorkerGlobalScope對象的closing標識爲true,則銷燬event loop,停止這些步驟,而後進行定義在Web workers章節的run a worker。
  9. 返回到1

主線程以外,還存在一個任務隊列,用來放置microtask。

簡單來講,event loop會不斷循環的去取tasks隊列的中最老的一個任務推入棧中執行,當次循環同步任務執行結束以後檢查是否存在microtasks隊列,若是有microtasks則先執行microtasks,執行結束清空microtasks棧,把下一個task放入執行棧內,如此循環。

說了這麼多關於event loop的東西,好像跟開篇的面試題並無什麼關係啊?

彆着急,下面咱們聊一下promise的實現; 咱們知道,promise是屬於es6的,在之前瀏覽器並不支持,也就衍生了各家諸如bluebird,q,when等promise庫,這些promise庫的實現方式不盡相同,但都遵循Promises/A+規範;

其中2.2.4就是:

onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].

這就意味着,在實現promise時,onFulfilled和onRejected要在新的執行上下文裏才能執行;

而在3.1中說起了

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.

即promise的then方法能夠採用「宏任務(macro-task)」機制或者「微任務(micro-task)」機制來實現。有的瀏覽器將then放入了macro-task隊列,有的放入了micro-task 隊列。開頭打印順序不一樣也正是源於此,不過一個廣泛的共識是promises屬於microtasks隊列。

那麼咱們就來簡單看一下promise的「宏任務(macro-task)」機制實現:

class Promise {
  constructor(executor) {
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    let resolve = (data) => {
      if (this.status === 'pending') {
        this.value = data;
        this.status = 'resolved';
        this.onResolvedCallbacks.forEach(fn => fn());
      }
    }
    let reject = (reason) => {
      if (this.status === 'pending') {
        this.reason = reason;
        this.status = 'rejected';
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    }
    try {
      executor(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }
  then(onFulFilled, onRejected) {
    onFulFilled = typeof onFulFilled === 'function' ? onFulFilled : y => y;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };
    let promise2;
    if (this.status === 'resolved') {
      promise2 = new Promise((resolve, reject) => {
        setTimeout(() => {  //「宏任務(macro-task)」機制實現
          try {
            let x = onFulFilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      });
    }
    if (this.status === 'rejected') {
      promise2 = new Promise((resolve, reject) => {
        setTimeout(() => {  //「宏任務(macro-task)」機制實現
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e);
          }
        }, 0);
      });
    }
    if (this.status === 'pending') {
      promise2 = new Promise((resolve, reject) => {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulFilled(this.value);
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e);
            }
          }, 0)
        });
        // 存放失敗的回調
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
      })
    }
    return promise2; // 調用then後返回一個新的promise
  }
  // catch接收的參數 只用錯誤
  catch(onRejected) {
    // catch就是then的沒有成功的簡寫
    return this.then(null, onRejected);
  }
}
複製代碼

沒錯咱們看到了setTimeout; 這種就是經過macro-task機制實現的,打印出來的順序就是如在safari 9.1.2中同樣了。 測試了一下bluebird的promise的實現,輸出的結果又和上面的都不同:

// 0
// 6
// 4
// 2
// 5
// 3
複製代碼

因此到底哪一個先輸出,要看你所使用的promise的實現方式;

固然正如上面提到的一個廣泛的共識是promises屬於microtasks隊列,因此通常狀況下,promise.then並非上面的這種實現,而是mic-task機制;

那麼再來看開篇的題目

console.log(0)      // 同步
let p = Promise.resolve();
setTimeout(()=>{    // 異步 macrotask
    console.log(4);
    setTimeout(()=>{
        console.log(5); // 異步 macrotask
    },0);
},0);
p.then(data=>{      // 異步 (經過macro-task實現則爲macrotask,經過micro-task實現則爲microtask)
    console.log(2);
    setTimeout(()=>{      // 異步 macrotask
        console.log(3);
    },0);
})
console.log(6)  // 同步
複製代碼

這樣就很清晰了對吧

上面有列出microtask有

  1. process.nextTick
  2. promises
  3. Object.observe
  4. MutationObserver

不知道用過vue1.0的同窗有沒有了解過vue1.0的nextTick是如何實現的呢?

有興趣能夠看一下源碼,就是經過MutationObserver實現的,只是由於兼容問題已經被取代了;

沒用過MutationObserver?不要緊,咱們舉一個簡單的例子 假如咱們要往一個id爲parent的dom中添加元素,咱們指望全部的添加操做都完成才執行咱們的回調 以下

let observe = new MutationObserver(function () {
          console.log('dom所有塞進去了');
    });
    // 一個微任務
    observe.observe(parent,{childList:true});
    for (let i = 0; i < 100; i++) {
      let p = document.createElement('p');
      div.appendChild(p);
    }
    console.log(1);
    let img = document.createElement('p');
    div.appendChild(img);
複製代碼

That's all ,如上;

references

從event loop規範探究javaScript異步及瀏覽器更新渲染時機

Promises/A+

webappapis L

相關文章
相關標籤/搜索