JavaScrip異步

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 異步操作的流程控制

如果有多個異步操作,就存在一個流程控制的問題:如何確定異步操作執行的順序,以及如何保證遵守這種順序

  1. 串行
    編寫一個流程控制函數,讓它來控制異步任務,一個任務完成以後,再執行另一個。
  2. 並行
    流程控制函數也可以是並行執行,即所有異步任務同時執行,等到全部完成以後,才執行final函數。
  3. 並行與串行的結合
    設置一個門檻,每次最多隻能並行執行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 實例具有三種狀態。

  1. 異步操作未完成(pending)
  2. [已定型]resolved - 異步操作成功(fulfilled)
  3. [已定型]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方法可以接受兩個回調函數,一旦狀態改變,就調用相應的回調函數。

  1. 第一個是異步操作成功(變爲fulfilled狀態)的回調函數,
  2. 第二個是異步操作失敗(變爲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);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章