‘異步’ 這個概念若是放到十年前的08,09年的時候,你們會以爲: 哇~ 這是一個新鮮的概念,不用再把全部Web頁面同步處理了,節省了服務資源的同時也提高了用戶體驗。也正是從那個時候開始,咱們開始關注先後端分離這個概念。
通過10年的努力,咱們如今很高興的看到,前端已經快速的成長爲一門有着獨立發展方向的技術。這一切也就是從異步這個關鍵的點開始的,所以可見 異步對於前端來講意味着什麼?大概就是意味着基石和根本吧。javascript這篇文章將不侷限於上述的Http異步網絡請求這個獨立的場景,將細數一下前端發展過程當中對於異步這個概念是如何逐步落實的。html
可能咱們的思惟固定化了,畢竟在漫長的JS腳本語言發展的過程當中,回調函數就曾經是異步編程的標準解決方案,直到如今WebAPI和NodeJs中還保留着大量的APi使用回調函數做爲異步結束的處理。爲了解決回調函數這個解決方案在開發體驗上的弱勢。ES6支持了Promise這樣使用同步編程的方式來開發異步程序前端
在以前咱們開發回調函數的時候,沒有人知道哪一個函數先執行,哪一個隨之執行,除非咱們把要逐次進行的函數進行嵌套,讓程序依照回調的層級從深層到淺層的執行。java
咱們可能常常會據說這樣的一句話Promise 是一個表現爲狀態機的異步容器。怎麼理解這句話呢:node
咱們在看完 # ES6-Promise 的文檔介紹的時候,都會躍躍欲試的使用其提供的API方法來進行開發和升級,並大呼過癮。在興奮之餘,咱們也一塊兒來盤點一下那些年咱們使用Promise。es6
咱們以前嘗試着理解了--狀態機這個概念。也清晰的知道了這個Promise 的一個重要特性。web
new Promise((resolve, reject) => {
resolve('程序執行成功');
setTimeout(() => reject('程序執行失敗'), 5000);
}).then(console.log, console.log);
複製代碼
咱們會想到第二個問題:
Promise 執行函數中,在resolve或者reject觸發以後的代碼會不會執行呢?ajax
new Promise((resolve, reject) => {
resolve('程序執行成功');
console.log('後面的程序會不會執行呢');
setTimeout(() => reject('程序執行失敗'), 5000);
}).then(console.log, console.log);
複製代碼
OUTPUT:chrome
後面的程序會不會執行呢
程序執行成功
複製代碼
在任何程序中,代碼的執行順序都是很重要的。既然說Promise是一個異步容器,容器中或外的代碼是同步的仍是異步的,甄別他們的執行順序也是須要有明確認識的。編程
console.log('A');
new Promise((resolve, reject) => {
console.log('B');
resolve('C');
console.log('D');
}).then(console.log, console.error);
console.log('E');
複製代碼
首先要區分一下,哪些代碼是同步的,那些代碼是異步執行的。
OUTPUT:
A B D E C
複製代碼
思考: 在Promise構造函數的參數函數中,代碼是同步執行的。若是在函數體中存在異步方法,好比 setTimeout()
執行順序會發生什麼變化?
【 這部份內容會在瀏覽器異步機制部分提到 】
Promise 做爲一個異步容器,他存在的意義就是爲了改變Promise的狀態。那麼在狀態已經觸發以後的代碼就變得沒有意義了。若是你已經判定 resolve 或 reject 後的代碼無心義。可使用 return resolve() / return reject()
避免發生沒必要要的錯誤。
new Promise((resolve, reject) => {
return resolve('程序執行成功');
console.log('後面的程序會不會執行呢');
setTimeout(() => reject('程序執行失敗'), 5000);
}).then(console.log, console.log);
複製代碼
做爲一個狀態機,咱們只須要關注Promise的狀態變化便可。狀態變化纔會觸發異步執行。峯迴路轉,狀況變幻無窮。仍是要關注他狀態機的本質。
說到Promise的API,就到了你們比較熟悉的內容了。在ES6發展以前社區就有對Promise的社區實現,凡是被大規模承認的實現官方也很快就會給出支持,這也是JavaScript語言得以不斷進步的緣由。
從Api的使用來看,then有兩個接受參數分別對應着的是 Promise 構造函數的參數函數的成功結果和失敗結果,也就是狀態機將狀態變爲了 成功 或者是 失敗。
其實對於Promise.prototype.then 這個api很容易理解,總結來看:
.then
能夠用於鏈式調用,也能夠不用。.then
的本質是建立了一個新的隱形的Promise,所以能夠繼續鏈式調用。.then
的參數函數(回調函數)在觸發以前,Promise的狀態已經發生了變化。.then
只有在Promise的參數函數中,有錯誤發生的時候纔會有reject。const p1= new Promise((resolve, reject) => resolve('hello'));
const p2 = p1.then(value => {
console.log(value); // hello
return value;
});
const p3 = p2.then(console.log); // hello
複製代碼
從上邊的總結來看,.then
是在Promise原型上的Promise.prototype.then
。要想讓p2和p3的then可以成功執行,必須保證前面調用then的那個對象是一個Promise。
...
const p2 = p1.then(value => {
// 替換這裏
return Promise.resolve(value);
});
const p3 = p2.then(console.log); // hello
複製代碼
Promise.resolve()
和在 .then
裏面直接用return返回能夠獲得一樣的結果。.then
函數(方法)的執行結果是一個新的 Promise。.then
若是要是返回空值,至關於 Promise.resolve()
;來繼續看.then
的最後一個Part,咱們知道.then
有兩個回調函數,第一個是在成功時候觸發的,第二個是在失敗時候觸發的。
const p1 = new Promise((resolve, reject) => resolve('hello'));
const p2 = p1.then(value => {
// return abcd; // ReferenceError: x is not defined
return Promise.reject('手動錯誤');
// VM258:4 Uncaught (in promise) 手動錯誤
});
const p3 = promiseB.then(console.log, console.error);
複製代碼
以上的DEMO是觸發第二個回調函數的兩種方法:程序錯誤OR手動拋錯(邏輯錯誤)。這兩種的側重點可能不由相同,所以能夠區別來使用。
這個Api從某些角度來看是.then
方法的一個小變種或者說是語法糖。怎麼來理解這個呢,在then的回調函數中,已經有對於err的處理,只不過在鏈式調用的過程當中,若是每一步都進行err的處理會嚴重的阻塞咱們的開發的流暢性。所以也就誕生了 .catch
來捕獲異常。
除了.catch
的Api以外,有如下常見總結:
.catch
會捕獲整個Promise鏈路上的異常。.catch
捕獲的異常包括 程序錯誤 && 手動拋錯(邏輯錯誤) 。Promise
會將全部的內部錯誤內部處理,不會影響外部的邏輯。詳細來講:
// 捕獲異常
new Promise((resolve, reject) => {
console.log(x);
resolve('hello Mr.ZA');
}).then(res => {
console.log(y);
}).catch(err => {
console.log('err', err);
})
setTimeout(() => { console.log('log: 後續程序') }, 1000);
// err VM8513:7 ReferenceError: x is not defined
// at <anonymous>:2:14
// at new Promise (<anonymous>)
// at <anonymous>:1:1
// log: 後續程序
new Promise((resolve, reject) => {
resolve(1);
console.log(x); // 區別在這裏
}).then(res => {
console.log(y);
}).catch(err => {
console.log(err);
})
ReferenceError: y is not defined
at <anonymous>:6:14
複製代碼
.catch
捕獲的異常不是全部的異常,而是捕獲第一個影響狀態變化的異常。new Promise((resolve, reject) => {
console.log(x);
resolve(1);
}).catch(err => {
if(err) { console.log('異常捕獲')};
return 'continue progress';
}).then(res => {
console.log('res: ', res);
})
// 異常捕獲
// res: continue progress
複製代碼
.catch
的位置不必定是在最後面,它和其餘的api同樣都會返回一個新的Promise爲鏈式調用提供服務。寫在最後面符合咱們對開發流程的認知。finally
在英語上來說是最終的意思,放在Promise的Api中,它會被咱們理解爲無論狀態如何變化,都會發生的事情。
對於.finally
,有如下常見總結:
.finally
是Promise狀態機狀態變化的兜底方案,也是不管如何都能執行的。.finally
這個Api的回調函數沒有參數。.finally
不必定放在鏈式調用的最後面,若是他在鏈式調用的中間,他前面的resolve或者reject傳出的值會跳過finally傳入下面的鏈式調用中。// 僞代碼,模擬發送請求處理loading的問題。
new Promise((resolve, reject) => {
this.loading = true;
$.ajax(url, data, function(res) {
resolve('res');
})
}).finally(() => {
this.loading = false;
return '嘗試更改';
}).then(value => {
// handle value
console.log(value), // res
}).catch(
// handle error
error => console.error(error),
);
複製代碼
.finally
會更關注於狀態的變化過程而不是狀態變化帶來的影響。.finally
若是在其中的回調中嘗試更改以前的流轉的值的時候,不能得到成功,可是若是有拋錯產生,會被錯誤處理程序依次捕獲。接下來關注一下這個Api的兼容性問題,我想之因此你尚未使用這種方法來減小重複的工做,頗有多是由於這個Api出世的時間比較晚。
據 MDN 的資料顯示,這個API是ES2018引入TC39規範的也就是 ES9。
下面來看看這個新Api的兼容性問題有如下關注點,根據本身狀況食用吧。
new Promise()
來建立實例。Promise.resolve = new Promise((resolve,reject)=>resolve('xx'));
Promise.reject = new Promise((resolve, reject)=>reject('xx'));
複製代碼
這兩個Api應該是Promise裏面比較難理解的Api了,可是在使用上他們其實很簡單。咱們仍是要追求一下實現的原理,這樣咱們在使用Api的時候也不會那麼迷惑何時應該有什麼樣的結果。這兩個Api也常常會放到一塊作一些對比。
對於他們來說,有如下常見總結:
.all()
和.race()
不是Promise的原型方法,所以在使用他們的時候不用new Promise()
.all()
和.race()
都接收一個數組爲參數,返回的也是一個數組。若是參數數組中的值不是一個Promise實例,那麼會被轉換成直接返回。.all()
中若是有一個Promise執行出錯,將中止執行返回錯誤。所有成功以後才返回值數組。.race()
中若是第一個Promise完成了就直接返回,不等待其他執行完畢。對於這些官方的Api及其用法,有人曾提出一個結論,使用Promise.all
能夠併發的執行異步動做,獲得性能的提高。那麼其中的原理是什麼呢?爲何單線程的JavaScript會有異步性能提高呢?咱們來看下其中的緣由。
若是你有關於【微任務和宏任務】的理解,下面的內容會更加容易理解。
Promise.all([
new Promise((resolve)=>{
setTimeout(() => {
console.log('-', 0)
resolve(0);
}, 1000)
}),
new Promise((resolve) => {
setTimeout(()=>{
console.log('-',1)
resolve(1);
}, 2000)
})
]).then(res=>console.log(res))
複製代碼
執行的過程:
pending
狀態的Promise實例放入棧中,並記錄下他們的順序編號。.then
是會建立一個新的Promise執行,所以在執行數組中實例的時候會建立新的 Promise(新的微任務)。下面來看一下Promise.all 的源碼實現。
Promise.all = function (arr) {
// ... Step0: 返回新的Promise
return new Promise(function (resolve, reject) {
var args = Array.prototype.slice.call(arr);
if (args.length === 0) return resolve([]);
var remaining = args.length;
function res(i, val) {
//...
};
// Step 1. 對數組進行同步循環
for (var i = 0; i < args.length; i++) {
// Step 2. 執行這些個Promise實例。
res(i, args[i]);
}
});
};
複製代碼
res
這個方法。
function res(i, val) {
// Step 3: 確認要執行的那個Promise實例
if (val && (typeof val === 'object' || typeof val === 'function')) {
// Step 4: 建立.then 也就是一個新的Promise微任務
var then = val.then;
if (typeof then === 'function') {
then.call(
val,
function (val) {
res(i, val);
},
reject
);
return;
}
}
args[i] = val;
if (--remaining === 0) {
resolve(args);
}
// race
// for (; i < len; i++) {
// promises[i].then(resolver,rejecter);
// }
}
複製代碼
結果也就大概簡化成了:
setTimeout(() => {
console.log(1)
}, 1000)
setTimeout(() => {
console.log(2)
}, 1500)
複製代碼
複製代碼
結論:瞭解宏任務和微任務能夠有效緩解焦慮。
也許咱們在講JavaScript的時候,都會去說Js是一個單線程的擁有異步特性的事件驅動的解釋型腳本語言。雖然它是單線程的,可是在保證流暢性和性能優化方便擁有各類各樣的異步任務和主線程進行通訊,異步能夠說是Js的一大難點和重點。不少時候初學者們都在爲這個異步任務什麼時候執行而感到迷茫。在瞭解異步機制以前,咱們仍是須要在一下基礎的概念或者理論上達成一個有效共識,這樣會很大程度上幫助咱們。
如今咱們就從Chrome瀏覽器的主要進程入手,瞭解一下咱們經常使用的工具是如何切分這些線程和進程的。這裏有一些關於進程、線程、多線程相關總結。
從通俗易懂的角度來理解:
進程
像是一個工廠,進程擁有獨立的資源 -- 獨立的內存空間。進程
之間相互獨立,沒有更大型的內存空間包裹。進程
(工廠)之間想要通訊,能夠藉助第三方進程 -- 管理進程。進程
內的多個線程,共享進程
的資源 -- 共享內存空間。用偏官方的話術來表示一下:
進程
是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)根據上面的知識,咱們很容易就能發現瀏覽器做爲不少程序的集合體,它的設計必定是一個多進程的,若是他只有一個進程那麼體驗會差到爆炸。咱們也常常會聽到這樣的一個說法,說Google Chrome瀏覽器是一個性能怪獸,每每打開它內存就會飆升。那麼一個瀏覽器中,有哪些進程呢:
從Chrome的任務管理器中,能夠清晰的看到那些正在運行在咱們瀏覽器上的進程。
擴展一下:關於Chrome有好多種內存管理的機制,這也是Chrome強大的地方,能夠在瀏覽器裏面輸入 chrome://flags
進行設置。
Process-per-site-instance
。每打開一個網站,而後從這個網站鏈開的一系列網站都屬於一個進程。這也是Chrome的默認進程模型。Process-per-site
。同域名範疇的網站屬於一個進程。Process-per-tab
。每個頁面都是一個獨立的進程。這就是外界盛傳的進程模型。Single Process
。傳統瀏覽器的單進程模型。對於整個瀏覽器來說,咱們上文說到了瀏覽器有自身的瀏覽器進程。可是這個進程對於每一個標籤頁中顯示的網頁內容來說,幫助不大。它只負責一個調度管理的做用。真正在瀏覽器大部分窗口內工做的仍是Renderer
進程。所以咱們把 Renderer進程稱之爲瀏覽器內核。
來了解一下Renderer進程包含哪些線程:
他們在Renderer進程下,各司其職。關於他們的詳細工做,估計又是一篇系列長文。待我寫完以後,會補充一個連接到這裏。
從這些共識中,咱們能夠理解以前的那句對JavaScript的描述了把。JavaScrit是一個單線程( JS引擎是單線程的 )的擁有異步特性( 擁有獨特的異步線程 )的事件驅動( 事件也是一個單獨的線程處理 )的解釋型腳本語言。
在這一章中,咱們不先不關心瀏覽器渲染進程中的其餘線程,也不關心具體的JS代碼上下文,做用域等細節問題。把注意力集中在JS引擎上,從宏觀上觀察一下瀏覽器內核的一些特性。這將在很長一段時間內有助於咱們梳理咱們所寫代碼執行流程,避免意外的發生。
在繼續深刻研究以前,咱們先來回憶一些知識點,避免有疏漏對下面的內容難以理解:
先來看段簡單的代碼理解一下調用關係。
console.log('1');
function a() {
console.log('2');
b();
console.log('3');
}
function b() {
console.log('4');
}
a();
// output: 1 2 4 3
複製代碼
相信你已經很快就獲得了答案,由於這段代碼中是純同步執行的,也沒有事件,IO等異步方法。因此咱們知道他的調用數序,可是程序是如何知道調用數序的呢?或者說程序執行的時候有什麼很牛的辦法麼?
你可能懷疑這樣的一個事情發生,就像剛剛學習這門技術時候的我同樣認爲程序會不會作下面的事情呢?
程序設計的時候可能沒有那麼的粗暴,由於這樣會致使一系列的問題好比函數做用域如何處理呢?那它可能有它做爲程序來說的辦法來實現這種調用 -- 執行棧(調用棧)
在上面的程序執行的時候,調用棧的工做順序爲:
注:
再來看下這個不通常的程序:
function hello() {
hello();
}
hello();
複製代碼
這個程序的獨特之處在於,它一直在像執行棧中插入 hello()
這個棧楨,沒一會咱們的執行棧就會溢出,(內存溢出)。這個時候瀏覽器就會假死掉,報出溢出的錯誤。
咱們在書寫代碼的時候,其實運用的 大部分是 JS這門高級語言封裝的各類API,剩下的一部分Api不是JS引擎封裝的,而是跟JS這麼門語言處於的環境有關係的。好比在瀏覽器中咱們直接使用的Navigator
就是瀏覽器環境決定的,在Node.js中就不能用,同理 Node.js中的 process
瀏覽器也是不能用的。
JS引擎是一個單線程的設計,可是在Web應用中少不了發送網絡請求的場景,JS引擎不能徹底靜止的等待網絡請求結束在進行下面的工做,所以咱們有理由懷疑網絡請求有本身的單獨的線程來處理,不和主線程搶資源。
const url = 'https://xxx.com/api/getName';
fetch(url).then(res => res.json()).then(console.log);
複製代碼
首先須要知道的是,定時器線程也是脫離JS引擎的獨立線程,爲何會給他這種特殊的待遇呢?道理我想很好理解:
setTimeout(function(){
console.log('1');
}, 0);
console.log('2');
複製代碼
由於是異步線程執行的,那麼結果應該是 2 1
。
const $btn = document.getElementById('btn');
$btn.addEventListener('click', console.log);
const timeoutId = setTimeout(() => {
for (let i = 0; i < 10000; i++) {
console.log('hello');
}
clearTimeout(timeoutId);
}, 5000);
複製代碼
看上面代碼的執行過程:在5s以後開啓一個事件循環,使JS引擎處於阻塞狀態,講道理的話若是事件的觸發不在單獨線程上解決,那麼在這5s以後JS處理循環的時候,事件都不會被感知和觸發(由於JS引擎阻塞了,你的入棧不會執行)。
可是事實結果確實: 在循環的開始的時候,你點擊按鈕也會獲得響應,只不過這個響應會在循環執行完成以後發生,可是已經說明了事件被觸發了。至於爲何在以後執行,咱們看下一章事件循環的時候會說起。
如今咱們知道了,無論是JS引擎實現的仍是瀏覽器等運行環境實現的一些Api,他們擁有特權 -- 專門處理本身事務的線程。這解決了不少問題,那麼如今新問題的關鍵出現了,獨立的線程是如何和JS引擎通訊的呢。搞懂了這個問題,那麼JS的異步運行的機制也就清晰了。
這應該就是咱們這個章節的主角 -- 大名鼎鼎的Task Queue。咱們先看一張圖找找Task Queue的位置。
對於任務隊列來講,上面所列就是通用規則,就是在不斷進步的過程當中總會對這些規則進行不斷修正。正因如此,在ES6的Promise
和HTML5的MutationObserver
出現以後,任務隊列就變得複雜了,主要體如今:將任務隊列中的任務按照等級從新肯定順序,等待Event—Loop的調用。咱們接下來的任務就是對這個順序進行研究。
事件循環,就是咱們常常說的那個 Event-Loop,想必你們應該都會對它有所耳聞。事件循環是任務隊列和JS主引擎之間的橋樑。EventLoop觸發也是有時機的,它被設計出來的目的也就是爲了保證JS引擎線程的安全和穩定的。全部只有等到JS引擎空閒的時候纔會經過EventLoop來取這時候在任務隊列中排隊等待的任務。
由於宏任務和微任務既設計任務隊列又跟EventLoop有關係,又是異步中很關鍵的一個概念,因此單獨來談談關於宏任務和微任務的問題。本章將從HTML規範 - Event Loop入手。來看看EventLoop是怎麼區分宏任務和微任務的。
首先,若是咱們按照任務隊列章節的內容來進行理解,隊列作爲一個數據結構應該是先進先出的結構,若是任務是一樣存在於一個隊列裏的,那應該按照順序執行。來看一個例子:
setTimeout(() => {
console.log(1)
},0)
Promise.resolve().then(() => {
console.log(2)
})
console.log(3)
複製代碼
輸出: 3 2 1
確定是大多數人都知道的結局,那麼這就直接和咱們對於任務隊列的理解是相悖的。也就是說 任務隊列 有點不同。帶着這個問題,咱們去翻翻規範。
爲了協調事件、用戶交互、腳本、渲染、網絡等,用戶代理必須使用這一小節描述的事件循環。
從上面的例子中,咱們產生了一個問題,並懷疑隊列中任務仍然是有優先級的。可是按照這個思路繼續想的話很容易產生矛盾的地方:
若是從性能的角度考慮,應該會設計成兩個獨立的列表,分別存聽任務。咱們仍是去看規範中,怎麼定義EventLoop,對於規範來說,着實是很是詳細的,總結來看有如下重要的不容錯過的點:
果真從規範中,咱們瞭解到EventLoop能夠對應多個隊列。流程也就變成了這樣。
對於 Task Queue 和 Microtask Queue 常有這樣的總結:
Promise
、process.nextTick
、MutaionObserver
因此通過理論的驗證咱們的出這樣的
同步任務 -> Micro -> Task -> Mic1, Mic2, ... -> Task -> Mic1, Mic2
能夠根據上面的結論來看一個DEMO
console.log(1)
// Part A
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
})
// Part B
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
// Part C
setTimeout(() => {
console.log(9)
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
複製代碼
首先:
1, 7
。 (Promise 的參數函數是同步的)PartA
和PartC
的兩個setTimeout
放入Task列表中,把PartB
中的 .then
產生的新的Promise放入到 Micro中。8
Part A
。2,4
, 把 .then 放入 Mico 而後清空它 ,輸出 5
。Part C
。9, 11
, 把 .then 放入 Mico 而後清空它 ,輸出 12
。1, 7, 8, 2, 4, 5, 9, 11, 12
在明白了Task和MicroTask的順序以後,基本上在瀏覽器中就不會有應用上的問題。
注意:若是你不想在Node中使用的話這部分能夠繞行避免發生混淆。接下來咱們在Node環境下運行代碼看看有沒有什麼'異常'發生。
console.log(1)
// Part A
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(3)
resolve()
}).then(() => {
console.log(4)
})
})
// Part B
setTimeout(() => {
console.log(5)
new Promise(resolve => {
console.log(6)
resolve()
}).then(() => {
console.log(7)
})
})
複製代碼
根據咱們以前的經驗,會很快得出結果。
1,2,3,4,5,6,7
。1,2,3,5,6,4,7
。詳細的說明這個問題,咱們能夠先提出這樣的一個懷疑 :
在 Node.js 中,setTimeout 和 Promise 用了一樣的方法實現。經過咱們以前的經驗來說,可能Node 用了和以前ES6-Promsie出現以前的方案同樣,使用了setTimeout進行僞實現,也就是說Node的Promise不是微任務。
帶着這個疑問我翻看了Node的源碼,源碼(V12.3.1)在下方的連接裏,這裏直接來看得出這個結論,結論可能跟咱們想的不太同樣,又差不太多:
這確實出乎咱們的意料,咱們用這個結論去跑一個示例,來看看能不能解釋的通:
console.log(1)
// Part A
setTimeout(() => {
console.log(2)
process.nextTick(() => {
console.log(3)
})
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
})
// Part B
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
// Part C
process.nextTick(() => {
console.log(6)
})
// Part D
setTimeout(() => {
console.log(9)
process.nextTick(() => {
console.log(10)
})
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
複製代碼
來分析一下Node執行的步驟:
1,7
。6
。8
。2, 4
9, 11
3, 10, 5, 2
總結輸出: 1 7 6 8 2 4 9 11 3 10 5 2
這應該就是 node 與 Chrome 瀏覽器中, 異步機制的不一樣之處把。,對於有一些研究文章說不會存在穩定的輸出結果,致使timer執行的不穩定,我以爲多是Node版本的問題,12中的結果是會穩定輸出的,多是數據結構進行了升級,這個部分仍是有待詳細研究。
new Promise((resolve) => {
console.log(1);
setTimeout(() => {
console.log(2);
resolve();
}, 1000)
}).then(() => {
console.log(3)
setTimeout(() => {
console.log(4)
}, 1000)
}).then(()=>{
console.log(5);
setTimeout(() => {
console.log(6)
}, 1000)
})
複製代碼
這個代碼是不會獲得理想輸出的。輸出結果爲: 1 -> 2 3 5 -> 4 6
new Promise((resolve) => {
console.log(1);
setTimeout(() => {
console.log(2);
resolve();
}, 1000)
}).then(() => {
console.log(3)
return new Promise((resolve)=>{
setTimeout(() => {
console.log(4)
resolve()
}, 1000)
})
}).then(()=>{
console.log(5);
new Promise(()=>{
setTimeout(() => {
console.log(6)
}, 1000)
})
})
複製代碼