ES2017 標準引入了 async 函數,使得異步操做變得更加方便。javascript
async 函數是什麼?一句話,它就是 Generator 函數的語法糖。html
前文有一個 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);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
複製代碼
上面代碼的函數gen
能夠寫成async
函數,就是下面這樣。java
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 函數的星號(*
)替換成async
,將yield
替換成await
,僅此而已。node
async
函數對 Generator 函數的改進,體如今如下四點。webpack
(1)內置執行器。git
Generator 函數的執行必須靠執行器,因此纔有了co
模塊,而async
函數自帶執行器。也就是說,async
函數的執行,與普通函數如出一轍,只要一行。github
asyncReadFile();
複製代碼
上面的代碼調用了asyncReadFile
函數,而後它就會自動執行,輸出最後結果。這徹底不像 Generator 函數,須要調用next
方法,或者用co
模塊,才能真正執行,獲得最後結果。web
(2)更好的語義。ajax
async
和await
,比起星號和yield
,語義更清楚了。async
表示函數裏有異步操做,await
表示緊跟在後面的表達式須要等待結果。
(3)更廣的適用性。
co
模塊約定,yield
命令後面只能是 Thunk 函數或 Promise 對象,而async
函數的await
命令後面,能夠是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時會自動轉成當即 resolved 的 Promise 對象)。
(4)返回值是 Promise。
async
函數的返回值是 Promise 對象,這比 Generator 函數的返回值是 Iterator 對象方便多了。你能夠用then
方法指定下一步的操做。
進一步說,async
函數徹底能夠看做多個異步操做,包裝成的一個 Promise 對象,而await
命令就是內部then
命令的語法糖。
async
函數返回一個 Promise 對象,可使用then
方法添加回調函數。當函數執行的時候,一旦遇到await
就會先返回,等到異步操做完成,再接着執行函數體內後面的語句。
下面是一個例子。
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name);
const stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result) {
console.log(result);
});
複製代碼
上面代碼是一個獲取股票報價的函數,函數前面的async
關鍵字,代表該函數內部有異步操做。調用該函數時,會當即返回一個Promise
對象。
下面是另外一個例子,指定多少毫秒後輸出一個值。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
複製代碼
上面代碼指定 50 毫秒之後,輸出hello world
。
因爲async
函數返回的是 Promise 對象,能夠做爲await
命令的參數。因此,上面的例子也能夠寫成下面的形式。
async function timeout(ms) {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
複製代碼
async 函數有多種使用形式。
// 函數聲明
async function foo() { return 1}
// 函數表達式
const foo = async function () {};
// 對象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// 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(…);
// 箭頭函數
const foo = async () => {};
複製代碼
async
函數的語法規則整體上比較簡單,難點是錯誤處理機制。
async
函數返回一個 Promise 對象。
async
函數內部return
語句返回的值,會成爲then
方法回調函數的參數。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
複製代碼
上面代碼中,函數f
內部return
命令返回的值,會被then
方法回調函數接收到。
@
async function f() {
await 'hello world';
}
f().then(v => console.log(v))
// undefined
// Promise {<resolved>: undefined}
// 上面的例子 沒有return 因此是undefined
@
async function f() {
a=await 'hello world';
return a
}
f().then(v => console.log(v))
// hello world
//Promise {<resolved>: undefined}
@
async function f() {
return await 'hello world';
}
f().then(v => console.log(v))
// hello world
// Promise {<resolved>: undefined}
@
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// hello world
// Promise {<resolved>: undefined}@
async function f() {
return setTimeout(()=>{console.log('hello world')});
}
f().then(v => console.log(v))
// 102
// Promise {<resolved>: undefined}
// hello world複製代碼
async
函數內部拋出錯誤,會致使返回的 Promise 對象變爲reject
狀態。拋出的錯誤對象會被catch
方法回調函數接收到。
async function f() {
throw new Error('出錯了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出錯了
複製代碼
async
函數返回的 Promise 對象,必須等到內部全部await
命令後面的 Promise 對象執行完,纔會發生狀態改變,除非遇到return
語句或者拋出錯誤。也就是說,只有async
函數內部的異步操做執行完,纔會執行then
方法指定的回調函數。
下面是一個例子。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
複製代碼
上面代碼中,函數getTitle
內部有三個操做:抓取網頁、取出文本、匹配頁面標題。只有這三個操做所有完成,纔會執行then
方法裏面的console.log
。
正常狀況下,await
命令後面是一個 Promise 對象,返回該對象的結果。若是不是 Promise 對象,就直接返回對應的值。
async function f() {
// 等同於
// return 123;
return await 123;
}
f().then(v => console.log(v))
// 123
複製代碼
上面代碼中,await
命令的參數是數值123
,這時等同於return 123
。
另外一種狀況是,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
命令後面是一個Sleep
對象的實例。這個實例不是 Promise 對象,可是由於定義了then
方法,await
會將其視爲Promise
處理。
這個例子還演示瞭如何實現休眠效果。JavaScript 一直沒有休眠的語法,可是藉助await
命令就可讓程序停頓指定的時間。下面給出了一個簡化的sleep
實現。
function sleep(interval) {
return new Promise(resolve => {
setTimeout(resolve, interval);
})
}
// 用法
async function one2FiveInAsync() {
for(let i = 1; i <= 5; i++) {
console.log(i);
await sleep(1000);
}
}
one2FiveInAsync();
複製代碼
await
命令後面的 Promise 對象若是變爲reject
狀態,則reject
的參數會被catch
方法的回調函數接收到。
async function f() {
await Promise.reject('出錯了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出錯了
複製代碼
注意,上面代碼中,await
語句前面沒有return
,可是reject
方法的參數依然傳入了catch
方法的回調函數。這裏若是在await
前面加上return
,效果是同樣的。
任何一個await
語句後面的 Promise 對象變爲reject
狀態,那麼整個async
函數都會中斷執行。
async function f() {
await Promise.reject('出錯了');
await Promise.resolve('hello world'); // 不會執行
}
複製代碼
上面代碼中,第二個await
語句是不會執行的,由於第一個await
語句狀態變成了reject
。
有時,咱們但願即便前一個異步操做失敗,也不要中斷後面的異步操做。這時能夠將第一個await
放在try...catch
結構裏面,這樣無論這個異步操做是否成功,第二個await
都會執行。
async function f() {
try {
await Promise.reject('出錯了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
複製代碼
另外一種方法是await
後面的 Promise 對象再跟一個catch
方法,處理前面可能出現的錯誤。
async function f() {
await Promise.reject('出錯了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出錯了
// hello world
複製代碼
若是await
後面的異步操做出錯,那麼等同於async
函數返回的 Promise 對象被reject
。
async function f() {
await new Promise(function (resolve, reject) {
throw new Error('出錯了');
});
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出錯了
複製代碼
上面代碼中,async
函數f
執行後,await
後面的 Promise 對象會拋出一個錯誤對象,致使catch
方法的回調函數被調用,它的參數就是拋出的錯誤對象。具體的執行機制,能夠參考後文的「async 函數的實現原理」。
防止出錯的方法,也是將其放在try...catch
代碼塊之中。
async function f() {
try {
await new Promise(function (resolve, reject) {
throw new Error('出錯了');
});
} catch(e) {
}
return await('hello world');
}
複製代碼
若是有多個await
命令,能夠統一放在try...catch
結構中。
async function main() {
try {
const val1 = await firstStep();
const val2 = await secondStep(val1);
const val3 = await thirdStep(val1, val2);
console.log('Final: ', val3);
}
catch (err) {
console.error(err);
}
}
複製代碼
下面的例子使用try...catch
結構,實現屢次重複嘗試。
const superagent = require('superagent');
const NUM_RETRIES = 3;
async function test() {
let i;
for (i = 0; i < NUM_RETRIES; ++i) {
try {
await superagent.get('http://google.com/this-throws-an-error');
break;
} catch(err) {}
}
console.log(i); // 3
}
test();
複製代碼
上面代碼中,若是await
操做成功,就會使用break
語句退出循環;若是失敗,會被catch
語句捕捉,而後進入下一輪循環。
第一點,前面已經說過,await
命令後面的Promise
對象,運行結果多是rejected
,因此最好把await
命令放在try...catch
代碼塊中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另外一種寫法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}
複製代碼
@錯誤處理的更優雅的方式:
你是否會爲了系統健壯性,亦或者是爲了捕獲異步的錯誤,而頻繁的在 async 函數中寫 try/catch 的邏輯?
async function func() {
try {
let res = await asyncFunc()
} catch (e) {
//......
}
}複製代碼
這樣咱們就可使用一個輔助函數包裹這個 async 函數實現錯誤捕獲
async function func() {
let [err, res] = await errorCaptured(asyncFunc)
if (err) {
//... 錯誤捕獲
}
//...
}
複製代碼
可是這麼作有一個缺陷就是每次使用的時候,都要引入 errorCaptured 這個輔助函數,有沒有「懶」的方法呢?
答案確定是有的,我在那篇博客後提出了一個新的思路,能夠經過一個 webpack loader 來自動注入 try/catch 代碼,最後的結果但願是這樣的
// development
async function func() {
let res = await asyncFunc()
//...其餘邏輯
}
// release
async function func() {
try {
let res = await asyncFunc()
} catch (e) {
//......
}
//...其餘邏輯
}
複製代碼
是否是很棒?在開發環境中不須要任何多餘的代碼,讓 webpack 自動給生產環境的代碼注入錯誤捕獲的邏輯,此處是經過ast來處理,上方是處理以前和以後的代碼。
第二點,多個await
命令後面的異步操做,若是不存在繼發關係,最好讓它們同時觸發。
let foo = await getFoo();
let bar = await getBar();
複製代碼
上面代碼中,getFoo
和getBar
是兩個獨立的異步操做(即互不依賴),被寫成繼發關係。這樣比較耗時,由於只有getFoo
完成之後,纔會執行getBar
,徹底可讓它們同時觸發。
// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
複製代碼
上面兩種寫法,getFoo
和getBar
都是同時觸發,這樣就會縮短程序的執行時間。
第三點,await
命令只能用在async
函數之中,若是用在普通函數,就會報錯。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 報錯
docs.forEach(function (doc) {
await db.post(doc);
});
}
複製代碼
上面代碼會報錯,由於await
用在普通函數之中了。可是,若是將forEach
方法的參數改爲async
函數,也有問題。
function dbFuc(db) { //這裏不須要 async
let docs = [{}, {}, {}];
// 可能獲得錯誤結果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
複製代碼
上面代碼可能不會正常工做,緣由是這時三個db.post
操做將是併發執行,也就是同時執行,而不是繼發執行。正確的寫法是採用for
循環。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
複製代碼
若是確實但願多個請求併發執行,可使用Promise.all
方法。當三個請求都會resolved
時,下面兩種寫法效果相同。
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的寫法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
複製代碼
第四點,async 函數能夠保留運行堆棧。
const a = () => {
b().then(() => c());
};
複製代碼
上面代碼中,函數a
內部運行了一個異步任務b()
。當b()
運行的時候,函數a()
不會中斷,而是繼續執行。等到b()
運行結束,可能a()
早就運行結束了,b()
所在的上下文環境已經消失了。若是b()
或c()
報錯,錯誤堆棧將不包括a()
。
如今將這個例子改爲async
函數。
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); });
});
}
複製代碼
咱們經過一個例子,來看 async 函數與 Promise、Generator 函數的比較。
假定某個 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 的寫法比回調函數的寫法大大改進,可是一眼看上去,代碼徹底都是 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;
});
}
複製代碼
上面代碼使用 Generator 函數遍歷了每一個動畫,語義比 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 函數的實現最簡潔,最符合語義,幾乎沒有語義不相關的代碼。它將 Generator 寫法中的自動執行器,改在語言層面提供,不暴露給用戶,所以代碼量最少。若是使用 Generator 寫法,自動執行器須要用戶本身提供。
實際開發中,常常遇到一組異步操做,須要按照順序完成。好比,依次遠程讀取一組 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) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
複製代碼
上面代碼確實大大簡化,問題是全部遠程操做都是繼發。只有前一個 URL 返回結果,纔會去讀取下一個 URL,這樣作效率不好,很是浪費時間。咱們須要的是併發發出遠程請求。
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
命令只能出如今 async 函數內部,不然都會報錯。
// 報錯
const data = await fetch('https://api.example.com');
複製代碼
上面代碼中,await
命令獨立使用,沒有放在 async 函數裏面,就會報錯。
目前,有一個語法提案,容許在模塊的頂層獨立使用await
命令。這個提案的目的,是借用await
解決模塊異步加載的問題。
// awaiting.js
let output;
async function main() {
const dynamic = await import(someMission);
const data = await fetch(url);
output = someProcess(dynamic.default, data);
}
main();
export { output };
複製代碼
上面代碼中,模塊awaiting.js
的輸出值output
,取決於異步操做。咱們把異步操做包裝在一個 async 函數裏面,而後調用這個函數,只有等裏面的異步操做都執行,變量output
纔會有值,不然就返回undefined
。
上面的代碼也能夠寫成當即執行函數的形式。
// awaiting.js
let output;
(async function main() {
const dynamic = await import(someMission);
const data = await fetch(url);
output = someProcess(dynamic.default, data);
})();
export { output };
複製代碼
下面是加載這個模塊的寫法。
// 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 對象判斷異步操做有沒有結束。
// awaiting.js
let output;
export default (async function main() {
const dynamic = await import(someMission);
const data = await fetch(url);
output = someProcess(dynamic.default, data);
})();
export { output };
複製代碼
上面代碼中,awaiting.js
除了輸出output
,還默認輸出一個 Promise 對象(async 函數當即執行後,返回一個 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 加載。
頂層的await
命令,就是爲了解決這個問題。它保證只有異步操做完成,模塊纔會輸出值。
// awaiting.js
const dynamic = import(someMission);
const data = fetch(url);
export const output = someProcess((await dynamic).default, await data);
複製代碼
上面代碼中,兩個異步操做在輸出的時候,都加上了await
命令。只有等到異步操做完成,這個模塊纔會輸出值。
加載這個模塊的寫法以下。
// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }
console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);
複製代碼
上面代碼的寫法,與普通的模塊加載徹底同樣。也就是說,模塊的使用者徹底不用關心,依賴模塊的內部有沒有異步操做,正常加載便可。
這時,模塊的加載會等待依賴模塊(上例是awaiting.js
)的異步操做完成,才執行後面的代碼,有點像暫停在那裏。因此,它老是會獲得正確的output
,不會由於加載時機的不一樣,而獲得不同的值。
下面是頂層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
命令有點像,交出代碼的執行權給其餘的模塊加載,等異步操做完成後,再拿回執行權,繼續向下執行。
Generator 函數是一個普通函數,可是有兩個特徵。一是,function
關鍵字與函數名之間有一個星號;二是,函數體內部使用yield
表達式,定義不一樣的內部狀態(yield
在英語裏的意思就是「產出」)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();複製代碼
上面代碼定義了一個 Generator 函數helloWorldGenerator
,它內部有兩個yield
表達式(hello
和world
),即該函數有三個狀態:hello,world 和 return 語句(結束執行)。
而後,Generator 函數的調用方法與普通函數同樣,也是在函數名後面加上一對圓括號。不一樣的是,調用 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象,也就是上一章介紹的遍歷器對象(Iterator Object)。
下一步,必須調用遍歷器對象的next
方法,使得指針移向下一個狀態。也就是說,每次調用next
方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield
表達式(或return
語句)爲止。換言之,Generator 函數是分段執行的,yield
表達式是暫停執行的標記,而next
方法能夠恢復執行。
上面代碼一共調用了四次next
方法。
第一次調用,Generator 函數開始執行,直到遇到第一個yield
表達式爲止。next
方法返回一個對象,它的value
屬性就是當前yield
表達式的值hello
,done
屬性的值false
,表示遍歷尚未結束。
第二次調用,Generator 函數從上次yield
表達式停下的地方,一直執行到下一個yield
表達式。next
方法返回的對象的value
屬性就是當前yield
表達式的值world
,done
屬性的值false
,表示遍歷尚未結束。
第三次調用,Generator 函數從上次yield
表達式停下的地方,一直執行到return
語句(若是沒有return
語句,就執行到函數結束)。next
方法返回的對象的value
屬性,就是緊跟在return
語句後面的表達式的值(若是沒有return
語句,則value
屬性的值爲undefined
),done
屬性的值true
,表示遍歷已經結束。
第四次調用,此時 Generator 函數已經運行完畢,next
方法返回對象的value
屬性爲undefined
,done
屬性爲true
。之後再調用next
方法,返回的都是這個值。
總結一下,調用 Generator 函數,返回一個遍歷器對象,表明 Generator 函數的內部指針。之後,每次調用遍歷器對象的next
方法,就會返回一個有着value
和done
兩個屬性的對象。value
屬性表示當前的內部狀態的值,是yield
表達式後面那個表達式的值;done
屬性是一個布爾值,表示是否遍歷結束。
ES6 沒有規定,function
關鍵字與函數名之間的星號,寫在哪一個位置。這致使下面的寫法都能經過。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
複製代碼
因爲 Generator 函數仍然是普通函數,因此通常的寫法是上面的第三種,即星號緊跟在function
關鍵字後面。
因爲 Generator 函數返回的遍歷器對象,只有調用next
方法纔會遍歷下一個內部狀態,因此其實提供了一種能夠暫停執行的函數。yield
表達式就是暫停標誌。
遍歷器對象的next
方法的運行邏輯以下。
(1)遇到yield
表達式,就暫停執行後面的操做,並將緊跟在yield
後面的那個表達式的值,做爲返回的對象的value
屬性值。
(2)下一次調用next
方法時,再繼續往下執行,直到遇到下一個yield
表達式。
(3)若是沒有再遇到新的yield
表達式,就一直運行到函數結束,直到return
語句爲止,並將return
語句後面的表達式的值,做爲返回的對象的value
屬性值。
(4)若是該函數沒有return
語句,則返回的對象的value
屬性值爲undefined
。
須要注意的是,yield
表達式後面的表達式,只有當調用next
方法、內部指針指向該語句時纔會執行,所以等於爲 JavaScript 提供了手動的「惰性求值」(Lazy Evaluation)的語法功能。
function* gen() {
yield 123 + 456;
}
複製代碼
上面代碼中,yield
後面的表達式123 + 456
,不會當即求值,只會在next
方法將指針移到這一句時,纔會求值。
ajax的異步處理
function* main() {
var result = yield request("http://www.filltext.com?rows=10&f={firstName}");
console.log(result);
//do 別的ajax請求;
}複製代碼
一、co 模塊,它基於 ES6 的 generator 和 yield ,讓咱們能用同步的形式編寫異步代碼的nodejs模塊。
二、co 模塊是能讓咱們以同步的形式編寫異步代碼的 nodejs 模塊
三、學習網絡地址:https://segmentfault.com/a/1190000002732081
四、代碼以下:
var co = require ('co');
co(function*() {
執行代碼。。。
});複製代碼
co 模塊能夠將異步解放成同步。co 函數接受一個 generator 函數做爲參數,在函數內部自動執行 yield 。co 函數接受一個 generator 函數,而且在 co 函數內部執行,生成一個 generator 實例。調用 generator 的 next 方法, 對生成的對象的 value 屬性值使用 toPromise 方法,生成一個 promise 實例,當這個 promise 實例的狀態變爲 resolved 時,執行 onFulfilled 方法,再次對 generator 實例執行 next 方法,而後重複整個過程。若是出現錯誤,則執行這個 promise 實例定義的 reject 函數即 onRejected 方法。
javascript中的thunk函數就是一個單參數函數,且該參數必須是一個callback函數,callback的簽名必須爲callback(err,args...);
所謂的thunkify就是將一個多參數函數轉化爲一個thunk函數,該多參數函數必須有一個callback做爲參數
next
方法,就會返回一個有着value
和done
兩個屬性的對象async
函數就是將 Generator 函數的星號(*
)替換成async
,將yield
替換成await
async function foo() { return 1}複製代碼
以上async函數雖然有返回值,可是,其實真正的返回值並不是所看到的那樣,而是被 promise 包裝過的返回值。
async function a() {
return await 2;
}
a(); // Promise {<resolved>: 2}
a().then(function (result) {
console.log(result);return 3
})
.then((data)=>console.log(data));
// 2
// 3
// Promise {<resolved>: undefined}複製代碼
感謝阮一峯老師的付出,本文主要內容來源於阮一峯老師博客,特此說明。