從 TS 造 Promise 的過程創建前端安全感

你還在用 npm 的 star 數來選擇依賴嗎?在 npm 安全性問題隨時爆發 的今天,做爲前端開發者的咱們應該具有源碼閱讀的能力,最好知道本身在用什麼,這樣在使用外部 npm 依賴時纔有安全感不是麼?前端

最近遇到一個細思極恐的問題,筆者最近總是收到防脫廣告推送亦或是一些與筆者最近說出來的話相關的廣告,之前只知道網上的信息流會被竊據,現在難不成語音也會監聽竊取了???嚇得我趕忙關掉了全部麥的權限。雖然咱們不能本身造個手機,但也不能活得沒有安全感。

拿 axios 這個咱們經常使用的依賴來講,若是哪天被篡改了,那後果然不敢想象。即使這基本是不可能的,但切圖仔中的精英不能就此中止追求進步的腳步。好在筆者前些天寫了篇 axios 重構經驗分享,多少能夠證實本身努力過。 😂java

這還不夠,翻了下最經常使用的依賴,其中 es6-promise 特別惹眼 (源碼比較晦澀難懂,說白了就是有點亂),原本早就想深刻了解 Promise ,因而絕不猶豫決定造它。因爲筆者在過渡到 TypeScript ,因此本次開發依舊會採用 TypeScript 來敲。node

這應該是筆者最後一次用 TypeScript 冠名分享文章,再見 🤞,我已經能夠安全上路了。( 喊了那麼屢次,快上車,都沒有多少人上車,那我就先走了。)ios

本文適合從零瞭解或者想從新深刻研究 Promise 的讀者,而且能夠獲得以下知識:git

  • Promise 重要知識點
  • Promise API 實現方法
  • 前端何來安全感

筆者但願讀者能夠僅經過看僅此一篇文章就能夠對 Promise 有個深入的認知,而且能夠本身實現一個 Promise 類。因此會從方方面面講 Promise,內容可能會比較多,建議讀者選讀。es6

爲何要實現 Promise

Promise 出如今 Es6 ,若是 Es5 須要使用 Promise,一般須要用到 Promise-polyfill 。也就是說,咱們要實現的是一個 polyfill。實現它不只有助於咱們深刻了解 Promise 並且能減小使用中犯錯的機率,以致於得到 Promise 最佳實踐。github

從另一個角度來講從新實現某個依賴也是一種源碼解讀的方式,堅持這麼作,意味着解讀源碼能力的提高。web

Promise

Promise 表示一個異步操做的最終結果,與之進行交互的方式主要是 then 方法,該方法註冊了兩個回調函數,用於接收 promise 的終值或本 promise 不能執行的緣由。

來看筆者用心畫的一張 API 結構圖 ( 看不清楚的能夠進個人 GitHub 看,有大圖和 xmind 源文件 ):面試

上圖只是一個 Promise 的 API 藍圖,其實 Promises/A+ 規範並不設計如何建立、解決和拒絕 promise,而是專一於提供一個通用的 then 方法。因此,Promise/A+ 規範的實現能夠與那些不太規範但可用的實現能良好共存。若是你們都按規範來,那麼就沒有那麼多兼容問題。(PS:包括 web 標準 ) 接着聊下 Promises/A+,看過的能夠跳過。ajax

Promises/A+

全部 Promise 的實現都離不開 Promises/A+ 規範,內容很少,建議你們能夠過一遍。這邊講一些規範中重要的點

術語

  • Promise 一個擁有 then 方法的對象或函數,其行爲符合 Promises/A+ 規範;
  • thenable 一個定義了 then 方法的對象或函數,也可視做 「擁有 then 方法」
  • 值(value)任何 JavaScript 的合法值(包括 undefined , thenable 和 promise)
  • 異常(exception) 使用 throw 語句拋出的一個值
  • 據因(reason) 表示一個 promise 的拒絕緣由。

Promise 的狀態

一個 Promise 的當前狀態 必須爲如下三種狀態中的一種:等待態(Pending)、執行態(Fulfilled)和拒絕態(Rejected)。
  • 等待態(Pending)

    處於等待態時,promise 需知足:能夠遷移至執行態或拒絕態

  • 執行態(Fulfilled)

    處於執行態時,promise 需知足:不能遷移至其餘任何狀態,必須擁有一個不可變終值

  • 拒絕態(Rejected)

    處於拒絕態時,promise 需知足:不能遷移至其餘任何狀態,必須擁有一個不可變據因

這裏的不可變指的是恆等(便可用 === 判斷相等),而不是意味着更深層次的不可變( 指當 value 或 reason 不是 基本值時,只要求其引用地址相等,但屬性值可被更改)。

Then 方法

一個 promise 必須提供一個 then 方法以訪問其當前值、終值和據因。

promise 的 then 方法接受兩個參數:

promise.then(onFulfilled, onRejected);
  • onFulfilledonRejected 都是可選參數。
  • 若是 onFulfilled 是函數,當 promise 執行結束後其必須被調用,其第一個參數爲 promise 的終值,在 promise 執行結束前其不可被調用,其調用次數不可超過一次
  • 若是 onRejected 是函數,當 promise 被拒絕執行後其必須被調用,其第一個參數爲 promise 的據因,在 promise 被拒絕執行前其不可被調用,其調用次數不可超過一次
  • onFulfilledonRejected 只有在執行環境堆棧僅包含平臺代碼 ( 指的是引擎、環境以及 promise 的實施代碼 )時纔可被調用
  • 實踐中要確保 onFulfilledonRejected 方法異步執行,且應該在 then 方法被調用的那一輪事件循環以後的新執行棧中執行。
  • onFulfilledonRejected 必須被做爲函數調用即沒有 this 值 ( 也就是說在 嚴格模式(strict) 中,函數 this 的值爲 undefined ;在非嚴格模式中其爲全局對象。)
  • then 方法能夠被同一個 promise 調用屢次
  • then 方法必須返回一個 promise 對象

Then 參數 (函數) 返回值

但願讀者能夠認真看這部分的內容,對於理解 promise 的 then 方法有很大的幫助。

先來看下 promise 執行過程:

大體的過程是,promise 會從 pending 轉爲 fulfilledrejected ,而後對應調用 then 方法參數的 onFulfilledonRejected ,最終返回 promise 對象。

進一步理解,假定 有以下兩個 promise:

promise2 = promise1.then(onFulfilled, onRejected);

會有如下幾種狀況:

  1. 若是 onFulfilled 或者 onRejected 拋出異常 e ,則 promise2 必須拒絕執行,並返回 拒因 e
  2. 若是 onFulfilled 不是函數 且 promise1 成功執行, promise2 必須成功執行並返回 相同的值
  3. 若是 onRejected 不是函數 且 promise1 拒絕執行, promise2 必須拒絕執行並返回 相同的據因

但願進一步搞懂的,能夠將下面代碼拷貝到 chrome 控制檯或其餘可執行環境感覺一下:

// 經過改變 isResolve 來切換 promise1 的狀態
const isResolve = true;

const promise1 = new Promise((resolve, reject) => {
  if (isResolve) {
    resolve('promise1 執行態');
  } else {
    reject('promise1 拒絕態');
  }
});

// 1、promise1 處於 resolve 以及 onFulfilled 拋出異常 的狀況
// promise2 必須拒絕執行,並返回拒因
promise1
  .then(() => {
    throw '拋出異常!';
  })
  .then(
    value => {
      console.log(value);
    },
    reason => {
      console.log(reason);
    }
  );

// 2、promise1 處於 resolve 以及 onFulfilled 不是函數的狀況
// promise2 必須成功執行並返回相同的值
promise1.then().then(value => {
  console.log(value);
});

// 3、promise1 處於 reject 以及 onRejected 不是函數的狀況
// promise2 必須拒絕執行並返回拒因
promise1.then().then(
  () => {},
  reason => {
    console.log(reason);
  }
);

// 4、promise1 處於 resolve 以及 onFulfilled 有返回值時
promise1
  .then(value => {
    return value;
  })
  .then(value => {
    console.log(value);
  });

下面還有一個比較重要的狀況,它關係到業務場景中傳值問題:

  1. onFulfilled 或者 onRejected 返回 一個 JavaScript 合法值 的狀況

咱們先來假定 then 方法內部有一個叫作 [[Resolve]] 的方法用於處理這種特殊狀況,下面來具體瞭解下這個方法。

[[Resolve]] 方法

通常像 [[...]] 這樣的認爲是內部實現,如 [[Resolve]],該方法接受兩個參數:

[[Resolve]](promise, x);

對於 x 值,有如下幾種狀況:

  • xthen 方法 且看上去像一個 Promise
  • x 爲對象或函數
  • xPromise

另外 promise 不能與 x 相等即 promise !== x,不然:

下面來看張圖,大體瞭解下各狀況的應對方式:

Promise/A+ 小結

至此,Promise/A+ 須要 瞭解的就講完了。主要包括了,術語以及 Then 方法的用法和相關注意事項。須要特別注意的是,then 方法中參數返回值的處理。接下來,咱們在規範的基礎上,用 TypeScript 來 實現 Promise。

接下來,文中出現的規範特指 Promise/A+ 規範

Promise 實現

Promise 自己是一個構造函數,便可以實現爲類。接下來,主要圍繞實現一個 Promise 類來說。

先來看下一個標準 promise 對象具有的屬性和 方法,作到心中有數。

Promise 提供的 API:

Promise 內部屬性包括:

下面開始正式的實現部分

聲明文件

在開始前,先來了解下,用 TypeScript 寫 Promise 涉及的一些類型聲明。能夠看這個聲明文件

主要包括:

TypeScript 中聲明文件用於外部模塊,是 TypeScript 的核心部分。另外,從一個聲明文件就能夠大體瞭解所用模塊暴露的 API 狀況 (接受什麼類型,或者會返回什麼類型的數據)。這種事先設計好 API 是一個好的開發習慣,但實際開發中會比較難。

接着來看,Promise 類核心實現的開始部分,構造函數。

構造函數

規範提到 Promise 構造函數接受一個 Resolver 類型的函數做爲第一個參數,該函數接受兩個參數 resolve 和 reject,用於處理 promise 狀態。

實現以下:

class Promise {
  // 內部屬性
  private ['[[PromiseStatus]]']: PromiseStatus = 'pending';
  private ['[[PromiseValue]]']: any = undefined;

  subscribes: any[] = [];

  constructor(resolver: Resolver<R>) {
    this[PROMISE_ID] = id++;
    // resolver 必須爲函數
    typeof resolver !== 'function' && resolverError();
    // 使用 Promise 構造函數,須要用 new 操做符
    this instanceof Promise ? this.init(resolver) : constructorError();
  }

  private init(resolver: Resolver<R>) {
    try {
      // 傳入兩個參數並獲取用戶傳入的終值或拒因。
      resolver(
        value => {
          this.mockResolve(value);
        },
        reason => {
          this.mockReject(reason);
        }
      );
    } catch (e) {
      this.mockReject(e);
    }
    return null;
  }

  private mockResolve() {
    // TODO
  }
  private mockReject() {
    // TODO
  }
}

[[Resolve]] 實現

經過前面規範部分,咱們瞭解到 [[Resolve]] 屬於內部實現,用於處理 then 參數的返回值。也就是這裏即將要實現的名爲 mockResolve 的方法。

根據規範內容能夠得知,mockResolve 方法接受的 value 可能爲 Promise,thenable,以及其餘有效 JavaScript 值。

private mockResolve(value: any) {
  // 規範提到 resolve 不能傳入當前返回的 promise
  // 即 `[[Resolve]](promise,x)` 中 promise !== x
  if (value === this) {
    this.mockReject(resolveSelfError);
    return;
  }
  // 非對象和函數,直接處理
  if (!isObjectORFunction(value)) {
    this.fulfill(value);
    return;
  }
  // 處理一些像 promise 的對象或函數,即 thenable
  this.handleLikeThenable(value, this.getThen(value));
}

處理 Thenable 對象

重點看下 handleLikeThenable 實現,可結合前面規範部分說起 Thenable 的幾種狀況來分析:

private handleLikeThenable(value: any, then: any) {
    // 處理 "真實" promise 對象
    if (this.isThenable(value, then)) {
      this.handleOwnThenable(value);
      return;
    }
    // 獲取 then 值失敗且拋出異常,則以此異常爲拒因 reject promise
    if (then === TRY_CATCH_ERROR) {
      this.mockReject(TRY_CATCH_ERROR.error);
      TRY_CATCH_ERROR.error = null;
      return;
    }
    // 若是 then 是函數,則檢驗 then 方法的合法性
    if (isFunction(then)) {
      this.handleForeignThenable(value, then);
      return;
    }
    // 非 Thenable ,則將該終植直接交由 fulfill 處理
    this.fulfill(value);
  }

處理 Thenable 中 Then 爲函數的狀況

規範說起:

若是 then 是函數,將 x 做爲函數的做用域 this 調用之。傳遞兩個回調函數做爲參數,第一個參數叫作 resolvePromise ,第二個參數叫作 rejectPromise。

此時,handleForeignThenable 就是用來檢驗 then 方法的。

實現以下:

private tryThen(then, thenable, resolvePromise, rejectPromise) {
    try {
      then.call(thenable, resolvePromise, rejectPromise);
    } catch (e) {
      return e;
    }
  }
  private handleForeignThenable(thenable: any, then: any) {
    this.asap(() => {
      // 若是 resolvePromise 和 rejectPromise 均被調用,
      // 或者被同一參數調用了屢次,則優先採用首次調用並忽略剩下的調用
      // 此處 sealed (穩定否),用於處理上訴邏輯
      let sealed = false;
      const error = this.tryThen(
        then,
        thenable,
        value => {
          if (sealed) {
            return;
          }
          sealed = true;
          if (thenable !== value) {
            this.mockResolve(value);
          } else {
            this.fulfill(value);
          }
        },
        reason => {
          if (sealed) {
            return;
          }
          sealed = true;
          this.mockReject(reason);
        }
      );

      if (!sealed && error) {
        sealed = true;
        this.mockReject(error);
      }
    });
  }

fulfill 實現

來看 [[Resolve]] 中最後一步,fulfill 實現:

private fulfill(value: any) {
    this['[[PromiseStatus]]'] = 'fulfilled';
    this['[[PromiseValue]]'] = value;

    // 用於處理異步狀況
    if (this.subscribes.length !== 0) {
      this.asap(this.publish);
    }
  }

看到這裏,你們可能有關注到不少方法都帶有 private 修飾符,在 TypeScript 中

private 修飾的屬性或方法是私有的,不能在聲明它的類的外部訪問

規範提到過,PromiseStatus 屬性不能由外部更改,也就是 promise 狀態只能改變一次,並且只能從內部改變,也就是這裏私有方法 fulfill 的職責所在。

[[Resolve]] 小結

至此,一個內部 [[Resolve]] 就實現了。咱們回顧一下,[[Resolve]] 用於處理如下狀況

// 實例化構造函數,傳入 resolve 的狀況
const promise = Promise(resolve => {
  const value: any;
  resolve(value);
});

以及

// then 方法中有 返回值的狀況
promise.then(
  () => {
    const value: any;
    return value;
  },
  () => {
    const reason: any;
    return reason;
  }
);

對於終值 value 有多種狀況,在處理 Thenable 的時候,請參考規範來實現。promise 除了 resolve 的還有 reject,但這部份內容比較簡單,咱們會放到後面再講解。先來看與 resolve 密不可分的 then 方法實現。這也是 promise 的核心方法。

Then 方法實現

經過前面的實現,咱們已經能夠從 Promise 構造函數來改變內部 [[PromiseStatus]] 狀態以及內部 [[PromiseValue]] 值,而且對於多種 value 值咱們都有作相應的兼容處理。接下來,是時候把這些值交由 then 方法中的第一個參數 onFulfilled 處理了。

在講解以前先來看下這種狀況:

promise2 = promise1.then(onFulfilled, onRejected);

使用 promise1then 方法後,會返回一個 promise 對象 promise2 實現以下:

class Promise {
  then(onFulfilled?, onRejected?) {
    // 對應上述的 promise1
    const parent: any = this;
    // 對應上述的 promise2
    const child = new parent.constructor(() => {});

    // 根據 promise 的狀態選擇處理方式
    const state = PROMISE_STATUS[this['[[PromiseStatus]]']];
    if (state) {
      // promise 各狀態對應枚舉值 'pending' 對應 0 ,'fulfilled' 對應 1,'rejected' 對應 2
      const callback = arguments[state - 1];
      this.asap(() =>
        this.invokeCallback(
          this['[[PromiseStatus]]'],
          child,
          callback,
          this['[[PromiseValue]]']
        )
      );
    } else {
      // 調用 then 方法的 promise 處於 pending 狀態的處理邏輯,通常爲異步狀況。
      this.subscribe(parent, child, onFulfilled, onRejected);
    }

    // 返回一個 promise 對象
    return child;
  }
}

這裏比較惹眼的 asap 後續會單獨講。先來理順一下邏輯,then 方法接受兩個參數,由當前 promise 的狀態決定調用 onFulfilled 仍是 onRejected

如今你們確定很關心 then 方法裏的代碼是如何被執行的,好比下面的 console.log

promise.then(value => {
  console.log(value);
});

接下來看與之相關的 invokeCallback 方法

then 方法中回調處理

then 方法中的 onFulfilledonRejected 都是可選參數,開始進一步講解前,建議你們先了解規範中說起的兩個參數的特性。

如今來說解 invokeCallback 接受的參數及其含義:

  • settled (穩定狀態),promise 處於非 pending 狀態則稱之爲 settled,settled 的值能夠爲 fulfilledrejected
  • child 即將返回的 promise 對象
  • callback 根據 settled 選擇的 onFulfilledonRejected 回調函數
  • detail 當前調用 then 方法 promise 的 value(終值) 或 reason(拒因)

注意這裏的 settleddetailsettled 用於指 fulfilledrejected, detail 用於指 valuereason 這都是有含義的

知道這些以後,就只須要參考規範建議實現的方式進行處理相應:

private invokeCallback(settled, child, callback, detail) {
    // 一、是否有 callback 的對應邏輯處理
    // 二、回調函數執行後是否會拋出異常,即相應處理
    // 三、返回值不能爲本身的邏輯處理
    // 四、promise 結束(執行結束或被拒絕)前不能執行回調的邏輯處理
    // ...
  }

須要處理的邏輯已給出,剩下的實現方式讀者可自行實現或看本項目源碼實現。建議全部實現都應參考規範來落實,在實現過程當中可能會出現遺漏或錯誤處理的狀況。(ps:考驗一個依賴健壯性的時候到了)

截至目前,都是處理同步的狀況。promise 號稱處理異步的大神,怎麼能少得了相應實現。處理異步的方式有回調和訂閱發佈模式,咱們實現 promise 就是爲了解決回調地獄的,因此這裏固然選擇使用 訂閱發佈模式。

then 異步處理

這裏要處理的狀況是指:當調用 then 方法的 promise 處於 pending 狀態時。

那何時會出現這種狀況呢?來看下這段代碼:

const promise = new Promise(resolve => {
  setTimeout(() => {
    resolve(1);
  }, 1000);
});

promise.then(value => {
  console.log(value);
});

代碼編寫到這裏,若是出現這種狀況。咱們的 promise 實際上是不能正常工做的。因爲 setTimeout 是一個異常操做,當內部 then 方法按同步執行的時候,resolve 根本沒執行,也就是說調用 then 方法的 promise 的 [[PromiseStatus]] 目前還處於 'pending',[[PromiseValue]] 目前爲 undefined,此時添加對 pending 狀態下的回調是沒有任何意義的 ,另外規範說起 then 方法的回調必須處於 settled( 以前有講過 ) 纔會調用相應回調。

或者咱們不用考慮是否是異步形成的,只須要明確一件事。存在這麼一種狀況,調用 then 方法的 promise 狀態可能爲pending

這時就必須有一套機制來處理這種狀況,對應代碼實現就是:

private subscribe(parent, child, onFulfillment, onRejection) {
    let {
      subscribes,
      subscribes: { length }
    } = parent;
    subscribes[length] = child;
    subscribes[length + PROMISE_STATUS.fulfilled] = onFulfillment;
    subscribes[length + PROMISE_STATUS.rejected] = onRejection;
    if (length === 0 && PROMISE_STATUS[parent['[[PromiseStatus]]']]) {
      this.asap(this.publish);
    }
  }

subscribe 接受 4 個參數 parentchild,onFulfillment,onRejection

  • parent 爲當前調用 then 方法的 promise 對象
  • child 爲即將由 then 方法返回的 promise 對象
  • onFulfillment then 方法的第一個參數
  • onFulfillment then 方法的第二個參數

用一個數組來存儲 subscribe ,主要保存即將返回的 promise 對象及相應的 onFulfillmentonRejection 回調函數。

知足 subscribe 是新增的狀況及調用 then 方法的 promise 對象的 [[PromiseStatus]] 值不爲 'pending',則調用 publish 方法。也就是說異步的狀況下,不會調用該 publish 方法。
這麼看來這個 publish 是跟執行回調相關的方法。

那異步的狀況,何時會觸發回調呢?能夠回顧以前講解過的 fulfill 方法:

private fulfill(value: any) {
    this['[[PromiseStatus]]'] = 'fulfilled';
    this['[[PromiseValue]]'] = value;

    // 用於處理異步狀況
    if (this.subscribes.length !== 0) {
      this.asap(this.publish);
    }
  }

當知足 this.subscribes.length !== 0 時會觸發 publish。也就是說當異步函數執行完成後調用 resolve 方法時會有這麼一個是否調用 subscribes 裏面的回調函數的判斷。

這樣就保證了 then 方法裏回調函數只會在異步函數執行完成後觸發。接着來看下與之相關的 publish 方法

publish 方法

首先明確,publish 是發佈,是經過 invokeCallback 來調用回調函數的。在本項目中,只與 subscribes 有關。直接來看下代碼:

private publish() {
    const subscribes = this.subscribes;
    const state = this['[[PromiseStatus]]'];
    const settled = PROMISE_STATUS[state];
    const result = this['[[PromiseValue]]'];
    if (subscribes.length === 0) {
      return;
    }
    for (let i = 0; i < subscribes.length; i += 3) {
      // 即將返回的 promise 對象
      const item = subscribes[i];
      const callback = subscribes[i + settled];
      if (item) {
        this.invokeCallback(state, item, callback, result);
      } else {
        callback(result);
      }
    }
    this.subscribes.length = 0;
  }

then 方法小結

到這咱們就實現了 promise 中的 then 方法,也就意味着目前實現的 promise 已經具有處理異步數據流的能力了。then 方法的實現離不開規範的指引,只要參考規範對 then 方法的描述,其他就只是邏輯處理了。

至此 promise 的核心功能已經講完了,也就是內部 [[Resolve]]then 方法。接下來快速看下其他 API。

語法糖 API 實現

catch 和 finally 都屬於語法糖

  • catch 屬於 this.then(null, onRejection)
  • finally 屬於 this.then(callback, callback);

promise 還提供了 resolverejectallrace 的靜態方法,爲了方便鏈式調用,上述方法均會返回一個新的 promise 對象用於鏈式調用。

以前主要講 resolve,如今來看下

reject

reject 處理方式跟 resolve 略微不一樣的是它不用處理 thenable 的狀況,規則說起 reject 的值 reason 建議爲 error 實例代碼實現以下:

private mockReject(reason: any) {
    this['[[PromiseStatus]]'] = 'rejected';
    this['[[PromiseValue]]'] = reason;
    this.asap(this.publish);
  }
  static reject(reason: any) {
    let Constructor = this;
    let promise = new Constructor(() => {});
    promise.mockReject(reason);
    return promise;
  }
  private mockReject(reason: any) {
    this['[[PromiseStatus]]'] = 'rejected';
    this['[[PromiseValue]]'] = reason;
    this.asap(this.publish);
  }

all & race

在前面 API 的基礎上,擴展出 all 和 race 並不難。先來看二者的做用:

  • all 用於處理一組 promise,當知足全部 promise 都 resolve 或 某個 promise reject 時返回對應由 value 組成的數組或 reject 的 reason
  • race 賽跑的意思,也就是在一組 promise 中看誰執行的快,則用這個 promise 的結果

對應實現代碼:

// all
let result = [];
let num = 0;
return new this((resolve, reject) => {
  entries.forEach(item => {
    this.resolve(item).then(data => {
      result.push(data);
      num++;
      if (num === entries.length) {
        resolve(result);
      }
    }, reject);
  });
});

// race
return new this((resolve, reject) => {
  let length = entries.length;
  for (let i = 0; i < length; i++) {
    this.resolve(entries[i]).then(resolve, reject);
  }
});

時序組合

若是有須要按順序執行的異步函數,能夠採用以下方式:

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());

在 ES7 中時序組合能夠經過使用 async/await 實現

for (let f of [func1, func2]) {
  await f();
}

更多使用方式可參考 這篇

知識補充

Promise 的出現是爲了更好地處理異步數據流,或者常說的回調地獄。這裏說的回調,是在異步的狀況下,若是非異步,則通常不須要用到回調函數。下面來了解下在實現 Promise 過程當中出現的幾個概念:

  • 回調函數
  • 異步 & 同步
  • EventLoop
  • asap

回調函數

回調函數的英文定義:

A callback is a function that is passed as an argument to another function and is executed after its parent function has completed。

字面上的理解,回調函數就是一個參數,將這個函數做爲參數傳到另外一個函數裏面,當那個函數執行完以後,再執行傳進去的這個函數。這個過程就叫作回調。

在 JavaScript 中,回調函數具體的定義爲: 函數 A 做爲參數(函數引用)傳遞到另外一個函數 B 中,而且這個函數 B 執行函數 A。咱們就說函數 A 叫作回調函數。若是沒有名稱(函數表達式),就叫作匿名回調函數。

首先須要聲明,回調函數只是一種實現,並非異步模式特有的實現。回調函數一樣能夠運用到同步(阻塞)的場景下以及其餘一些場景。

回調函數須要和異步函數區分開來。

異步函數 & 同步函數

  • 若是在函數返回的時候,調用者就可以獲得預期結果,這個就是同步函數。
  • 若是在函數返回的時候,調用者還不可以獲得預期結果,而是須要在未來經過必定的手段獲得,那麼這個函數就是異步的。

那什麼是異步,同步呢?

異步 & 同步

首先要明確,Javascript 語言的執行環境是"單線程"(single thread)。所謂"單線程",就是指一次只能完成一件任務。若是有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推。

這種模式會形成一個阻塞問題,爲了解決這個問題,Javascript 語言將任務的執行模式分紅兩種:同步(Synchronous)和異步(Asynchronous)。

但須要注意的是:異步機制是瀏覽器的兩個或以上常駐線程共同完成的,Javascript 的單線程和異步更多的應該是屬於瀏覽器的行爲。也就是說 Javascript 自己是單線程的,並無異步的特性

因爲 Javascript 的運用場景是瀏覽器,瀏覽器自己是典型的 GUI 工做線程,GUI 工做線程在絕大多數系統中都實現爲事件處理,避免阻塞交互,所以產生了 Javascript 異步基因。全部涉及到異步的方法和函數都是由瀏覽器的另外一個線程去執行的。

瀏覽器中的線程

  • 瀏覽器事件觸發線程 當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待 JavaScript 引擎的處理。這些事件能夠是當前執行的代碼塊如:定時任務、也可來自瀏覽器內核的其餘線程如鼠標點擊AJAX 異步請求等,但因爲 JavaScript 是單線程,全部這些事件都得排隊等待 JavaScript 引擎處理;
  • 定時觸發器線程 瀏覽器定時計數器並非由 JavaScript 引擎計數的, 由於 JavaScript 引擎是單線程的, 若是處於阻塞線程狀態就會影響記計時的準確, 所以經過單獨線程來計時並觸發定時是更爲合理的方案;
  • 異步 HTTP 請求線程 XMLHttpRequest 在鏈接後是經過瀏覽器新開一個線程請求,將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件放到 JavaScript 引擎的處理隊列中 等待處理;

經過以上了解,能夠知道其實 JavaScript 是經過 JS 引擎線程與瀏覽器中其餘線程交互協做實現異步

可是回調函數具體什麼時候加入到 JS 引擎線程中執行?執行順序是怎麼樣的?

接下來了解一下,與之相關的 EventLoop 機制

EventLoop

先來看一些概念,Stack,Heap,Queue,直接上圖:

  • 那些不須要回調函數的操做均可歸爲 Stack 這一類
  • Heap 用來存儲聲明的變量、對象
  • 一旦某個異步任務有了響應就會被推入 Queue 隊列中

一個大體的流程以下:

JS 引擎線程用來執行棧中的同步任務,當全部同步任務執行完畢後,棧被清空,而後讀取消息隊列中的一個待處理任務,並把相關回調函數壓入棧中,單線程開始執行新的同步任務。

JS 引擎線程從消息隊列中讀取任務是不斷循環的,每次棧被清空後,都會在消息隊列中讀取新的任務,若是沒有新的任務,就會等待,直到有新的任務,這就叫事件循環

這張圖不知道誰畫的,真的是很是棒!先借來描述下 AJAX 大概的流程:

AJAX 請求屬於很是耗時的異步操做,瀏覽器有提供專門的線程來處理它。當主線程上有調用 AJAX 的代碼時,會觸發異步任務。執行這個異步任務的工做交由 AJAX 線程,主線程並無等待這個異步操做的結果而是接着執行。假設主線程代碼在某個時刻執行完畢,也就是此時的 Stack 爲空。而在早些時刻,異步任務執行完成後已經將消息存放在 Queue 中,以便 Stack 爲空時從中拿去一個回調函數來執行。

這背後運做的就叫 EventLoop,有了上面的大體瞭解。下面來從新瞭解下 EventLoop:

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

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

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

事件,用戶交互,腳本,渲染,網絡這些都是咱們所熟悉的東西,他們都是由 event loop 協調的。觸發一個 click 事件,進行一次 ajax 請求,背後都有 event loop 在運做。

task

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

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

task 任務源:

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

microtask

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

一般認爲是 microtask 任務源有:

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver

一道關於 EventLoop 的面試題

console.log('start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve()
  .then(function() {
    console.log('promise1');
  })
  .then(function() {
    console.log('promise2');
  });

console.log('end');

// start
// end
// promise1
// promise2
// setTimeout
上面的順序是在 chrome 運行得出的,有趣的是在 safari 9.1.2 中測試,promise1 promise2 會在 setTimeout 的後邊,而在 safari 10.0.1 中獲得了和 chrome 同樣的結果。promise 在不一樣瀏覽器的差別正源於有的瀏覽器將 then 放入了 macro-task 隊列,有的放入了 micro-task 隊列。

EventLoop 小結

event loop 涉及到的東西不少,但本文怕重點偏離,這裏只是說起可能與 promise 相關的知識點。若是想深刻了解的同窗,建議看完這篇一定會受益不淺。

什麼是 asap

as soon as possible 英文的縮寫,在 promise 中起到的做用是儘快響應變化。

Promises/A+規範Notes 3.1 中說起了 promise 的 then 方法能夠採用「宏任務(macro-task)」機制或者「微任務(micro-task)」機制來實現。

本項目,採用 macro-task 機制

private asap(callback) {
    setTimeout(() => {
      callback.call(this);
    }, 1);
  }

或者 MutationObserver 模式:

function flush() {
...
}
function useMutationObserver() {
  var iterations = 0;
  var observer = new MutationObserver(flush);
  var node = document.createTextNode('');
  observer.observe(node, { characterData: true });

  return function () {
    node.data = iterations = ++iterations % 2;
  };
}

初次看這個 useMutationObserver 函數總會頗有疑惑,MutationObserver 不是用來觀察 dom 的變化的嗎,這樣憑空造出一個節點來反覆修改它的內容,來觸發觀察的回調函數有何意義?

答案就是使用 Mutation 事件能夠異步執行操做(例子中的 flush 函數),一是能夠儘快響應變化,二是能夠去除重複的計算。

或者 node 環境下:

function useNextTick() {
  return () => process.nextTick(flush);
}

前端安全感

既然標題提到了 前端安全感,固然要說點什麼。現現在前端處於模塊化開發的鼎盛時期,面對 npm 那成千上萬的 "名牌包包" 不少像我這樣的小白不免會迷失自我,漸漸沒有了安全感。

是作拿來主義,仍是像筆者同樣偶爾研究下底層原理,看看別人的代碼,本身擼一遍來感悟其中的真諦來提高安全感。

作前端也有段時日了,被業務埋沒的那些年,真是感嘆!回想曾經差點去作設計的我終於要入門前端了,心中不免想去搓一頓,那就寫到這吧!

結語

寫的有點多,好累。但願對你們有所幫助。

源碼

參考

Promises/A+
MDN Promises
Promise 使用
Event loop

相關文章
相關標籤/搜索