JavaScript 從誕生起就是單線程
但是JavaScript 引擎有多個線程,單個腳本只能在一個線程上運行(稱爲主線程),其他線程都是在後臺配合。
文章目錄
- 同步任務是那些沒有被引擎掛起、在主線程上排隊執行的任務。只有前一個任務執行完畢,才能執行後一個任務。
- 異步任務是那些被引擎放在一邊,不進入主線程、而進入任務隊列的任務。只有引擎認爲某個異步任務可以執行了(比如 Ajax 操作從服務器得到了結果),該任務(採用回調函數的形式)纔會進入主線程執行。
任務隊列和事件循環
JavaScript 運行時,除了一個正在運行的主線程,引擎還提供一個任務隊列(task queue),裏面是各種需要當前程序處理的異步任務。
線程會去執行所有的同步任務。等到同步任務全部執行完,就會去看任務隊列裏面的異步任務。如果滿足條件,那麼異步任務就重新進入主線程開始執行,這時它就變成同步任務了。等到執行完,下一個異步任務再進入主線程開始執行。一旦任務隊列清空,程序就結束執行。
事件循環(Event Loop):一種循環檢查的機制, 只要同步任務執行完了引擎就會去檢查那些掛起來的異步任務,是不是可以進入主線程了。
異步操作
異步操作的幾種模式
1 回調函數
function f1(callback) {
// ...
callback();
}
function f2() {
// ...
}
f1(f2);
優點是簡單、容易理解和實現,
缺點是不利於代碼的閱讀和維護,各個部分之間高度耦合(coupling),使得程序結構混亂、流程難以追蹤(尤其是多個回調函數嵌套的情況),而且每個任務只能指定一個回調函數。
2 事件監聽
事件驅動模式。異步任務的執行不取決於代碼的順序,而取決於某個事件是否發生。
爲f1綁定一個事件,當事件發生時執行f2
優點是比較容易理解,可以綁定多個事件,每個事件可以指定多個回調函數,而且可以“去耦合”(decoupling),有利於實現模塊化。
缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。閱讀代碼的時候,很難看出主流程。
3 發佈/訂閱
某個任務執行完成,就向信號中心“發佈”(publish)一個信號,其他任務可以向信號中心“訂閱”(subscribe)這個信號,從而知道什麼時候自己可以開始執行。這就叫做”發佈/訂閱模式”(publish-subscribe pattern),又稱“觀察者模式”(observer pattern)。
性質與“事件監聽”類似,但是明顯優於後者。因爲可以通過查看“消息中心”,瞭解存在多少信號、每個信號有多少訂閱者,從而監控程序的運行。
4 異步操作的流程控制
如果有多個異步操作,就存在一個流程控制的問題:如何確定異步操作執行的順序,以及如何保證遵守這種順序
- 串行
編寫一個流程控制函數,讓它來控制異步任務,一個任務完成以後,再執行另一個。 - 並行
流程控制函數也可以是並行執行,即所有異步任務同時執行,等到全部完成以後,才執行final函數。 - 並行與串行的結合
設置一個門檻,每次最多隻能並行執行n個異步任務,這樣就避免了過分佔用系統資源。
定時器
定時器(timer),主要由setTimeout()和setInterval()這兩個函數來完成。它們向任務隊列添加定時任務。
setTimeout()
用來指定某個函數或某段代碼,在多少毫秒之後執行。它返回一個整數,表示定時器的編號,以後可以用來取消這個定時器。
//延遲delay毫秒後執行 func函數或code代碼
var timerId = setTimeout(func|code, delay);
setInterval()
setInterval函數的用法與setTimeout完全一致,區別僅僅在於setInterval指定某個任務每隔一段時間就執行一次,也就是無限次的定時執行, 直到關閉當前窗口。
clearTimeout(),clearInterval()
setTimeout和setInterval函數,都返回一個整數值,表示計數器編號。將該整數傳入clearTimeout和clearInterval函數,就可以取消對應的定時器。
var id1 = setTimeout(f, 1000);
var id2 = setInterval(f, 1000);
clearTimeout(id1);
clearInterval(id2);
運行機制
setTimeout和setInterval的運行機制,是將指定的代碼移出本輪事件循環,等到下一輪事件循環,再檢查是否到了指定時間。如果到了,就執行對應的代碼;如果不到,就繼續等待。
這意味着,setTimeout和setInterval指定的回調函數,必須等到本輪事件循環的所有同步任務都執行完,纔會開始執行。由於前面的任務到底需要多少時間執行完,是不確定的,所以沒有辦法保證,setTimeout和setInterval指定的任務,一定會按照預定時間執行。
Promise 對象
Promise 是一個對象,也是一個構造函數。
起到代理作用(proxy),充當異步操作與回調函數之間的中介 ,讓異步操作寫起來,就像在寫同步操作的流程,而不必一層層地嵌套回調函數。
Promise 的設計思想是,所有異步任務都返回一個 Promise 實例。Promise 實例有一個then方法,用來指定下一步的回調函數。
function f1(resolve, reject) {
// 異步代碼...
}
var p1 = new Promise(f1);
//f1的異步操作執行完成,就會執行f2
p1.then(f2);
// 傳統寫法
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// ...
});
});
});
});
// Promise 的寫法
(new Promise(step1))
.then(step2)
.then(step3)
.then(step4);
Promise 實例具有三種狀態。
- 異步操作未完成(pending)
- [已定型]resolved - 異步操作成功(fulfilled)
- [已定型]resolved - 異步操作失敗(rejected)
Promise 構造函數
var promise = new Promise(function (resolve, reject) {
// ...
if (/* 異步操作成功 */){
resolve(value);
} else { /* 異步操作失敗 */
reject(new Error());
}
});
Promise構造函數接受一個函數作爲參數,該函數的兩個參數分別是resolve和reject。它們是兩個函數,由 JavaScript 引擎提供,不用自己實現。
- resolve函數的作用是,將Promise實例的狀態從“未完成”變爲“成功”(即從pending變爲fulfilled)
在異步操作成功時調用,並將異步操作的結果作爲參數傳遞出去。 - reject函數的作用是,將Promise實例的狀態從“未完成”變爲“失敗”(即從pending變爲rejected)
在異步操作失敗時調用,並將異步操作報出的錯誤作爲參數傳遞出去。
Promise.prototype.then()
then方法,用來添加回調函數
then方法可以接受兩個回調函數,一旦狀態改變,就調用相應的回調函數。
- 第一個是異步操作成功(變爲fulfilled狀態)的回調函數,
- 第二個是異步操作失敗(變爲rejected狀態)的回調函數(該參數可以省略)。
//p1的狀態變爲成功, 對應的回調函數會收到異步操作傳回的值,然後在控制檯輸出
var p1 = new Promise(function (resolve, reject) {
resolve('成功');
});
p1.then(console.log, console.error);
// "成功"
//p2的狀態變爲失敗, 對應的回調函數會收到異步操作傳回的值,然後在控制檯輸出
var p2 = new Promise(function (resolve, reject) {
reject(new Error('失敗'));
});
p2.then(console.log, console.error);
// Error: 失敗
then方法可以鏈式使用
p1
.then(step1)
.then(step2)
.then(step3)
/*
最後一個then方法,回調函數是console.log和console.error,
console.log只顯示step3的返回值,
console.error可以顯示p1、step1、step2、step3之中任意一個發生的錯誤。
*/
.then(
console.log,
console.error
);
//前一步的狀態變爲fulfilled,就會依次執行緊跟在後面的回調函數。
//如果step1的狀態變爲rejected,那麼step2和step3都不會執行了(因爲它們是fulfilled的回調函數), 會執行接下來第一個爲rejected的回調函數
用法
Promise 的用法,簡單說就是一句話:使用then方法添加回調函數
// 寫法一
f1().then(function () {
return f2();
});
//f3回調函數的參數,是f2函數的運行結果。
f1().then(function () {
return f2();
}).then(f3);
// 寫法二
f1().then(function () {
f2();
});
//f3回調函數的參數是undefined
f1().then(function () {
f2();
return;
}).then(f3);
// 寫法三
f1().then(f2());
//f3回調函數的參數,是f2函數返回的函數的運行結果
f1().then(f2())
.then(f3);
// 寫法四
f1().then(f2);
//f3回調函數的參數,是f2函數的運行結果。 f2會接收到f1()返回的結果
f1().then(f2)
.then(f3);