Promise對象的兩個特點
- 對象的狀態不受外界影響。Promise對象代表一個異步操作,有三種狀態:
pending(進行中)
、fulfilled(已成功)
和rejected(已失敗)
- 一旦狀態改變,就不會再變。狀態的改變有兩種可能:
從pending到fulfilled和從pending變爲rejected。
優點
有了Promise
對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise
對象提供統一的接口,使得控制異步操作更加容易。
基本用法
下面是一個Promise實例
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise構造函數接受一個函數作爲參數,該函數的兩個參數分別爲resolve和reject。resolve函數的作用是將Promise對象的狀態由pending轉化爲fulfilled。reject函數的作用是將Promise對象的狀態由pending轉化爲rejected。
Promise實例生成之後,可以使用then方法分別指定resolved狀態和rejected狀態的回調函數,其中rejected狀態的回調函數是可選的。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
還有下面一種更爲常見的方式,網絡請求就常用此方式。此方式和上述方式是等價的。
promise.then(function(value) {
// success
}).catch(function(error) {
// failure
});)
下面是一個Promise對象的簡單例子。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
});
在知道上面函數調用的過程之前,另外需要知道的就是setTimeout
函數的調用方式,用於在指定的毫秒數後調用函數或計算表達式,還可以給指定調用的函數傳遞參數
。例如代碼setTimeout((a) => {console.log(a)}, 100, 'hello world')
就會在0.1秒之後打印hello world
。
這樣就可以理解上述的Promise對象例子的用法了。在返回的Promise對象狀態由pending狀態到fulfilled狀態之後,會執行傳入的resolve函數。setTimeout中傳遞的第一個參數resolve
就是用於回調的函數,第二個參數ms
就是在ms
毫秒之後執行resolve
函數,第三個參數'done'
就是傳遞給resolve函數調用時的參數。timeout(100)返回的Promise對象在狀態由pending
轉化爲fulfilled
的時候會調用傳入的匿名函數,該函數僅會打印傳入的參數值,由於傳入的參數爲字符串'done'
,所以會打印'done'
。
注意
Promise建立之後會立即執行。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// 打印結果順序如下:
// Promise
// Hi!
// resolved
上述代碼中,Promise新建之後會立即執行,所以首先輸出的是Promise
,但是then指定的回調函數會在最後將在當前腳本所有同步任務執行完纔會執行,所以resolved
最後輸出。原因可在之後的Event Loop中找到答案。
任務隊列
JavaScript中所有的任務可以分成兩種,一種是同步任務(synchronous)
,一種是異步任務(asynchronous)
。同步任務指的是在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程,而進步"任務隊列"(task queue)
的任務,只有"任務隊列"
通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。
具體來說,異步執行的運行機制如下。(同步執行也是如此,因爲它可以被視爲沒有異步任務的異步執行。
(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)主線程不斷重複上面的第三步。
下圖就是主線程和任務隊列的示意圖。
[外鏈圖片轉存失敗(img-KkV7Chyo-1569028299084)(http://www.ruanyifeng.com/blogimg/asset/2014/bg2014100801.jpg)]
只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會不斷重複。
事件和回調函數
“任務隊列"是一個事件的隊列(也可以理解成消息的隊列),IO設備完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務可以進入"執行棧"了。主線程讀取"任務隊列”,就是讀取裏面有哪些事件。
“任務隊列"中的事件,除了IO設備的事件以外,還包括一些用戶產生的事件(比如鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入"任務隊列”,等待主線程讀取。
所謂"回調函數"(callback),就是那些會被主線程掛起來的代碼。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。
"任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。但是,由於存在後文提到的"定時器"功能,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。
Event Loop
主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱爲Event Loop(事件循環)。
heap
中存放對象,stack
中存放當前運行的代碼。dom,ajax和setTimeout(setInterval一樣)之類的操作會交給WebAPIs
執行,在執行完成之後放到callback queue(和上述的任務隊列一致)
。在stack
中代碼全部執行完成之後,會查看callback queue
中是否有任務需要執行。如此反覆的進行就是事件循環。圖是截取自名爲《Help, I’m stuck in an event-loop》的演講。
上述關於隊列的介紹可以用一個簡單的例子來說明
setTimeout(function() {console.log(1)}, 0)
console.log(2)
// 打印順序如下:
// 2
// 1
setTimeout設置的定時時間爲0ms,即在0ms後打印,按照’正常的’步驟應該是立即打印1,然後再打印2:但是實際的打印結果中的順序是先打印2,然後再打印1。這是由於在執行setTimeout的時候,會交給WebAPIs
定時,定時完成之後交給callback queue
,而執行stack
會繼續往下執行,在執行完console.log(2)
之後,纔會查看在callback queue
中的任務並執行。由於這個定時任務定時0ms後執行,在咱看似沒有必要,卻仍然會按照既定的步驟執行。