探究 JavaScript Promises 的詳細實現

寫在前面:編程

這是一篇總結文章,但也能夠理解爲是一篇翻譯,主體脈絡參考自下面這篇文章:json

www.mattgreer.org/articles/pr…數組

若英文閱讀無障礙,牆裂推薦該文章的閱讀。promise

前言

在平常寫代碼的過程當中,我很常常會用到 promises 語法。當我自覺得了解 promises 詳細用法時,卻在一次討論中被問住了:「你知道 promises 內部的實現過程是怎樣的麼?」 是的,回想起來,我只是知道該如何使用它,殊不知道其內部真正的實現原理。這篇文章正是我本身的關於 promises 的回顧與總結。若是你看完了整篇文章,但願你也會更加理解 promises 的實現與原理。瀏覽器

咱們將會從零開始,逐步實現一個本身的 promises。最終的代碼將會和 Promises/A+ 規範類似,而且將會明白 promises 在異步編程中的重要性。固然,本文會假設讀者已經擁有了關於 promises 的基礎知識。bash

最簡單的 Promises

讓咱們從最簡單的 promises 實現開始吧。當咱們想要將下面的代碼閉包

doSomething(function(value) {
  console.log('Got a value:' + value);
});
複製代碼

轉變爲異步

doSomething().then(function(value) {
  console.log('Got a value:' + value);
});
複製代碼

這個時候,咱們須要怎麼作呢?很是簡單的方式就是,將原來的 doSomething()函數從原來的寫法異步編程

function doSomething(callback) {
  var value = 42;
  callback(value);
}
複製代碼

轉變爲以下這種 'promise' 寫法:函數

function doSomething() {
  return {
    then: function(callback) {
      var value = 42;
      callback(value);
    }
  };
}
複製代碼

上面只是一個 callback 寫法的一種語法糖包裝而已,看起來毫無心義。不過,這是個很是重要的轉變,咱們已經開始觸達了 promises 的一個核心理念:

Promises 捕獲最終值( eventual values ),並將其放入到一個 Object 中。

Ps: 這裏有必要解釋「最終值」的概念。它是異步函數的返回值,狀態是不肯定的,有可能成功,也有可能失敗(以下圖)。

eventual value

關於 Promises 與最終值( eventual values ),下文會包含更多的討論。

定義一個簡單的 Promise 函數

上面簡單的改寫並不足以對 promise 的特性作任何的說明,讓咱們來定義一個真正的 promise 函數吧:

function Promise(fn) {
  var callback = null;
  this.then = function(cb) {
    callback = cb;
  };

  function resolve(value) {
    callback(value);
  }

  fn(resolve);
}
複製代碼

代碼解析:將then的寫法拆分,同時引入了resolve函數,方便處理 Promise 的傳入對象(函數)。同時,使用callback做爲溝通then函數與resolve函數的橋樑。這個代碼實現,有一點 Promise 該有的樣子了,不是麼?

在此基礎上,咱們的doSomething()函數將會寫成這種形式:

function doSomething() {
  return new Promise(function(resolve) {
    var value = 42;
    resolve(value);
  });
}
複製代碼

當咱們嘗試執行的時候,會發現執行會報錯。這是由於,在上面的代碼實現中,resolve()會比then更早被調用,此時的callback仍是null。爲了解決這個問題,咱們使用setTimeout的方式 hack 一下:

function Promise(fn) {
  var callback = null;
  this.then = function(cb) {
    callback = cb;
  };

  function resolve(value) {
    // 強制此處的 callback 在 event loop 的下一個
    // 迭代中調用,這樣 then()將會在其以前執行
    setTimeout(function() {
      callback(value);
    }, 1);
  }

  fn(resolve);
}
複製代碼

通過這樣的修改以後,咱們的代碼將能夠成功運行。

這樣的代碼糟糕透了

咱們設想的實現,是能夠在異步狀況下也能夠正常工做的。可是此時的代碼,是很是脆弱的。只要咱們的then()函數中包含有異步的狀況,那麼變量callback將會再次變成null。既然這個代碼這麼渣渣,爲何還要寫下來呢?由於上面的模式很方便咱們待會的拓展,同時,這個簡單的寫法,也可讓大腦對thenresolve的工做方式有一個初步的瞭解。下面咱們考慮在此基礎上作必定的改進。

Promises 擁有狀態

Promises 是擁有狀態的,咱們須要先了解 Promises 中都有哪些狀態:

一個 promise 在等待最終值的時候,將會是 pending 狀態,當獲得最終值的時候,將會是 resolved 狀態。

當一個 promise 成功獲得最終值的時候,它將會一直保持這個值,不會再次 resolve。

(固然,一個 promise 的狀態也能夠是 rejected,下文會細述)

爲了將狀態引入到咱們的代碼實現中,咱們將原來的代碼改寫爲下面:

function Promise(fn) {
  var state = 'pending';
  //value 表示經過resolve函數傳遞的參數
  var value;
  //deferred 用於保存then()裏面的函數參數
  var deferred;

  function resolve(newValue) {
    value = newValue;
    state = 'resolved';

    if(deferred) {
      handle(deferred);
    }
  }

  function handle(onResolved) {
    if(state === 'pending') {
      deferred = onResolved;
      return;
    }

    onResolved(value);
  }

  this.then = function(onResolved) {
    handle(onResolved);
  };

  fn(resolve);
}
複製代碼

這個代碼看起來更加複雜了。不過此時的代碼可讓調用方任意調用then()方法,也能夠任意使用resolve()方法了。它也能夠同時運行在同步、異步的狀況下。

代碼解析:代碼中使用了state這個flag。同時,then()resolve()將公共的邏輯提取到了一個新的函數handle()中:

  • then()resolve()更早被調用的時候,此時的狀態是 pending,對應的 value 值並無準備好。咱們將then()裏面對應的回調參數保存在 deferred 中,方便 promise 在獲取到 resolved 的時候調用。
  • resolve()then()更早被調用的時候,此時的狀態設置爲 resolved,對應的 value 值也已經獲得。當then()被調用的時候,直接調用then()裏面對應的回調參數便可。
  • 因爲then()resolve()將公共的邏輯提取到了一個新的函數handle()中,所以無論上面的兩個 case 誰被觸發,最終都會執行 handle 函數。

若是你仔細看會發現,此時的setTimeout已經不見了。咱們經過 state 的狀態控制,已經獲得了正確的執行順序。固然,下面的文章中,還有會使用到setTimeout的時候。

經過使用 promise,咱們調用對應方法的順序將不會受到任何影響。只要符合咱們的需求,在任什麼時候刻調用resolve()then()都不會影響其內部邏輯。

此時,咱們能夠嘗試屢次調用then方法,會發現每一次獲得的都是相同的 value 值。

var promise = doSomething();

promise.then(function(value) {
  console.log('Got a value:', value);
});

promise.then(function(value) {
  console.log('Got the same value again:', value);
});
複製代碼

鏈式 Promises

在咱們平常針對 promises 的編程中,下面的鏈式模式是常見的:

getSomeData()
.then(filterTheData)
.then(processTheData)
.then(displayTheData);
複製代碼

getSomeData()返回的是一個 promise,此時能夠經過調用then()方法。但值得注意的是,第一個then()方法的返回值也必須是一個 promise,這樣纔可讓咱們的鏈式 promises 一直延續下去。

then()方法必須永遠返回一個 promise。

爲了實現這個目的,咱們將代碼作進一步的改造:

function Promise(fn) {
  var state = 'pending';
  var value;
  var deferred = null;

  function resolve(newValue) {
    value = newValue;
    state = 'resolved';

    if(deferred) {
      handle(deferred);
    }
  }

  function handle(handler) {
    if(state === 'pending') {
      deferred = handler;
      return;
    }

    if(!handler.onResolved) {
      handler.resolve(value);
      return;
    }

    var ret = handler.onResolved(value);
    handler.resolve(ret);
  }

  this.then = function(onResolved) {
    return new Promise(function(resolve) {
      handle({
        onResolved: onResolved,
        resolve: resolve
      });
    });
  };

  fn(resolve);
}
複製代碼

呼啦~ 如今的代碼讓人看起來彷佛有點抓狂😩。哈哈哈,你是否會慶幸一開始的時候咱們代碼不是那麼複雜呢?這裏面真正的一個關鍵點在於:then()方法永遠返回一個新的 promise。

doSomething().then(function(result){
  console.log("first result : ", result);
  return 88;
}).then(function(secondResult){
  console.log("second result : ", secondResult);
  return 99;
})
複製代碼

讓咱們來詳細看看第二個 promise 的 resolve 過程。它接收來自第一個 promise 的 value 值。詳細的過程發生在 handle()方法的底部。入參handler帶有兩個參數:一個是 onResolved回調,一個是對resolve()方法的引用。在這裏,每個新的 promise 都會有一個對內部方法resolve()的拷貝以及對應的運行時閉包。這是鏈接第一個 promise 與第二個 promise 的橋樑。

在代碼中,咱們能夠獲得第一個 promise 的 value 值:

var ret = handler.onResolved(value);
複製代碼

在上面的例子中,handler.onResolved表示的是:

function(result){
  console.log("first result : ", result);
  return 88;
}
複製代碼

也就是說,handler.onResolved實際上返回的是第一個 promise 的 then 被調用時候的傳入參數(函數)。第一個 handler 的返回值被用於第二個 promise 的 resolve 傳入參數。

這就是整個鏈式 promise 的工做方式。

若是咱們想要將全部的 then 返回的結果,該怎麼作呢?咱們可使用一個數組,來存放每一次的返回值:

doSomething().then(function(result) {
  var results = [result];
  results.push(88);
  return results;
}).then(function(results) {
  results.push(99);
  return results;
}).then(function(results) {
  console.log(results.join(', ');
});

// the output is
//
// 42, 88, 99
複製代碼

promises 永遠 resolve 返回的是一個值。當你想要返回多個值的時候,能夠經過建立某些符合結構來實現(如數組、object等)。

then 中的傳入參數是可選的

then() 中的傳入參數(回調函數)是並非必填的。若是爲空,在鏈式 promise 中,將會返回前一個 promise 的返回值。

doSomething().then().then(function(result) {
  console.log('got a result', result);
});

// the output is
//
// got a result 42
複製代碼

你能夠查看handle()中的實現方式,當前一個 promise 沒有 then 的傳入參數的時候,它會 resolve 前一個 promise 的value 值:

if(!handler.onResolved) {
  handler.resolve(value);
  return;
}
複製代碼

在鏈式 promise 中返回新的 promise

咱們的鏈式 promise 實現,依然顯得有些簡單。這裏的 resolve 返回的是一個簡單的值。假如想要 resolve 返回的是一個新的 promise 呢?好比下面的方式:

doSomething().then(function(result) {
  // doSomethingElse 返回的是一個promise
  return doSomethingElse(result);
}).then(function(finalResult) {
  console.log("the final result is", finalResult);
});
複製代碼

若是是這樣的狀況,那麼咱們上面的代碼彷佛沒法應對這樣的狀況。對於緊隨其後的那個 promise 而言,它獲得的 value 值將會是一個 promise。爲了獲得預期的值,咱們須要這樣作:

doSomething().then(function(result) {
  // doSomethingElse 返回的是一個promise
  return doSomethingElse(result);
}).then(function(anotherPromise) {
  anotherPromise.then(function(finalResult) {
    console.log("the final result is", finalResult);
  });
});
複製代碼

OMG... 這樣的實現實在是太糟糕了。難道做爲使用者,我還要每一次都須要本身來手動書寫這些冗餘的代碼麼?是否能夠在 promise 代碼內部處理一下這些邏輯呢?實際上,咱們只須要在已有代碼中的 resolve()中增長一點判斷便可:

function resolve(newValue) {
  if(newValue && typeof newValue.then === 'function') {
    newValue.then(resolve);
    return;
  }
  state = 'resolved';
  value = newValue;

  if(deferred) {
    handle(deferred);
  }
}
複製代碼

上面的代碼邏輯中咱們看到,resolve()中若是遇到的是 promise,將會一直迭代調用resolve()。直到最後得到的值再也不是一個 promise,纔會依照已有的邏輯繼續執行。

還有一個值得注意的點:看看代碼中是如何斷定一個對象是否是具備 promise 屬性的?經過斷定這個對象是否有then方法。這種斷定方法被稱爲 "鴨子類型"(咱們並不關心對象是什麼類型,究竟是不是鴨子,只關心行爲)。

這種寬鬆的界定方式,可使得具體的不一樣 promise 實現彼此之間有一個很好地兼容。

Promises 的 rejecting

在鏈式 promise 章節中,咱們的實現已經相對而言是很是完整的。可是咱們並無討論到 promises 中的錯誤處理。

在 promise 的決議過程當中,若是發生了錯誤,那麼 promise 將會拋出一個拒絕決議,同時給出對應的理由。對於調用者,怎麼知道錯誤發生了呢?能夠經過 then()方法的第二個傳入參數(函數):

doSomething().then(function(value) {
  console.log('Success!', value);
}, function(error) {
  console.log('Uh oh', error);
});
複製代碼

正如上面提到的,一個 promise 會從初始狀態 pending 轉換爲要麼是resolved 狀態,要麼是 rejected 狀態。這二者,只能有一個做爲最終的狀態。對應到then()的兩個參數,只有一個會被真正執行。

在 promise 內部實現中,一樣容許有一個reject()函數來處理 reject 狀態,能夠看作是 resolve()函數的孿生兄弟。此時,doSomething()函數也將會被改寫爲支持錯誤處理的方式:

function doSomething() {
  return new Promise(function(resolve, reject) {
    var result = somehowGetTheValue();
    if(result.error) {
      reject(result.error);
    } else {
      resolve(result.value);
    }
  });
}
複製代碼

對於此,咱們的代碼該作如何的對應改造呢?來看代碼:

function Promise(fn) {
  var state = 'pending';
  var value;
  var deferred = null;

  function resolve(newValue) {
    if(newValue && typeof newValue.then === 'function') {
      newValue.then(resolve, reject);
      return;
    }
    state = 'resolved';
    value = newValue;

    if(deferred) {
      handle(deferred);
    }
  }

  function reject(reason) {
    state = 'rejected';
    value = reason;

    if(deferred) {
      handle(deferred);
    }
  }

  function handle(handler) {
    if(state === 'pending') {
      deferred = handler;
      return;
    }

    var handlerCallback;

    if(state === 'resolved') {
      handlerCallback = handler.onResolved;
    } else {
      handlerCallback = handler.onRejected;
    }

    if(!handlerCallback) {
      if(state === 'resolved') {
        handler.resolve(value);
      } else {
        handler.reject(value);
      }

      return;
    }

    var ret = handlerCallback(value);
    handler.resolve(ret);
  }

  this.then = function(onResolved, onRejected) {
    return new Promise(function(resolve, reject) {
      handle({
        onResolved: onResolved,
        onRejected: onRejected,
        resolve: resolve,
        reject: reject
      });
    });
  };

  fn(resolve, reject);
}
複製代碼

代碼解析:不只僅新增了一個reject()函數,並且handle()方法內部也增長了對 reject的邏輯處理:經過對state的判斷,來決定具體執行handlerreject/resolved

不可知的錯誤,一樣應該引起rejection

上面的代碼,只對已知的錯誤進行了處理。當發生某些不可知錯誤的時候,一樣應該引起 rejection。須要在對應的處理函數中增長try...catch

首先是在resolve()方法中:

function resolve(newValue) {
  try {
    // ... as before
  } catch(e) {
    reject(e);
  }
}
複製代碼

一樣的,在 handle()執行具體 callback的時候,也可能發生未知的錯誤:

function handle(handler) {
  // ... as before

  var ret;
  try {
    ret = handlerCallback(value);
  } catch(e) {
    handler.reject(e);
    return;
  }

  handler.resolve(ret);
}
複製代碼

Promises 會吞下錯誤

有時候,對於 promises 的錯誤解讀,將會致使 promises 吞下錯誤。這是個常常坑開發者的點。

讓咱們來考慮這個例子:

function getSomeJson() {
  return new Promise(function(resolve, reject) {
    var badJson = "<div>uh oh, this is not JSON at all!</div>";
    resolve(badJson);
  });
}

getSomeJson().then(function(json) {
  var obj = JSON.parse(json);
  console.log(obj);
}, function(error) {
  console.log('uh oh', error);
});
複製代碼

這段代碼將會如何進行呢?在then()中的 resolve 執行的是對 JSON 的解析。它覺得可以執行,結果卻拋出了異常,由於傳入的 value 值並非 JSON 格式。咱們寫了一個 error callback 來捕獲這個錯誤。這樣是沒有問題,對吧?

不,結果可能並不符合你的指望。此時的 error callback 並不會觸發。結果將會是:控制檯上沒有任何的 log 輸出。這個錯誤就這樣被平靜地吞掉了。

爲何會這樣?由於咱們的錯誤發生在then()的 resolve 回調內部,源碼上看是發生在 handle()方法內部。這將會致使的是,then()返回的新的 promise 將會被觸發 reject,而不是現有的這個 promise 會觸發 reject:

function handle(handler) {
  // ... as before

  var ret;
  try {
    ret = handlerCallback(value);
  } catch(e) {
  	// 到達這裏,觸發的是handler.reject()
  	// 這是then()返回的新的promise的reject()
  	// 若是改爲 handler.onRejected(ex),將會觸發本promise的reject()
    handler.reject(e);
    return;
  }

  handler.resolve(ret);
}
複製代碼

若是將上面代碼中的catch部分改寫成:handler.onRejected(ex);將會觸發的是本 promise 的reject()。但這就違背了 promises 的原則:

一個 promise 會從初始狀態 pending 轉換爲要麼是 resolved 狀態,要麼是 rejected 狀態。這二者,只能有一個做爲最終的狀態。對應到then()的兩個參數,只有一個會被真正執行。

由於已經觸發了 resolved 狀態,那麼久不可能再次觸發 rejected 狀態。錯誤是在具體執行 resolved 函數的時候發生的,那麼這個 error,將會被下一個 promise 捕獲。

咱們能夠這樣驗證:

getSomeJson().then(function(json) {
  var obj = JSON.parse(json);
  console.log(obj);
}).then(null, function(error) {
  console.log("an error occured: ", error);
});
複製代碼

這多是 promises 中最坑人的一個點了。固然,只要理解了其中的原因,那麼就能夠很好地避免。爲了更好地體驗,咱們有什麼解決方法來規避這個坑呢?請看下一節:

done()來幫忙

大部分的 promise 庫都包含有一個 done()方法。它實現的功能和then()方法類似,只是很好的規避了剛剛提到的then()的坑。

done()方法能夠像then()那樣被調用。二者之間主要有兩點不一樣:

  • done()方法返回的不是一個 promise
  • done()中的任何錯誤將不會被 promise 實現捕獲(直接拋出)

在咱們的例子中,若是使用done()方法,將會更加保險:

getSomeJson().done(function(json) {
  // when this throws, it won't be swallowed
  var obj = JSON.parse(json);
  console.log(obj);
});
複製代碼

從rejection中恢復

從 promise 中的 rejection 恢復是有可能的。若是在一個包含有 rejection 的 promise 中增長更多的then()方法,那麼從這個then() 開始,將會延續鏈式 promise 的正常處理流程:

aMethodThatRejects().then(function(result) {
  // won't get here
}, function(err) {
  // since aMethodThatRejects calls reject()
  // we end up here in the errback
  return "recovered!";
}).then(function(result) {
  console.log("after recovery: ", result);
}, function(err) {
  // we won't actually get here
  // since the rejected promise had an errback
});

// the output is
// after recovery: recovered!
複製代碼

Promise 決議必須是異步的

在本文的開頭,咱們使用了一個 hack 來讓咱們的簡單代碼可以正確容許。還記得麼?使用了一個 setTimeout。當咱們完善了對應的邏輯以後,這個 hack 就沒有再使用了。但事實是:Promises/A+ 規範要求 promise 決議必須是一步的。爲了實現這個需求,最簡單的作法就是再次使用 setTimeout將咱們的handle()方法包裝一層:

function handle(handler) {
  if(state === 'pending') {
    deferred = handler;
    return;
  }
  setTimeout(function() {
    // ... as before
  }, 1);
}
複製代碼

很是簡單的實現。可是,實際上的 promises 庫並不傾向於使用setTimeout。若是對應的庫是用於 NodeJS,那麼它們傾向於使用 process.nextTick,若是對應的庫是用於瀏覽器,那麼它們傾向於使用setImmediate

爲何

具體的作法咱們知道了,可是爲何規範中會有這樣的要求呢?

爲了確保一致性與可信賴的執行過程。讓咱們考慮這樣的狀況:

var promise = doAnOperation();
invokeSomething();
promise.then(wrapItAllUp);
invokeSomethingElse();
複製代碼

上面的代碼會被怎樣執行呢?基於命名,你可能設想這個執行過程會是這樣的:invokeSomething() -> invokeSomethingElse() -> wrapItAllUp()。但實際上,這取決於在咱們當前的實現過程當中,promise 的 resolve 過程是同步的仍是異步的。若是doAnOperation()的 promise 執行過程是異步的,那麼其執行過程將會是設想的流程。若是doAnOperation()的 promise 執行過程是同步的,它真實的執行過程將會是invokeSomething() -> wrapItAllUp() -> invokeSomethingElse()。這時,可能會致使某些意想不到的後果。

所以,爲了確保一致性與可信賴的執行過程。promise 的 resolve 過程被要求是異步的,即便自己可能只是簡單的同步過程。這樣作,可讓全部的使用體驗都是一直的,開發者在使用過程當中,也再也不須要擔憂各類不一樣的狀況的兼容。

結論

若是讀到了這裏,那麼能夠肯定是真愛了!咱們將 promises 的核心概念都講了一遍。固然,文章中的代碼實現,大部分都是簡陋的。可能也會和真正的代碼庫實現有必定的出入。但但願不妨礙您對總體 promises 的理解。更多的關於 promises 的實現細節(如:all()race等),能夠查看更多的文檔與源碼實現。

當真正理解了 promises 的工做原理以及它的一些邊界狀況,我才真正喜歡上它。今後個人項目中關於 promises 的代碼也變得更加簡潔。關於 promises,還有不少內容值得去探討,本文只是一個開始。

相關文章
相關標籤/搜索