Promise then中回調為什麼是非同步執行?

Promise then中回調為什麼是非同步執行?


《你不知道的JavaScript》解釋過這個問題:

getify/You-Dont-Know-JS

下面說說我閱讀後的理解:

因為 then 總是返回 Promise,xxx.then(a =&> a) 的效果實際上是 return new Promise(resolve =&> resolve(a)),這個過程由引擎隱式完成。而 Promise 是一個 Job,所以必然非同步的。這涉及 ES6 中的一個概念,叫作 Job Queue;Promise 中的一個個 then 就是一種 Job Queue。

關於Job Queue

JavaScript Runtime 對於 Job Queue 與 Event Loop Queue 的處理有所不同。

相同點:

  • 都用作先進先出隊列

相異點:

  • 每個 JavaScript Runtime 可以有多個 Job Queue,但只有一個 Event Loop Queue
  • 當 JavaScript Engine 處理完當前事件隊列中的代碼後,再執行本次任務中所有的 Job Queue,然後再處理 Event Loop Queue(下一次事件循環任務)

ES6 中,一個 Promise 就是一個 PromiseJob,一種 Job(非同步和 Promise 的入門,可以參考這篇:JavaScript 中的非同步:Event Loop 及其他)。

之所以這樣設計,原因在於回調是可以有副作用的。

回調同步執行:

var arr = [1,2,3];

arr.forEach(function (x) {
console.log("first");
});

console.log("last");

// first first first
// last

回調非同步執行:

setTimeout(function () {
console.log("last")
}, 1000);

console.log("first");

// first
// last

一件有副作用的事情,如果放在多線程環境下,就構成了競態條件。而回調(例如 then 中的)如果可能同步、也可能非同步,執行順序就變成了不確定的,也就類似多線程中的競態條件。為了避免這種不確定性,then 的回調總是作為 Job Queue 中的下一個 Job 執行(非同步執行)。

除此之外,Promise 的設計還是為了解決以下情形導致的不確定性:

  • Call the callback too early
  • Call the callback too late (or never)
  • Call the callback too few or too many times
  • Fail to pass along any necessary environment/parameters
  • Swallow any errors/exceptions that may happen


http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony


一個原因是:同步和非同步共存的情況無法保證程序邏輯的一致性,假設按照then可能同步可能非同步的設計,以下代碼就存在一些問題:

let cache = null;
function getValue() {
if(cache) {
return Promise.resolve(cache); // 存在cache,這裡為同步調用
}
return fetch("/api/xxx").then(r =&> cache = r); // 這裡為非同步調用
}
console.log("before getValue");
getValue.then(() =&> console.log("getValue"));
console.log("after getValue");

這段代碼中,當有緩存且被resolved的情況下,返回的resolved Promise then是同步調用,列印如下:

before getValue

getValue

after getValue

當緩存未被resolved的情況下,返回pending promise, then為非同步,列印如下:

before getValue

after getValue

getValue

當getValue的緩存機製作為內部實現,介面作為黑盒提供給外部調用時,調用方完全無法判斷內部到底是同步還是非同步,對程序的可維護性和可測性帶來極大影響。

如果都統一設計為非同步,則永遠只會有一種確定的情況,從可維護和可測性角度來看都無疑是最佳實踐。


1、統一機制,防止競爭

2、防止棧溢出,這個問題應該就是賀老提到的 http://blog.izs.me/post/59142742143/designing-apis-for-asynchronyhttp://blog.izs.me/post/59142742143/designing-apis-for-asynchrony 的 Release Zalgo 問題 。大概就是同步回調,會出現 Maximum call stack size exceeded 的錯誤,一不小心是可能成為爆棧工程師的


一是要統一,二是順便解決同步可能會引發的 stackoverflow 問題。

Promise a+ 規範說需要在 stack empty 的環境下執行回調函數,而所謂 stack empty ,對 EcmaScript 來說實際上就是另一個 Job 並以回調函數為入口函數,通過這種方法使得

所有的回調函數執行統一成非同步模式。

我們假設如果不統一成非同步的話,競爭問題會變得很複雜。因為 Promise 本身可能是非同步調用 resolve,也可能是同步調用 resolve,即便同步調用 resolve 也可能因為 resolve 的是 thenable 而最終非同步 fulfill,這樣使得程序的可預測性很脆弱,而需要開發者來花很多精力維護這些狀態確保程序的正確性。但事實證明,這個問題完全可以通過在語言層面上統一成非同步模型來大度降低開發難度的。

個人理解。


因為then的兩個參數分別是兩個函數,完成處理函數和拒絕處理函數。

而兩個參數的值都是從Promise返回的。

Promise的兩個參數resolve,reject(兩個都是函數)的內部實現應該是通過事件循環和任務隊列完成的,因此then想接收從Promise的值就必須等到其完成才能調用相應的處理函數。


做為在沒看過標準的情況下就完整實現過Promise的渣渣,來強答一發

這裡題主的重點不在是不是microtask,而是問為什麼要非同步執行回調

雖然標準里也是這麼說的,但並沒有解釋原因,而我在最初實現的時候,是完全沒有參考標準的,最後也實現成了then的回調是非同步執行,究其原因,是因為如果你不這麼實現,有些情況下的回調會執行出錯,或者說,通不過測試用例,不信大家可以自己試試。

具體是哪種場景會出錯我現在也不記得了,自己實現一遍就肯定會知道的。總之就是同步resolve,非同步resolve,同步添加回調,非同步添加回調,回調再同步或非同步結束promise,各種組合,導致必須讓then里的回調非同步調用才能正確執行。

所以即使標準不這麼要求,你最終也會實現成這個樣子,就是這樣。


Promise 的機制就是 then 回調函數必須非同步執行。為什麼?因為這樣保障了代碼執行順序的一致性。

先看一個場景:

promise.then(function(){
if (trueOrFalse) {
// 同步執行
foo();
} else {
// 非同步執行 (如:使用第三方庫)
setTimeout(function(){
foo();
})
}
});

bar();

  1. 如果 promise then 回調是同步執行的,請問 foo() 和 bar() 函數誰先執行?
    答案是,如果 trueOrFalse 為 true 則 foo() 先執行,bar() 後執行;否則 bar() 先執行,foo() 後執行。在大部分情況下,你沒法預料到 trueOrFalse 的值,這也就意味著,你不能確定這段代碼真正的執行順序,這可能會導致一些難以想到的 bug。
  2. 如果 promise then 回調是非同步執行的,請問 foo() 和 bar() 函數誰先執行?
    答案一目了然,bar() 先執行,foo() 後執行。

所以為了保證代碼執行順序的一致性, then 回調必須保證是非同步的。


額,你這個問題彷彿又回到了同步時代程序員的質疑:為什麼我們需要非同步?你聯想 js 的執行環境,單線程,自然理解了。

回到 Promise.then。Promise 本身就被設計為用來取代傳統回調函數的一種非同步流程式控制制機制,也就是以前 callback 可以做的事情,今天我們用 Promise.then 也可以做到。不過要注意這兩個有區別。callback 是存在 callback queue 的,它由事件觸發,最終並沒有進入執行棧,而 promise.then 的回調則是註冊為 microtask 中的任務,最終會進入執行棧。也就是說,在 callback 時代,我們沒有 throw 的能力,不管你怎麼 throw,她都不理不睬;在 promise 時代,我們重新獲得了 throw 的能力,你可以同步地 throw,也可以非同步地 throw(rejected),她就在那裡。這才是 Promise 的魅力所在,而不僅僅是為了推翻 callback hell。Promise.then 強大的地方還在於你可以返回一個 promise,也可以返回一個同步的值,後續的 then 都可以捕獲到,不過,永遠記得 return or throw,return or throw,return or throw。


記得看過一個文章說

如果promise是同步代碼 那麼resolve會先於then執行 那個時候回調隊列還是空的


至於為什麼promise.then比setTimeout優先執行,原因如下:

Promise/A+規範指出:
Here 「platform code」 means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. 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. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or 「trampoline」 in which the handlers are called.

V8實現中,兩個隊列各包含不同的任務:

macrotasks: script(整體代碼),setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promises, Object.observe, MutationObserver

執行過程如下:
JavaScript引擎首先從macrotask queue中取出第一個任務
執行完畢後,將microtask queue中的所有任務取出,按順序全部執行
然後再從macrotask queue中取下一個
執行完畢後,再次將microtask queue中的全部取出;
循環往複,直到兩個queue中的任務都取完。

解釋:
代碼開始執行時,所有這些代碼在macrotask queue中,取出來執行之。
後面遇到了setTimeout,又加入到macrotask queue中,
然後,遇到了promise.then,放入到了另一個隊列microtask queue
等整個execution context stack執行完後,
下一步該取的是microtask queue中的任務了。
因此promise.then的回調比setTimeout先執行。

參考

8.1.4 Event loops - Perform a microtask checkpoint

轉自 [JavaScript] Macrotask Queue和Microtask Quque


還不如直接看下源碼更能解釋你的疑惑

Promise源碼分析 - 知乎專欄


直接原因: 這是Promise/A+ 和 ES6 的規定

Promise 被設計成 "傳給then的函數會在 resolve 後被非同步執行" 這樣一個弱而一致的行為,正是為了讓使用者不需要 (也無法) 考慮時機。

一個Promise對象可能在過去已經 resolve,在將來 resolve,永遠不 resolve, 在使用此對象的代碼看來無法區分。這樣我們才能放心地在代碼的任意位置,執行中的任意時間組合 Promise 和函數。

基於同樣原因,在標準中也沒有阻塞地從Promise讀出結果 (如 Java 的 Future&#get() ) 的操作。


不是很懂問題的描述。

如果不是非同步,是同步,那麼著Promise 還有什麼意義?不如寫同步代碼。


貌似是它跟setTimeout的task一樣,是放入任務隊列,不過比setTimeout塊,是microtask。

其實我也是瞎扯。。。拋磚引玉


推薦閱讀:

既然 Node.js 是單線程,又是怎麼做到支持非同步函數調用的?
vuejs怎麼在伺服器部署?
node.js和前端js有什麼區別?需要重新學習嗎?
typescript調用js(node)組件,必須在每一個引用的地方都寫reference嗎?
前後端使用同一種編程語言有什麼優勢和劣勢?

TAG:Nodejs | ECMAScript2015 |