ES6
經常使用但被忽略的方法 系列文章,整理做者認爲一些平常開發可能會用到的一些方法、使用技巧和一些應用場景,細節深刻請查看相關內容鏈接,歡迎補充交流。async
函數是 Generator
函數的語法糖。const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
// Generator函數寫法
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
// async 函數寫法
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
複製代碼
async
函數對 Generator
函數的改進:
Generator
函數的執行必須靠執行器,因此纔有了co
模塊(基於 ES6
的 Generator
和 yield
,讓咱們能用同步的形式編寫異步代碼),而async
函數自帶執行器。(async
函數的執行,與普通函數如出一轍,只要一行。)asyncReadFile();
複製代碼
Generator
函數,須要調用next
方法,或者用co
模塊,才能真正執行,獲得最後結果。const gg = gen();
gg.next()
複製代碼
async
和await
,比起*
和yield
,語義更清楚。async
表示函數裏有異步操做,await
表示緊跟在後面的表達式須要等待結果。co
模塊約定,yield
命令後面只能是 Thunk
函數或 Promise
對象,而async
函數的await
命令後面,能夠是 Promise
對象和原始類型的值(數值、字符串和布爾值,但這時會自動轉成當即 resolved
的 Promise
對象)。Promise
。async
函數的返回值是 Promise
對象,這比 Generator
函數的返回值是 Iterator
對象方便不少。async
函數返回一個 Promise
對象,可使用then
方法添加回調函數。async
函數內部return
語句返回的值,會成爲then
方法回調函數的參數。 async
函數內部拋出錯誤,會致使返回的 Promise
對象變爲reject
狀態。拋出的錯誤對象會被catch
方法回調函數接收到。async function getName() {
return 'detanx';
}
getName().then(val => console.log(val))
// "detanx"
// 拋出錯誤
async function f() {
throw new Error('出錯了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出錯了
複製代碼
// 函數聲明
async function foo() {}
// 函數表達式
const foo = async function () {};
// 對象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// 箭頭函數
const foo = async () => {};
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
複製代碼
async
函數返回的 Promise
對象,必須等到內部全部await
命令後面的 Promise
對象執行完,纔會發生狀態改變,除非遇到return
語句或者拋出錯誤。await
命令
await
命令後面是一個 Promise
對象,返回該對象的結果。若是不是 Promise
對象,就直接返回對應的值。async function getName() {
return 'detanx';
}
getName().then(val => console.log(val))
// "detanx"
複製代碼
await
命令後面是一個thenable
對象(定義了then
方法的對象),那麼await
會將其等同於 Promise
對象。class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(
() => resolve(Date.now() - startTime),
this.timeout
);
}
}
(async () => {
const sleepTime = await new Sleep(1000);
console.log(sleepTime);
})();
// 1000
複製代碼
await
語句後面的 Promise
對象變爲reject
狀態,那麼整個async
函數都會中斷執行。async function f() {
await Promise.reject('出錯了');
await Promise.resolve('hello world'); // 不會執行
}
複製代碼
await
放在try...catch
裏面async function f() {
try {
await Promise.reject('出錯了');
} catch(e) {
}
return await Promise.resolve('detanx');
}
f()
.then(v => console.log(v))
// detanx
複製代碼
await
後面的 Promise
對象再跟一個catch
方法,處理前面可能出現的錯誤。async function f() {
await Promise.reject('出錯了')
.catch(e => console.log(e));
return await Promise.resolve('detanx');
}
f()
.then(v => console.log(v))
// 出錯了
// detanx
複製代碼
await
命令後面的Promise
對象,運行結果多是rejected
,因此最好把await
命令放在try...catch
代碼塊中。await
命令後面的異步操做,若是不存在繼發關係,最好讓它們同時觸發。// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
複製代碼
await
命令只能用在async
函數之中,若是用在普通函數,就會報錯。正確的寫法是採用for
循環或者使用數組的reduce
方法。但願多個請求併發執行,可使用Promise.all
或者Promise.allSettled
方法。async function dbFuc(db) {
let docs = [{}, {}, {}];
// 報錯,await的上一級函數不是async函數
docs.forEach(function (doc) {
await db.post(doc);
});
=> for循環
for (let doc of docs) {
await db.post(doc);
}
=> 數組的reduce方法
await docs.reduce(async (_, doc) => {
await _;
await db.post(doc);
}, undefined);
}
複製代碼
async
函數能夠保留運行堆棧。const a = () => {
b().then(() => c());
};
複製代碼
上面代碼中,函數a
內部運行了一個異步任務b()
。當b()
運行的時候,函數a()
不會中斷,而是繼續執行。等到b()
運行結束,可能a()
早就運行結束,b()
所在的上下文環境已經消失。若是b()
或c()
報錯,錯誤堆棧將不包括a()
。git
如今將這個例子改爲async
函數。es6
const a = async () => {
await b();
c();
};
複製代碼
b()
運行的時候,a()
是暫停執行,上下文環境都保存着。一旦b()
或c()
報錯,錯誤堆棧將包括a()
。async
函數的實現原理,就是將 Generator
函數和自動執行器,包裝在一個函數裏。async function fn(args) {
// ...
}
// 等同於
function fn(args) {
return spawn(function* () {
// ...
});
}
複製代碼
async
函數均可以寫成上面的第二種形式,其中的spawn
函數就是自動執行器。下面spawn
函數的實現,基本就是前文自動執行器的翻版。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); });
});
}
複製代碼
傳統方法,ES6
誕生之前,異步編程的方法,大概有下面四種。github
Promise
對象經過一個例子,主要來看 Promise
、Generator
函數與async
函數的比較,其餘的不太熟悉的能夠本身去查相關的資料。shell
假定某個 DOM
元素上面,部署了一系列的動畫,前一個動畫結束,才能開始後一個。若是當中有一個動畫出錯,就再也不往下執行,返回上一個成功執行的動畫的返回值。數據庫
Promise
的寫法function chainAnimationsPromise(elem, animations) {
// 變量ret用來保存上一個動畫的返回值
let ret = null;
// 新建一個空的Promise
let p = Promise.resolve();
// 使用then方法,添加全部動畫
for(let anim of animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
});
}
// 返回一個部署了錯誤捕捉機制的Promise
return p.catch(function(e) {
/* 忽略錯誤,繼續執行 */
}).then(function() {
return ret;
});
}
複製代碼
Promise
的 API
(then
、catch
等等),操做自己的語義反而不容易看出來。Generator
函數的寫法。function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
let ret = null;
try {
for(let anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略錯誤,繼續執行 */
}
return ret;
});
}
複製代碼
Promise
寫法更清晰,用戶定義的操做所有都出如今spawn
函數的內部。問題在於,必須有一個任務運行器,自動執行 Generator
函數,上面代碼的spawn
函數就是自動執行器,它返回一個 Promise
對象,並且必須保證yield
語句後面的表達式,必須返回一個 Promise
。async
函數的寫法。async function chainAnimationsAsync(elem, animations) {
let ret = null;
try {
for(let anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略錯誤,繼續執行 */
}
return ret;
}
複製代碼
Async
函數的實現最簡潔,最符合語義,幾乎沒有語義不相關的代碼。全部的異步處理方法存在即合理,沒有那個最好,只有最合適,在處理不一樣的實際狀況時,咱們選擇最適合的處理方法便可。編程
URL
,而後按照讀取的順序輸出結果。
Promise
的寫法。function logInOrder(urls) {
// 遠程讀取全部URL
const textPromises = urls.map(url => {
return fetch(url).then(response => response.text());
});
// 按次序輸出
textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise)
.then(text => console.log(text));
}, Promise.resolve());
}
複製代碼
fetch
方法,同時遠程讀取一組 URL
。每一個fetch
操做都返回一個 Promise
對象,放入textPromises
數組。而後,reduce
方法依次處理每一個 Promise
對象,而後使用then
,將全部 Promise
對象連起來,所以就能夠依次輸出結果。async
函數實現。async function logInOrder(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);
}
}
複製代碼
map
方法的參數是async
函數,但它是併發執行的,由於只有async
函數內部是繼發執行,外部不受影響。後面的for..of
循環內部使用了await
,所以實現了按順序輸出。await
await
命令只能出如今 async
函數內部,不然都會報錯。有一個語法提案(目前提案處於Status: Stage 3
),容許在模塊的頂層獨立使用 await
命令,使得上面那行代碼不會報錯了。這個提案的目的,是借用 await
解決模塊異步加載的問題。
awaiting.js
的輸出值output
,取決於異步操做。// awaiting.js
let output;
(async function1 main() {
const dynamic = await import(someMission);
const data = await fetch(url);
output = someProcess(dynamic.default, data);
})();
export { output };
複製代碼
awaiting.js
模塊// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);
複製代碼
outputPlusValue()
的執行結果,徹底取決於執行的時間。若是awaiting.js
裏面的異步操做沒執行完,加載進來的output
的值就是undefined
。Promise
對象,從這個 Promise
對象判斷異步操做有沒有結束。// usage.js
import promise, { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
promise.then(() => {
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);
});
複製代碼
awaiting.js
對象的輸出,放在promise.then()
裏面,這樣就能保證異步操做完成之後,纔去讀取output
。Promise
加載,只使用正常的加載方法,依賴這個模塊的代碼就可能出錯。並且,若是上面的usage.js
又有對外的輸出,等於這個依賴鏈的全部模塊都要使用 Promise
加載。// awaiting.js
const dynamic = import(someMission);
const data = fetch(url);
export const output = someProcess((await dynamic).default, await data);
// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);
複製代碼
await
命令。只有等到異步操做完成,這個模塊纔會輸出值。await
的一些使用場景。// import() 方法加載
const strings = await import(`/i18n/${navigator.language}`);
// 數據庫操做
const connection = await dbConnector();
// 依賴回滾
let jQuery;
try {
jQuery = await import('https://cdn-a.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.com/jQuery');
}
複製代碼
await
命令的模塊,加載命令是同步執行的。// x.js
console.log("X1");
await new Promise(r => setTimeout(r, 1000));
console.log("X2");
// y.js
console.log("Y");
// z.js
import "./x.js";
import "./y.js";
console.log("Z");
複製代碼
z.js
加載x.js
和y.js
,打印結果是X1
、Y
、X2
、Z
。這說明,z.js
並無等待x.js
加載完成,再去加載y.js
。await
命令有點像,交出代碼的執行權給其餘的模塊加載,等異步操做完成後,再拿回執行權,繼續向下執行。