Async/Await 如何經過同步的方式實現異步

  • last update:2020-12-14

.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}javascript

首先想要更好的理解 Async/Await,須要瞭解這兩個知識點:vue

  • 同步
  • 異步

背景

首先,js 是單線程的(重複三遍),所謂單線程, 通俗的講就是,一根筋(比喻有點過度,哈哈)執行代碼是一行一行的往下走(即所謂的同步), 若是上面的沒執行完,就癡癡的等着(是否是很像戀愛中在路邊等她/他的你,僞裝 new 了個對象,啊哈哈哈,調皮一下很開心), 仍是舉個 🌰 吧:java

// chrome 81
function test() {
  let d = Date.now();
  for (let i = 0; i < 1e8; i++) {}
  console.log(Date.now() - d); // 62ms-90ms左右
}

function test1() {
  let d = Date.now();

  console.log(Date.now() - d); // 0
}

test();
test1();
複製代碼

上面僅僅是一個 for 循環,而在實際應用中,會有大量的網絡請求,它的響應時間是不肯定的,這種狀況下也要癡癡的等麼?顯然是不行的,於是 js 設計了異步,即 發起網絡請求(諸如 IO 操做,定時器),因爲須要等服務器響應,就先不理會,而是去作其餘的事兒,等請求返回告終果的時候再說(即異步)。 那麼如何實現異步呢?其實咱們平時已經在大量使用了,那就是 callback,例如:node

// 網絡請求
$.ajax({
  url: 'http://xxx',
  success: function(res) {
    console.log(res);
  },
});
複製代碼

success 做爲函數傳遞過去並不會當即執行,而是等請求成功了才執行,即回調函數(callback)git

// IO操做
const fs = require('fs');

fs.rename('舊文件.txt', '新文件.txt', err => {
  if (err) throw err;
  console.log('重命名完成');
});
複製代碼

和網絡請求相似,等到 IO 操做有告終果(不管成功與否)纔會執行第三個參數:(err)=>{}es6

從上面咱們就能夠看出,實現異步的核心就是回調鉤子,將 cb 做爲參數傳遞給異步執行函數,當有告終果後在觸發 cb。想了解更多,去看看 event-loop 機制吧。github

至於 async/await 是如何出現的呢,在 es6 以前,大多 js 數項目中會有相似這樣的代碼:ajax

ajax1(url, () => {
  // do something 1
  ajax2(url, () => {
    // do something 2
    ajax3(url, () => {
      // do something 3
      // ...
    });
  });
});
複製代碼

這種函數嵌套,大量的回調函數,使代碼閱讀起來晦澀難懂,不直觀,形象的稱之爲回調地獄(callback hell),因此爲了在寫法上能更通俗一點,es6+陸續出現了 PromiseGeneratorAsync/await,力求在寫法上簡潔明瞭(扁平化),可讀性強(更優雅、更簡潔)。chrome

========================= 我是分割線 ==========================數組

以上只是鋪墊,下面在進入正題 👇,開始說道說道主角:async/await

========================= 我是分割線 ==========================

async/await 是參照 Generator 封裝的一套異步處理方案,能夠理解爲 Generator 的語法糖,

async-await

因此瞭解 async/await 就不得不講一講 Generator(首次將協程的概念引入 js,是協程的子集,不過因爲不能指定讓步的協程,只能讓步給生成器(迭代器)的調用者,因此也稱爲非對稱協程),

Generator 又返回迭代器Iterator對象,

因此就得先講一講 Iterator,

IteratorGenerator 都屬於協程,

終於找到源頭了:協程

協程

wiki:協程(英語:coroutine)是計算機程序的一類組件,推廣了協做式多任務的子程序,容許執行被掛起與被恢復。相對子例程而言,協程更爲通常和靈活,但在實踐中使用沒有子例程那樣普遍。協程更適合於用來實現彼此熟悉的程序組件,如協做式多任務、異常處理、事件循環、迭代器、無限列表和管道

協程能夠經過 yield(取其「讓步」之義而非「出產」)來調用其它協程,接下來的每次協程被調用時,從協程上次 yield 返回的位置接着執行,經過 yield 方式轉移執行權的協程之間不是調用者與被調用者的關係,而是彼此對稱、平等的

協程是追求極限性能和優美的代碼結構的產物 協程間的調用是邏輯上可控的,時序上肯定的

協程是一種比線程更加輕量級的存在,是語言層級的構造,可看做一種形式的控制流,在內存間執行,無像線程間切換的開銷。你能夠把協程當作是跑在線程上的任務,一個線程上能夠存在多個協程,可是在線程上同時只能執行一個協程。

協程概念的提出比較早,單核CPU場景中發展出來的概念,經過提供掛起恢復接口,實如今單個CPU上交叉處理多個任務的併發功能。

那麼本質上就是在一個線程的基礎上,增長了不一樣任務棧的切換,經過不一樣任務棧的掛起和恢復,線程中進行交替運行的代碼片斷,實現併發的功能。

其實從這裏能夠看出 「協程間的調用是邏輯上可控的,時序上肯定的」

那麼如何理解 js 中的協程呢?

  • js 公路只是單行道(主線程),可是有不少車道(輔助線程)均可以匯入車流(異步任務完成後回調進入主線程的任務隊列)
  • generator 把 js 公路變成了多車道(協程實現),可是同一時間只有一個車道上的車能開(依然單線程),不過能夠自由變道(移交控制權)

協程實現

這裏是一個簡單的例子證實協程的實用性。假設這樣一種生產者-消費者的關係,一個協程生產產品並將它們加入隊列,另外一個協程從隊列中取出產品並消費它們。僞碼錶示以下:

var q := 新建隊列

coroutine 生產者
  loop
    while q 不滿載
      創建某些新產品
      向 q 增長這些產品
    yield 給消費者

coroutine 消費者
  loop
    while q 不空載
      從 q 移除某些產品
      使用這些產品
    yield 給生產者
複製代碼

v8 實現源碼:js-generatorruntime-generator

編譯模擬實現(es5):regenerator

經過以上,我僞裝你明白什麼是協程,下一步開始說一說迭代器 Iterator

Iterator

Iterator 翻譯過來就是**迭代器(遍歷器)**讓咱們先來看看它的遍歷過程(相似於單向鏈表):

  • 建立一個指針對象,指向當前數據結構的起始位置
  • 第一次調用指針對象的 next 方法,將指針指向數據結構的第一個成員
  • 第二次調用指針對象的 next 方法,將指針指向數據結構的第二個成員
  • 不斷的調用指針對象的 next 方法,直到它指向數據結構的結束位置

一個對象要變成可迭代的,必須實現 @@iterator 方法,即對象(或它原型鏈上的某個對象)必須有一個名字是 Symbol.iterator 的屬性(原生具備該屬性的有:StringArrayTypedArrayMapSet)可經過常量 Symbol.iterator 訪問:

屬性
[Symbol.iterator]: 返回一個對象的無參函數,被返回對象符合迭代器協議

當一個對象須要被迭代的時候(好比開始用於一個 for..of 循環中),它的 @@iterator 方法被調用而且無參數,而後返回一個用於在迭代中得到值的迭代器

迭代器協議:產生一個有限或無限序列的值,而且當全部的值都已經被迭代後,就會有一個默認的返回值

當一個對象只有知足下述條件纔會被認爲是一個迭代器:

它實現了一個 next() 的方法,該方法必須返回一個對象,對象有兩個必要的屬性:

  • done(bool)
    • true:迭代器已經超過了可迭代次數。這種狀況下,value 的值能夠被省略
    • 若是迭代器能夠產生序列中的下一個值,則爲 false。這等效於沒有指定 done 這個屬性
  • value 迭代器返回的任何 JavaScript 值。done 爲 true 時可省略

根據上面的規則,我們來自定義一個簡單的迭代器:

const getRawType = (target) => Object.prototype.toString.call(target).slice(8,-1);

const __createArrayIterable = (arr) => {
  if (typeof Symbol !== 'function' || !Symbol.iterator) return {};
  if(getRawType(arr) !== 'Array') throw new Error('it must be Array');
  const iterable = {};
  iterable[Symbol.iterator] = () => {
    arr.length++;
    const iterator = {
      next: () => ({ value: arr.shift(), done: arr.length <= 0 })
    }
    return iterator;
  };
  return iterable;
};

const itable = __createArrayIterable(['人月',  '神話']);
const it = itable[Symbol.iterator]();

console.log(it.next()); // { value: "人月", done: false }
console.log(it.next()); // { value: "神話", done: false }
console.log(it.next()); // {value: undefined, done: true }
複製代碼

咱們還能夠自定義一個可迭代對象:

Object.prototype[Symbol.iterator] = function () {
  const items = Object.entries(this);
  items.length++;
  return {
    next: () => ({ value: items.shift(), done: items.length <= 0 })
  }
}
// or
Object.prototype[Symbol.iterator] = function* () {
  const items = Object.entries(this);
  for (const item of items) {
    yield item;
  }
}
const obj = { name: 'amap', bu: 'sharetrip'}
for (let value of obj) {
  console.log(value);
}
// ["name", "amap"]
// ["bu", "sharetrip"]
// or
console.log([...obj]); // [["name", "amap"], ["bu", "sharetrip"]]
複製代碼

💡 除了 for map forEach 等方法如何遍歷一個數組?

參考答案
const getIterator = (iteratorable) => iteratorable[Symbol.iterator]();
const arr = [0,1,2,3,4,5];
const iterator = getIterator(arr);
while(true){
  const obj = iterator.next();
  if(obj.done){
    break;
  }
  console.log(obj.value);
}
複製代碼

瞭解了迭代器,下面能夠進一步瞭解生成器了

Generator

Generator:生成器對象是生成器函數(GeneratorFunction)返回的,它符合可迭代協議迭代器協議,既是迭代器也是可迭代對象,能夠調用 next 方法,但它不是函數,更不是構造函數

生成器函數(GeneratorFunction):

function* name([param[, param[, ... param]]]) { statements }

  • name:函數名
  • param:參數
  • statements:js 語句

調用一個生成器函數並不會立刻執行它裏面的語句,而是返回一個這個生成器的迭代器對象,當這個迭代器的 next() 方法被首次(後續)調用時,其內的語句會執行到第一個(後續)出現 yield 的位置爲止(讓執行處於暫停狀,掛起),yield 後緊跟迭代器要返回的值。或者若是用的是 yield*(多了個星號),則表示將執行權移交給另外一個生成器函數(當前生成器暫停執行),調用 next() (再啓動,喚醒)方法時,若是傳入了參數,那麼這個參數會做爲上一條執行的 yield 語句的返回值,例如:

function* another() {
  yield '人月神話';
}

function* gen() {
  yield* another(); // 移交執行權
  const a = yield 'hello';
  const b = yield a; // a='world' 是 next('world') 傳參賦值給了上一個 yidle 'hello' 的左值
  yield b; // b=! 是 next('!') 傳參賦值給了上一個 yidle a 的左值
}

const g = gen();
g.next(); // {value: "人月神話", done: false}
g.next(); // {value: "hello", done: false}
g.next('world'); // {value: "world", done: false} 將 'world' 賦給上一條 yield 'hello' 的左值,即執行 a='world',
g.next('!'); // {value: "!", done: false} 將 '!' 賦給上一條 yield a 的左值,即執行 b='!',返回 b
g.next(); // {value: undefined, done: false}
複製代碼

看到這裏,你可能會問,Generatorcallback 有啥關係,如何處理異步呢?其實兩者沒有任何關係,咱們只是經過一些方式強行的它們產生了關係,纔會有 Generator 處理異步

咱們來總結一下 Generator 的本質,暫停,它會讓程序執行到指定位置先暫停(yield),而後再啓動(next),再暫停(yield),再啓動(next),而這個暫停就很容易讓它和異步操做產生聯繫,由於咱們在處理異步時:開始異步處理(網絡求情、IO 操做),而後暫停一下,等處理完了,再該幹嗎幹嗎。不過值得注意的是,js 是單線程的(又重複了三遍),異步仍是異步,callback 仍是 callback,不會由於 Generator 而有任何改變

下面來看看,用 Generator + Promise 寫一段異步代碼:

const gen = function*() {
  const res1 = yield Promise.resolve({a: 1});
  const res2 = yield Promise.resolve({b: 2});
};

const g = gen();

const g1 = g.next();

console.log('g1:', g1);

g1.value
  .then(res1 => {
    console.log('res1:', res1);
    const g2 = g.next(res1);
    console.log('g2:', g2);
    g2.value
      .then(res2 => {
        console.log('res2:', res2);
        g.next(res2);
      })
      .catch(err2 => {
        console.log(err2);
      });
  })
  .catch(err1 => {
    console.log(err1);
  });
// g1: { value: Promise { <pending> }, done: false }
// res1: { "a": 1 }
// g2: { value: Promise { <pending> }, done: false }
// res2: { "b": 2 }
複製代碼

以上代碼是 Generatorcallback 結合實現的異步,能夠看到,仍然須要手動執行 .then 層層添加回調,但因爲 next() 方法返回對象 {value: xxx,done: true/false} 因此咱們能夠簡化它,寫一個自動執行器:

function run(gen) {
  const g = gen();

  function next(data) {
    const res = g.next(data);
    // 深度遞歸,只要 `Generator` 函數還沒執行到最後一步,`next` 函數就調用自身
    if (res.done) return res.value;
    res.value.then(function(data) {
      next(data);
    });
  }

  next();
}

run(function*() {
  const res1 = yield Promise.resolve({a: 1});
  console.log(res1);
  // { "a": 1 }
  const res2 = yield Promise.resolve({b: 2});
  console.log(res2);
  // { "b": 2 }
});
複製代碼

說了這麼多,怎麼尚未到 async/await,客官別急,立刻來了(其實我已經漏了一些內容沒說:Promise 和 callback 的關係,thunk 函數,co 庫,感興趣的能夠去 google 一下,ruanyifeng 老師講的es6 入門很是棒,我時不時的都會去看一看)

💡 分析下面 log 輸出什麼內容?

function* gen() {
  const ask1 = yield "2 + 2 = ?";
  console.log(ask1);

  const ask2 = yield "3 * 3 = ?"
  console.log(ask2);
}

const generator = gen();

console.log( generator.next().value );

console.log( generator.next(4).value );

console.log( generator.next(9).done );
複製代碼
參考答案
// 2 + 2 = ?
// 4
// 3 + 3 = ?
// 6
// true
複製代碼

Async/Await

首先,async/awaitGenerator 的語法糖,上面我是分割線下的第一句已經講過,先來看一下兩者的對比:

// Generator
run(function*() {
  const res1 = yield Promise.resolve({a: 1});
  console.log(res1);

  const res2 = yield Promise.resolve({b: 2});
  console.log(res2);
});

// async/await
const aa = async ()=>{
  const res1 = await Promise.resolve({a: 1});
  console.log(res1);

  const res2 = await Promise.resolve({b: 2});
  console.log(res2);

  return 'done';
}
const res = aa();
複製代碼

能夠看到,async function 代替了 function*await 代替了 yield,同時也無需本身手寫一個自動執行器 run

如今再來看看async/await 的特色:

  • await 後面跟的是 Promise 對象時,纔會異步執行,其它類型的數據會同步執行
  • 執行 const res = aa(); 返回的仍然是個 Promise 對象,上面代碼中的 return 'done'; 會直接被下面 then 函數接收到
res.then(data => {
  console.log(data); // done
});
複製代碼

最後我們來總結一下:

優勢:

  • 內置執行器:自帶執行器
  • 更好的語義:比起星號和 yield,語義更清楚了
  • 更廣的適用性:await 命令後面,能夠跟 Promise 對象和原始類型的值(這時等同於同步操做)

注意點:

  • await 命令後面的 Promise 對象,運行結果多是 rejected,因此最好把 await 命令放在 try...catch 代碼塊中
  • await 命令只能用在 async 函數之中,若是用在普通函數,就會報錯
  • 多個 await 命令後面的異步操做,若是不存在繼發關係,最好讓它們同時觸發(Promise.all
  • 再循環中需注意它的使用,儘可能在 for/for..of(迭代遍歷器) 中使用,永遠不要在 forEach/filter 中使用,也儘可能不要在 map 中使用
  • 兼容性(caniusenode.green)不太好,固然通常狀況下,能夠藉助編譯工具來進行 polyfill(babel)或 es6-shim(轉換後即語法糖實現的協程效率低,co + generatorcb 的方式性能差)
  • 能夠在生命週期函數中使用,在線例子: ReactVue
  • 錯誤捕獲:須要捕獲多個錯誤並作不一樣的處理時,能夠考慮給 await 後的 promise 對象添加 catch 函數,爲此咱們須要寫一個 helper:
// to.js
export default function to(promise) {
  return promise.then(data => {
    return [null, data];
  })
  .catch(err => [err]);
}

/***使用***/
import to from './to';

async function asyncTask() {

  const [err1, res1] = await to(fn1);
  if(!res1) throw new CustomerError('No res1 found');

  const [err2, res2] = await to(fn2);
  if(err) throw new CustomError('Error occurred while task2');
}
複製代碼

💡 給定一個 URL 數組,如何實現接口的繼發和併發?

參考答案
// 繼發一
async function loadData() {
  var res1 = await fetch(url1);
  var res2 = await fetch(url2);
  var res3 = await fetch(url3);
  return "when all done";
}
// 繼發二
async function loadData(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
/********/
// 併發一
async function loadData() {
  var res = await Promise.all([fetch(url1), fetch(url2), fetch(url3)]);
  return "when all done";
}
// 併發二
async function loadData(urls) {
  // 併發讀取 url
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });
  // 按次序輸出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
複製代碼

啊,終於完了,一個 async-await 連帶出來這麼多知識點,之後在使用它時,但願可以幫助到你

【參考】:

  1. developer.mozilla.org/zh-CN/docs/…
  2. es6.ruanyifeng.com/#docs/itera…
  3. es6.ruanyifeng.com/#docs/async

===🧐🧐 文中不足,歡迎指正 🤪🤪===

相關文章
相關標籤/搜索