Async:簡潔優雅的異步之道

前言

在異步處理方案中,目前最爲簡潔優雅的即是async函數(如下簡稱A函數)。通過必要的分塊包裝後,A函數能使多個相關的異步操做如同同步操做同樣聚合起來,使其相互間的關係更爲清晰、過程更爲簡潔、調試更爲方便。它本質是Generator函數的語法糖,通俗的說法是使用G函數進行異步處理的加強版。segmentfault

嘗試

學習A函數必須有Promise基礎,最好還了解Generator函數,有須要的可查看延伸小節。數組

爲了直觀的感覺A函數的魅力,下面使用Promise和A函數進行了相同的異步操做。該異步的目的是獲取用戶的留言列表,須要分頁,分頁由後臺控制。具體的操做是:先獲取到留言的總條數,再更正當前須要顯示的頁數(每次切換到不一樣頁時,總數目可能會發生變化),最後傳遞參數並獲取到相應的數據。併發

let totalNum = 0; // Total comments number.
let curPage = 1; // Current page index.
let pageSize = 10; // The number of comment displayed in one page.

// 使用A函數的主代碼。
async function dealWithAsync() {
  totalNum = await getListCount();
  console.log('Get count', totalNum);
  if (pageSize * (curPage - 1) > totalNum) {
    curPage = 1;
  }

  return getListData();
}

// 使用Promise的主代碼。
function dealWithPromise() {
  return new Promise((resolve, reject) => {
    getListCount().then(res => {
      totalNum = res;
      console.log('Get count', res);
      if (pageSize * (curPage - 1) > totalNum) {
        curPage = 1;
      }

      return getListData()
    }).then(resolve).catch(reject);
  });
}

// 開始執行dealWithAsync函數。
// dealWithAsync().then(res => {
//   console.log('Get Data', res)
// }).catch(err => {
//   console.log(err);
// });

// 開始執行dealWithPromise函數。
// dealWithPromise().then(res => {
//   console.log('Get Data', res)
// }).catch(err => {
//   console.log(err);
// });

function getListCount() {
  return createPromise(100).catch(() => {
    throw 'Get list count error';
  });
}

function getListData() {
  return createPromise([], {
    curPage: curPage,
    pageSize: pageSize,
  }).catch(() => {
    throw 'Get list data error';
  });
}


function createPromise(
  data, // Reback data
  params = null, // Request params
  isSucceed = true,
  timeout = 1000,
) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      isSucceed ? resolve(data) : reject(data);
    }, timeout);
  });
}

對比dealWithAsyncdealWithPromise兩個簡單的函數,能直觀的發現:使用A函數,除了有await關鍵字外,與同步代碼無異。而使用Promise則須要根據規則增長不少包裹性的鏈式操做,產生了太多回調函數,不夠簡約。另外,這裏分開了每一個異步操做,並規定好各自成功或失敗時傳遞出來的數據,近乎實際開發。異步

1 登堂

1.1 形式

A函數也是函數,因此具備普通函數該有的性質。不過形式上有兩點不一樣:一是定義A函數時,function關鍵字前須要有async關鍵字(意爲異步),表示這是個A函數。二是在A函數內部可使用await關鍵字(意爲等待),表示會將其後面跟隨的結果當成異步操做並等待其完成。async

如下是它的幾種定義方式。函數

// 聲明式
async function A() {}

// 表達式
let A = async function () {};

// 做爲對象屬性
let o = {
  A: async function () {}
};

// 做爲對象屬性的簡寫式
let o = {
  async A() {}
};

// 箭頭函數
let o = {
  A: async () => {}
};

1.2 返回值

執行A函數,會固定的返回一個Promise對象。學習

獲得該對象後即可監設置成功或失敗時的回調函數進行監聽。若是函數執行順利並結束,返回的P對象的狀態會從等待轉變成成功,並輸出return命令的返回結果(沒有則爲undefined)。若是函數執行途中失敗,JS會認爲A函數已經完成執行,返回的P對象的狀態會從等待轉變成失敗,並輸出錯誤信息。spa

// 成功執行案例

A1().then(res => {
  console.log('執行成功', res); // 10
});

async function A1() {
  let n = 1 * 10;
  return n;
}

// 失敗執行案例

A2().catch(err => {
  console.log('執行失敗', err); // i is not defined.
});

async function A2() {
  let n = 1 * i;
  return n;
}

1.3 await

只有在A函數內部纔可使用await命令,存在於A函數內部的普通函數也不行。調試

引擎會統一將await後面的跟隨值視爲一個Promise,對於不是Promise對象的值會調用Promise.resolve()進行轉化。即使此值爲一個Error實例,通過轉化後,引擎依然視其爲一個成功的Promise,其數據爲Error的實例。code

當函數執行到await命令時,會暫停執行並等待其後的Promise結束。若是該P對象最終成功,則會返回成功的返回值,至關將await xxx替換成返回值。若是該P對象最終失敗,且錯誤沒有被捕獲,引擎會直接中止執行A函數並將其返回對象的狀態更改成失敗,輸出錯誤信息。

最後,A函數中的return x表達式,至關於return await x的簡寫。

// 成功執行案例

A1().then(res => {
  console.log('執行成功', res); // 約兩秒後輸出100。
});

async function A1() {
  let n1 = await 10;
  let n2 = await new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 2000);
  });
  return n1 * n2;
}

// 失敗執行案例

A2().catch(err => {
  console.log('執行失敗', err); // 約兩秒後輸出10。
});

async function A2() {
  let n1 = await 10;
  let n2 = await new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(10);
    }, 2000);
  });
  return n1 * n2;
}

2 入室

2.1 繼發與併發

對於存在於JS語句(for, while等)的await命令,引擎遇到時也會暫停執行。這意味着能夠直接使用循環語句處理多個異步。

如下是處理繼發的兩個例子。A函數處理相繼發生的異步尤其簡潔,總體上與同步代碼無異。

// 兩個方法A1和A2的行爲結果相同,都是每隔一秒輸出10,輸出三次。

async function A1() {
  let n1 = await createPromise();
  console.log('N1', n1);
  let n2 = await createPromise();
  console.log('N2', n2);
  let n3 = await createPromise();
  console.log('N3', n3);
}

async function A2() {
  for (let i = 0; i< 3; i++) {
    let n = await createPromise();
    console.log('N' + (i + 1), n);
  }
}

function createPromise() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}

接下來是處理併發的三個例子。A1函數使用了Promise.all生成一個聚合異步,雖然簡單但靈活性下降了,只有都成功和失敗兩種狀況。A3函數相對A2僅僅爲了說明應該怎樣配合數組的遍歷方法使用async函數。重點在A2函數的理解上。

A2函數使用了循環語句,實際是繼發的獲取到各個異步值,但在整體的時間上至關併發(這裏須要好好理解一番)。由於一開始建立reqs數組時,就已經開始執行了各個異步,以後雖然是逐一繼發獲取,但總花費時間與遍歷順序無關,恆等於耗時最多的異步所花費的時間(不考慮遍歷、執行等其它的時間消耗)。

// 三個方法A1, A2和A3的行爲結果相同,都是在約一秒後輸出[10, 10, 10]。

async function A1() {
  let res = await Promise.all([createPromise(), createPromise(), createPromise()]);
  console.log('Data', res);
}

async function A2() {
  let res = [];
  let reqs = [createPromise(), createPromise(), createPromise()];
  for (let i = 0; i< reqs.length; i++) {
    res[i] = await reqs[i];
  }
  console.log('Data', res);
}

async function A3() {
  let res = [];
  let reqs = [9, 9, 9].map(async (item) => {
    let n = await createPromise(item);
    return n + 1;
  });
  for (let i = 0; i< reqs.length; i++) {
    res[i] = await reqs[i];
  }
  console.log('Data', res);
}

function createPromise(n = 10) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(n);
    }, 1000);
  });
}

2.2 錯誤處理

一旦await後面的Promise轉變成rejected,整個async函數便會終止。然而不少時候咱們不但願由於某個異步操做的失敗,就終止整個函數,所以須要進行合理錯誤處理。注意,這裏所說的錯誤不包括引擎解析或執行的錯誤,僅僅是狀態變爲rejectedPromise對象。

處理的方式有兩種:一是先行包裝Promise對象,使其始終返回一個成功的Promise。二是使用try.catch捕獲錯誤。

// A1和A2都執行成,且返回值爲10。
A1().then(console.log);
A2().then(console.log);

async function A1() {
  let n;
  n = await createPromise(true);
  return n;
}

async function A2() {
  let n;
  try {
    n = await createPromise(false);
  } catch (e) {
    n = e;
  }
  return n;
}

function createPromise(needCatch) {
  let p = new Promise((resolve, reject) => {
    reject(10);
  });
  return needCatch ? p.catch(err => err) : p;
}

2.3 實現原理

前言中已經說起,A函數是使用G函數進行異步處理的加強版。既然如此,咱們就從其改進的方面入手,來看看其基於G函數的實現原理。A函數相對G函數的改進體如今這幾個方面:更好的語義,內置執行器和返回值是Promise

更好的語義。G函數經過在function後使用*來標識此爲G函數,而A函數則是在function前加上async關鍵字。在G函數中可使用yield命令暫停執行和交出執行權,而A函數是使用await來等待異步返回結果。很明顯,asyncawait更爲語義化。

// G函數
function* request() {
  let n = yield createPromise();
}

// A函數
async function request() {
  let n = await createPromise();
}

function createPromise() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}

內置執行器。調用A函數便會一步步自動執行和等待異步操做,直到結束。若是須要使用G函數來自動執行異步操做,須要爲其建立一個自執行器。經過自執行器來自動化G函數的執行,其行爲與A函數基本相同。能夠說,A函數相對G函數最大改進即是內置了自執行器。

// 二者都是每隔一秒鐘打印出10,重複兩次。

// A函數
A();

async function A() {
  let n1 = await createPromise();
  console.log(n1);
  let n2 = await createPromise();
  console.log(n2);
}

// G函數,使用自執行器執行。
spawn(G);

function* G() {
  let n1 = yield createPromise();
  console.log(n1);
  let n2 = yield createPromise();
  console.log(n2);
}

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}


function createPromise() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(10);
    }, 1000);
  });
}

2.4 執行順序

在瞭解A函數內部與包含它外部間的執行順序前,須要明白兩點:一爲Promise的實例方法是推遲到本輪事件末尾才執行的後執行操做,詳情請查看連接。二爲Generator函數是經過調用實例方法來切換執行權進而控制程序執行順序,詳情請查看連接。理解好A函數的執行順序,能更加清楚的把握此三者的存在。

先看如下代碼,對比A一、A2和A3方法的結果。

F(A1); // 接連打印出:1 3 4 2 5。
F(A2); // 接連打印出:1 3 2 4 5。
F(A3); // 先打印出:1 3 2,隔兩秒後打印出:4 9。

function F(A) {
  console.log(1);
  A().then(console.log);
  console.log(2);
}

async function A1() {
  console.log(3);
  console.log(4);
  return 5;
}

async function A2() {
  console.log(3);
  let n = await 5;
  console.log(4);
  return n;
}

async function A3() {
  console.log(3);
  let n = await createPromise();
  console.log(4);
  return n;
}

function createPromise() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(9);
    }, 2000);
  });
}

從結果上可概括出一些表面形態。執行A函數,會即刻執行其函數體,直到遇到await命令。遇到await命令後,執行權會轉向A函數外部,即無論A函數內部執行而開始執行外部代碼。執行完外部代碼(本輪事件)後,才繼續執行以前await命令後面的代碼。

概括到此已成功一半,以後着手分析其成因。若是客官您對本樓有所瞭解,那必定不會忘記‘自執行器’這位大嬸吧?估計是忘記了。A函數的本質就是帶有自執行器的G函數,因此探究A函數的執行原理就是探究使用自執行器的G函數的執行原理。想起了?

再看下面代碼,使用相同邏輯的G函數會獲得與A函數相同的結果。

F(A); // 先打印出:1 3 2,隔兩秒後打印出:4 9。
F(() => {
  return spawn(G);
}); // 先打印出:1 3 2,隔兩秒後打印出:4 9。

function F(A) {
  console.log(1);
  A().then(console.log);
  console.log(2);
}

async function A() {
  console.log(3);
  let n = await createPromise();
  console.log(4);
  return n;
}

function* G() {
  console.log(3);
  let n = yield createPromise();
  console.log(4);
  return n;
}

function createPromise() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(9);
    }, 2000);
  });
}

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

自動執行G函數時,遇到yield命令後會使用Promise.resolve包裹其後的表達式,併爲其設置回調函數。不管該Promise是馬上有告終果仍是過某段時間以後,其回調函數都會被推遲到在本輪事件末尾執行。以後再是下一步,再下一步。一樣的道理適用於A函數,當遇到await命令時(此處略去三五字),因此有了如此這般的執行順序。謝幕。

延伸

ES6精華:Promise
Generator:JS執行權的真實操做者

相關文章
相關標籤/搜索