.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+陸續出現了 Promise
、Generator
、Async/await
,力求在寫法上簡潔明瞭(扁平化),可讀性強(更優雅、更簡潔)。chrome
========================= 我是分割線 ==========================數組
以上只是鋪墊,下面在進入正題 👇,開始說道說道主角:async/await
========================= 我是分割線 ==========================
async/await
是參照 Generator
封裝的一套異步處理方案,能夠理解爲 Generator
的語法糖,
因此瞭解 async/await
就不得不講一講 Generator
(首次將協程的概念引入 js
,是協程的子集,不過因爲不能指定讓步的協程,只能讓步給生成器(迭代器)的調用者,因此也稱爲非對稱協程),
而 Generator
又返回迭代器Iterator
對象,
因此就得先講一講 Iterator
,
而 Iterator
和 Generator
都屬於協程,
終於找到源頭了:協程
wiki:協程(英語:coroutine)是計算機程序的一類組件,推廣了協做式多任務的子程序,容許執行被掛起與被恢復。相對子例程而言,協程更爲通常和靈活,但在實踐中使用沒有子例程那樣普遍。協程更適合於用來實現彼此熟悉的程序組件,如協做式多任務、異常處理、事件循環、迭代器、無限列表和管道
協程能夠經過 yield(取其「讓步」之義而非「出產」)來調用其它協程,接下來的每次協程被調用時,從協程上次 yield 返回的位置接着執行,經過 yield 方式轉移執行權的協程之間不是調用者與被調用者的關係,而是彼此對稱、平等的
協程是追求極限性能和優美的代碼結構的產物 協程間的調用是邏輯上可控的,時序上肯定的
協程是一種比線程更加輕量級的存在,是語言層級的構造,可看做一種形式的控制流,在內存間執行,無像線程間切換的開銷。你能夠把協程當作是跑在線程上的任務,一個線程上能夠存在多個協程,可是在線程上同時只能執行一個協程。
協程概念的提出比較早,單核CPU場景中發展出來的概念,經過提供掛起和恢復接口,實如今單個CPU上交叉處理多個任務的併發功能。
那麼本質上就是在一個線程的基礎上,增長了不一樣任務棧的切換,經過不一樣任務棧的掛起和恢復,線程中進行交替運行的代碼片斷,實現併發的功能。
其實從這裏能夠看出 「協程間的調用是邏輯上可控的,時序上肯定的」
那麼如何理解 js 中的協程呢?
generator
把 js 公路變成了多車道(協程實現),可是同一時間只有一個車道上的車能開(依然單線程),不過能夠自由變道(移交控制權)這裏是一個簡單的例子證實協程的實用性。假設這樣一種生產者-消費者的關係,一個協程生產產品並將它們加入隊列,另外一個協程從隊列中取出產品並消費它們。僞碼錶示以下:
var q := 新建隊列
coroutine 生產者
loop
while q 不滿載
創建某些新產品
向 q 增長這些產品
yield 給消費者
coroutine 消費者
loop
while q 不空載
從 q 移除某些產品
使用這些產品
yield 給生產者
複製代碼
v8 實現源碼:js-generator、runtime-generator
編譯模擬實現(es5):regenerator
經過以上,我僞裝你明白什麼是協程,下一步開始說一說迭代器 Iterator
Iterator
翻譯過來就是**迭代器(遍歷器)**讓咱們先來看看它的遍歷過程(相似於單向鏈表):
next
方法,將指針指向數據結構的第一個成員next
方法,將指針指向數據結構的第二個成員next
方法,直到它指向數據結構的結束位置一個對象要變成可迭代的,必須實現 @@iterator
方法,即對象(或它原型鏈上的某個對象)必須有一個名字是 Symbol.iterator
的屬性(原生具備該屬性的有:String
、Array
、TypedArray
、Map
和 Set
)可經過常量 Symbol.iterator
訪問:
屬性 | 值 |
---|---|
[Symbol.iterator]: | 返回一個對象的無參函數,被返回對象符合迭代器協議 |
當一個對象須要被迭代的時候(好比開始用於一個 for..of
循環中),它的 @@iterator
方法被調用而且無參數,而後返回一個用於在迭代中得到值的迭代器
迭代器協議:產生一個有限或無限序列的值,而且當全部的值都已經被迭代後,就會有一個默認的返回值
當一個對象只有知足下述條件纔會被認爲是一個迭代器:
它實現了一個 next()
的方法,該方法必須返回一個對象,對象有兩個必要的屬性:
done
(bool)
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"]]
複製代碼
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
:生成器對象是生成器函數(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}
複製代碼
看到這裏,你可能會問,Generator
和 callback
有啥關係,如何處理異步呢?其實兩者沒有任何關係,咱們只是經過一些方式強行的它們產生了關係,纔會有 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 }
複製代碼
以上代碼是 Generator
和 callback
結合實現的異步,能夠看到,仍然須要手動執行 .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 入門很是棒,我時不時的都會去看一看)
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
是 Generator
的語法糖,上面我是分割線下的第一句已經講過,先來看一下兩者的對比:
// 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
中使用co + generator
比 cb
的方式性能差)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');
}
複製代碼
// 繼發一
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
連帶出來這麼多知識點,之後在使用它時,但願可以幫助到你
【參考】:
===🧐🧐 文中不足,歡迎指正 🤪🤪===