前言
在公司的項目中拆讯,我們經(jīng)常用到async await 這樣的函數(shù)宰翅,它的作用也很奇特,可以讓異步的函數(shù)等待異步執(zhí)行的結(jié)果出來再繼續(xù)往下進行嘿架。我一直很好奇這是怎么做到的,它內(nèi)部的機理是怎么樣的蝉娜,就一個關(guān)鍵詞在函數(shù)前面加async,在異步操作前面加await就可以做到。他是怎么做到的呢坛增?
再拋出幾個問題
1 出處在哪里
現(xiàn)在我們用的vue項目就會把我們的語法打包編輯成瀏覽器可以識別的語句,那么async,await 是從什么地方出來的呢罢艾,他是怎么實現(xiàn)異步變同步的呢?
2 異步錯誤處理
我們的異步操作async await 如果錯了就不會繼續(xù)執(zhí)行,如果我想讓他繼續(xù)執(zhí)行應(yīng)該怎么做期奔? try cache? 還有呢馁痴? 為什么可以呢?內(nèi)部是怎么執(zhí)行的呢?
function Fun(){
return new Promise((resolve,reject) => {
setTimeout(reject(new Error('你錯了')),3000);
})
}
function Fun2(){
return new Promise((resolve) => {
setTimeout(resolve,3000);
})
}
async function g() {
// try{
await Fun();
// }catch(e){
// console.log('錯了');
// }
console.log(123);
await Fun2();
console.log(123);
}
g();
除了try catch 還可以怎么樣呢?
function Fun(){
return new Promise((resolve,reject) => {
setTimeout(reject(new Error('你錯了')),3000);
})
}
function Fun2(){
return new Promise((resolve) => {
setTimeout(resolve,3000);
})
}
async function g() {
await Fun().catch((e)=>{
console.log(e);
});
console.log(123);
await Fun2();
console.log(123);
}
g();
3 一個async 里面可以寫幾個await呢?
4 多個await 都是一個等執(zhí)行完再進行下一個垛吗,如果我所有的await 一起執(zhí)行應(yīng)該怎么做呢?
function Fun(){
return new Promise((resolve) => {
setTimeout(resolve,3000);
})
}
function Fun2(){
return new Promise((resolve) => {
setTimeout(resolve,3000);
})
}
async function g() {
await Fun();
console.log(123);
await Fun2();
console.log(123);
}
g();
// 方法一
let [fun1, fun2] = await Promise.all([Fun(),Fun2()]);
console.info(fun1);
console.info(fun2);
// 方法二
let Fun3 = Fun();
let Fun4 = Fun2();
let fun5 = await Fun3;
let fun6 = await Fun4;
console.info(fun5);
console.info(fun6);
我覺得這么多問題就足夠我們?nèi)ニ伎紴槭裁矗楷F(xiàn)在我們就開始試著去理解這些現(xiàn)象,和內(nèi)層的原理
但是想要了解 這些東西我們需要很多的基礎(chǔ)知識儲備掠归,有了這些知識儲備,其實也是很好理解的±欠福現(xiàn)在讓我們開始整理我們需要知道的知識點
首先去查 async await ,查到的結(jié)果是
ES2017 標準引入了 async 函數(shù)宋舷,使得異步操作變得更加方便音诈。 (也就是說 async 是 es7 的內(nèi)容)
async 函數(shù)就是 Generator 函數(shù)的語法糖。
async 函數(shù)就是將 Generator 函數(shù)的星號(*)替換成 async,將 yield 替換成 await
那么問題來了 Genertor函數(shù) 是什么函數(shù) 加() 替換成async 加 的函數(shù)是什么函數(shù)誓篱,yield 又是什么呢摆屯?党远??
要理解這些還要對promise 有一個基本的認識吧济似。
解密
Promise
1 概念
Promise 是異步編程的一種解決方案唉铜,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強大竞惋。它由社區(qū)最早提出和實現(xiàn)拆宛,ES6 將其寫進了語言標準,統(tǒng)一了用法物蝙,原生提供了Promise對象盖矫。
所謂promise责掏,簡單說就是一個容器,里面保存著某個未來才會結(jié)束的事件(通常是一個異步操作)的結(jié)果湃望。從語法上說换衬,Promise 是一個對象,從它可以獲取異步操作的消息证芭。Promise 提供統(tǒng)一的 API,各種異步操作都可以用同樣的方法進行處理
new Promise(function(resolve,reject){
// 異步代碼
if(成功){
resolve();
}else{
reject();
}
})
promise對象有以下兩個特點废士。
(1)對象的狀態(tài)不受外界影響叫潦。promise對象代表一個異步操作,有三種狀態(tài) pending(進行中) fulfilled(已成功) rejected(已失敼傧酢)矗蕊。只有異步操作的結(jié)果,可以決定當(dāng)前是哪一種狀態(tài)氢架,任何其他操作都無法改變這個狀態(tài)傻咖。這也是parmise的這個名字的由來,它的英語意思就是“承諾”岖研,表示其他手段無法改變卿操。
(2)一旦狀態(tài)改變,就不會再變,任何時候都可以得到這個結(jié)果 promise對象的狀態(tài)改變硬纤,只有兩種可能:從pending變?yōu)閒ulfilled和從pending變?yōu)閞ejected解滓。只要這兩種情況發(fā)生,狀態(tài)就凝固了筝家,不會再變了,會一直保持這個結(jié)果邻辉,這時就稱為 resolved(已定型)陈瘦。如果改變已經(jīng)發(fā)生了敢靡,你再對promise對象添加回調(diào)函數(shù),也會立即得到這個結(jié)果。這與事件(Event)完全不同掏湾,事件的特點是,如果你錯過了它宛官,再去監(jiān)聽媒咳,是得不到結(jié)果的。
Promise構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù)使碾,該函數(shù)的兩個參數(shù)分別是resolve和
reject蜜徽。它們是兩個函數(shù),由 JavaScript 引擎提供票摇,不用自己部署拘鞋。
resolve函數(shù)的作用是,將Promise對象的狀態(tài)從“未完成”變?yōu)椤俺晒Α?即從 pending 變?yōu)?resolved)矢门,在異步操作成功時調(diào)用盆色,并將異步操作的結(jié)果,作為參數(shù)傳遞出去;reject函數(shù)的作是祟剔,將Promise對象的狀態(tài)從“未完成”變?yōu)椤笆?敗”(即從 pending變?yōu)閞ejected)隔躲,在異步操作失敗時調(diào)用,并將異步操作報 出的錯誤物延,作為參數(shù)傳遞出去宣旱。 Promise實例生成以后,可以用 then方法分別指定resolved狀態(tài)和rejected狀態(tài) 的回調(diào)函數(shù)教届。
function Fun(){
return new Promise((resolve,reject) => {
setTimeout(resolve,3000);
})
}
Fun().then(function(value){
console.log(123);
});
then 法可以接受兩個回調(diào)函數(shù)作為參數(shù)响鹃。第1個回調(diào)函數(shù)是Promise對象的狀 態(tài)變?yōu)閞esolved時調(diào)用,第2個回調(diào)函數(shù)是Promise對象的狀態(tài)變?yōu)閞ejected時調(diào)用 案训。其中买置,第2個函數(shù)是可選的,不一定要提供强霎。這兩個函數(shù)都接受Promise對象傳出的值作為參數(shù)忿项。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done'); });
}
timeout(100).then(
(value) => {console.log(value); }
);
let promise = new Promise(function(resolve,reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
setTimeout(function(){
console.log('setTimeout');
},0)
function timeout() {
return new Promise((resolve, reject) => {
console.log('Promise1')
resolve();
});
}
timeout().then(
() => { console.log('Promise2');
});
//Promise1
//Promise2
//setTimeout
代碼中,Promise 新建后立即執(zhí)行,所以先輸出的是Promise轩触。然后寞酿,then方 法指定的回調(diào)函數(shù),將在當(dāng)前腳本所有同步任務(wù)執(zhí)行完才會執(zhí)行脱柱,所以resolved最后輸出伐弹。
Promise對象實現(xiàn)的Ajax操作的例子
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
}else{
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出錯 ', error);
});
2 。Promise.prototype.then()
Promise 實例具有then方法榨为,也就是說惨好,then方法是定義在原型對象Promise.prototype上的。它的作用是為 Promise 實例添加狀態(tài)改變時的回調(diào)函數(shù)随闺。前面說過日川,then方法的第一個參數(shù)是resolved狀態(tài)的回調(diào)函數(shù),第二個參數(shù)(可選)是rejected狀態(tài)的回調(diào)函數(shù)矩乐。
then方法返回的是一個新的Promise實例(注意龄句,不是原來那個Promise實例)。因此可以采用鏈式寫法散罕,即then方法后面再調(diào)用另一個then方法分歇。
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
上面的代碼使用then方法,依次指定了兩個回調(diào)函數(shù)笨使。第一個回調(diào)函數(shù)完成以后卿樱,會將返回結(jié)果作為參數(shù),傳入第二個回調(diào)函數(shù)硫椰。
采用鏈式的then繁调,可以指定一組按照次序調(diào)用的回調(diào)函數(shù)。這時靶草,前一個回調(diào)函數(shù)蹄胰,有可能返回的還是一個Promise對象(即有異步操作),這時后一個回調(diào)函數(shù)奕翔,就會等待該Promise對象的狀態(tài)發(fā)生變化裕寨,才會被調(diào)用。
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function funcA(comments) {
console.log("resolved: ", comments);
}, function funcB(err){
console.log("rejected: ", err);
});
面代碼中派继,第一個then方法指定的回調(diào)函數(shù)宾袜,返回的是另一個Promise對象。這時驾窟,第二個then方法指定的回調(diào)函數(shù)庆猫,就會等待這個新的Promise對象狀態(tài)發(fā)生變化。如果變?yōu)閞esolved绅络,就調(diào)用funcA月培,如果狀態(tài)變?yōu)閞ejected嘁字,就調(diào)用funcB。
如果采用箭頭函數(shù)杉畜,上面的代碼可以寫得更簡潔纪蜒。
getJSON("/post/1.json").then(
post => getJSON(post.commentURL)
).then(
comments => console.log("resolved: ", comments),
err => console.log("rejected: ", err)
Promise.prototype.catch()
Promise.prototype.catch方法是.then(null,rejection)的別名,用于指定發(fā)生錯誤時的回調(diào)函數(shù)此叠。
getJSON('/posts.json').then(function(posts) {
}).catch(function(error) {
// 處理 getJSON 和 前一個回調(diào)函數(shù)運行時發(fā)生的錯誤
console.log('發(fā)生錯誤纯续!', error);
});
上面代碼中,getJSON方法返回一個Promise對象拌蜘,如果該對象狀態(tài)變?yōu)閞esolved杆烁,則會調(diào)用then方法指定的回調(diào)函數(shù);如果異步操作拋出錯誤简卧,狀態(tài)就會變?yōu)閞ejected,就會調(diào)用catch方法指定的回調(diào)函數(shù)烤芦,處理這個錯誤举娩。另外,then方法指定的回調(diào)函數(shù)构罗,如果運行中拋出錯誤铜涉,也會被catch方法捕獲。
下面是一個例子遂唧。
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
// Error: test
上面代碼中芙代,promise拋出一個錯誤,就被catch方法指定的回調(diào)函數(shù)捕獲盖彭。注意纹烹,上面的寫法與下面兩種寫法是等價的。
// 寫法一
const promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
promise.catch(function(error) {
console.log(error);
});
// 寫法二
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});
比較上面兩種寫法召边,可以發(fā)現(xiàn)reject方法的作用铺呵,等同于拋出錯誤。
如果 Promise 狀態(tài)已經(jīng)變成resolved隧熙,再拋出錯誤是無效的片挂。
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
上面代碼中,Promise在resolve語句后面贞盯,再拋出錯誤音念,不會被捕獲,等于沒有拋出躏敢。因為 Promise 的狀態(tài)一旦改變闷愤,就永久保持該狀態(tài),不會再變了父丰。
一般來說肝谭,不要在then方法里面定義 Reject 狀態(tài)的回調(diào)函數(shù)(即then的第二個參數(shù))掘宪,總是使用catch方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
上面代碼中攘烛,第二種寫法要好于第一種寫法魏滚,理由是第二種寫法可以捕獲前面then方法執(zhí)行中的錯誤,也更接近同步的寫法(try/catch)坟漱。因此鼠次,建議總是使用catch方法,而不使用then方法的第二個參數(shù)芋齿。
Promise.all()
Promise.all方法用于將多個 Promise 實例腥寇,包裝成一個新的 Promise 實例
const p = Promise.all([p1, p2, p3]);
1
上面代碼中,Promise.all方法接受一個數(shù)組作為參數(shù)觅捆,p1赦役、p2、p3都是 Promise 實例栅炒,如果不是掂摔,就會先調(diào)用Promise.resolve方法,將參數(shù)轉(zhuǎn)為 Promise 實例赢赊,再進一步處理乙漓。(Promise.all方法的參數(shù)可以不是數(shù)組,但必須具有 Iterator 接口释移,且返回的每個成員都是 Promise 實例叭披。)
p的狀態(tài)由p1、p2玩讳、p3決定涩蜘,分成兩種情況。
(1)只有p1锋边、p2皱坛、p3的狀態(tài)都變成fulfilled,p的狀態(tài)才會變成fulfilled豆巨,此時p1剩辟、p2、p3的返回值組成一個數(shù)組往扔,傳遞給p的回調(diào)函數(shù)贩猎。
(2)只要p1、p2萍膛、p3之中有一個被rejected吭服,p的狀態(tài)就變成rejected,此時第一個被reject的實例的返回值蝗罗,會傳遞給p的回調(diào)函數(shù)艇棕。
下面是一個具體的例子蝌戒。
// 生成一個Promise對象的數(shù)組
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
上面代碼中,promises是包含 6 個 Promise實例的數(shù)組沼琉,只有這6個實例的狀態(tài)都變成fulfilled北苟,或者其中有一個變?yōu)閞ejected,才會調(diào)用Promise.all方法后面的回調(diào)函數(shù)打瘪。
注意友鼻,如果作為參數(shù)的 Promise 實例,自己定義了catch方法闺骚,那么它一旦被rejected彩扔,并不會觸發(fā)Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 報錯了]
上面代碼中僻爽,p1會resolved虫碉,p2首先會rejected,但是p2有自己的catch方法胸梆,該方法返回的是一個新的 Promise 實例蔗衡,p2指向的實際上是這個實例。該實例執(zhí)行完catch方法后乳绕,也會變成resolved,導(dǎo)致Promise.all()方法參數(shù)里面的兩個實例都會resolved逼纸,因此會調(diào)用then方法指定的回調(diào)函數(shù)洋措,而不會調(diào)用catch方法指定的回調(diào)函數(shù)。
如果p2沒有自己的catch方法杰刽,就會調(diào)用Promise.all()的catch方法菠发。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了');
})
.then(result => result);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 報錯了
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了');
}).then(result => result).catch(e => {
console.log(123);
console.log(e);
});
console.log(p2);
Promise {<pending>}__proto__: Promise[[PromiseStatus]]: "resolved"[[PromiseValue]]: undefined
Iterator(遍歷器)的概念
JavaScript 原有的表示“集合”的數(shù)據(jù)結(jié)構(gòu),主要是數(shù)組(Array)和對象(Object)贺嫂,ES6 又添加了Map和Set滓鸠。這樣就有了四種數(shù)據(jù)集合,用戶還可以組合使用它們第喳,定義自己的數(shù)據(jù)結(jié)構(gòu)糜俗,比如數(shù)組的成員是Map,Map的成員是對象曲饱。這樣就需要一種統(tǒng)一的接口機制悠抹,來處理所有不同的數(shù)據(jù)結(jié)構(gòu)。
遍歷器(Iterator)就是這樣一種機制扩淀。它是一種接口楔敌,為各種不同的數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪問機制。任何數(shù)據(jù)結(jié)構(gòu)只要部署 Iterator 接口驻谆,就可以完成遍歷操作(即依次處理該數(shù)據(jù)結(jié)構(gòu)的所有成員)卵凑。
Iterator 的作用有三個:一是為各種數(shù)據(jù)結(jié)構(gòu)庆聘,提供一個統(tǒng)一的、簡便的訪問接口勺卢;二是使得數(shù)據(jù)結(jié)構(gòu)的成員能夠按某種次序排列伙判;三是 ES6 創(chuàng)造了一種新的遍歷命令for…of循環(huán),Iterator 接口主要供for…of消費
Iterator 的遍歷過程是這樣的值漫。
(1)創(chuàng)建一個指針對象澳腹,指向當(dāng)前數(shù)據(jù)結(jié)構(gòu)的起始位置。也就是說杨何,遍歷器對象本質(zhì)上酱塔,就是一個指針對象。
(2)第一次調(diào)用指針對象的next方法危虱,可以將指針指向數(shù)據(jù)結(jié)構(gòu)的第一個成員羊娃。
(3)第二次調(diào)用指針對象的next方法,指針就指向數(shù)據(jù)結(jié)構(gòu)的第二個成員埃跷。
(4)不斷調(diào)用指針對象的next方法蕊玷,直到它指向數(shù)據(jù)結(jié)構(gòu)的結(jié)束位置。
每一次調(diào)用next方法弥雹,都會返回數(shù)據(jù)結(jié)構(gòu)的當(dāng)前成員的信息垃帅。具體來說,就是返回一個包含value和done兩個屬性的對象剪勿。其中贸诚,value屬性是當(dāng)前成員的值,done屬性是一個布爾值厕吉,表示遍歷是否結(jié)束酱固。
下面是一個模擬next方法返回值的例子。
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
上面代碼定義了一個makeIterator函數(shù)头朱,它是一個遍歷器生成函數(shù)运悲,作用就是返回一個遍歷器對象。對數(shù)組[‘a(chǎn)’, ‘b’]執(zhí)行這個函數(shù)项钮,就會返回該數(shù)組的遍歷器對象(即指針對象)it班眯。
指針對象的next方法,用來移動指針寄纵。開始時鳖敷,指針指向數(shù)組的開始位置。然后程拭,每次調(diào)用next方法定踱,指針就會指向數(shù)組的下一個成員。第一次調(diào)用恃鞋,指向a崖媚;第二次調(diào)用亦歉,指向b。
next方法返回一個對象畅哑,表示當(dāng)前數(shù)據(jù)成員的信息肴楷。這個對象具有value和done兩個屬性,value屬性返回當(dāng)前位置的成員荠呐,done屬性是一個布爾值赛蔫,表示遍歷是否結(jié)束,即是否還有必要再一次調(diào)用next方法泥张。
總之呵恢,調(diào)用指針對象的next方法,就可以遍歷事先給定的數(shù)據(jù)結(jié)構(gòu)媚创。
調(diào)用 Iterator 接口的場合
(1)解構(gòu)賦值
對數(shù)組和 Set 結(jié)構(gòu)進行解構(gòu)賦值時渗钉,會默認調(diào)用Symbol.iterator方法。
let set = new Set().add('a').add('b').add('c');
let [x,y] = set;
// x='a'; y='b'
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
(2)擴展運算符
擴展運算符(…)也會調(diào)用默認的 Iterator 接口钞钙。
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
上面代碼的擴展運算符內(nèi)部就調(diào)用 Iterator 接口鳄橘。
實際上,這提供了一種簡便機制芒炼,可以將任何部署了 Iterator 接口的數(shù)據(jù)結(jié)構(gòu)瘫怜,轉(zhuǎn)為數(shù)組。也就是說本刽,只要某個數(shù)據(jù)結(jié)構(gòu)部署了 Iterator 接口宝磨,就可以對它使用擴展運算符,將其轉(zhuǎn)為數(shù)組盅安。
yield*
yield*后面跟的是一個可遍歷的結(jié)構(gòu),它會調(diào)用該結(jié)構(gòu)的遍歷器接口世囊。
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
Generator 函數(shù)的語法
基本概念
Generator 函數(shù)是 ES6 提供的一種異步編程解決方案别瞭,語法行為與傳統(tǒng)函數(shù)完全不同。
Generator 函數(shù)有多種理解角度株憾。語法上蝙寨,首先可以把它理解成,Generator 函數(shù)是一個狀態(tài)機嗤瞎,封裝了多個內(nèi)部狀態(tài)墙歪。
執(zhí)行 Generator 函數(shù)會返回一個遍歷器對象,也就是說贝奇,Generator 函數(shù)除了狀態(tài)機虹菲,還是一個遍歷器對象生成函數(shù)。返回的遍歷器對象掉瞳,可以依次遍歷 Generator 函數(shù)內(nèi)部的每一個狀態(tài)毕源。
形式上浪漠,Generator 函數(shù)是一個普通函數(shù),但是有兩個特征霎褐。
一是址愿,function關(guān)鍵字與函數(shù)名之間有一個星號;
二是冻璃,函數(shù)體內(nèi)部使用yield表達式响谓,定義不同的內(nèi)部狀態(tài)(yield在英語里的意思就是“產(chǎn)出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代碼定義了一個 Generator 函數(shù)helloWorldGenerator省艳,它內(nèi)部有兩個yield表達式(hello和world)娘纷,即該函數(shù)有三個狀態(tài):hello,world 和 return 語句(結(jié)束執(zhí)行)拍埠。
然后失驶,Generator 函數(shù)的調(diào)用方法與普通函數(shù)一樣,也是在函數(shù)名后面加上一對圓括號枣购。不同的是嬉探,調(diào)用 Generator 函數(shù)后,該函數(shù)并不執(zhí)行棉圈,返回的也不是函數(shù)運行結(jié)果涩堤,而是一個指向內(nèi)部狀態(tài)的指針對象,也就是上一章介紹的遍歷器對象
下一步分瘾,必須調(diào)用遍歷器對象的next方法胎围,使得指針移向下一個狀態(tài)。也就是說德召,每次調(diào)用next方法白魂,內(nèi)部指針就從函數(shù)頭部或上一次停下來的地方開始執(zhí)行,直到遇到下一個yield表達式(或return語句)為止上岗。換言之福荸,Generator 函數(shù)是分段執(zhí)行的,yield表達式是暫停執(zhí)行的標記肴掷,而next方法可以恢復(fù)執(zhí)行敬锐。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
第一次調(diào)用,Generator 函數(shù)開始執(zhí)行呆瞻,直到遇到第一個yield表達式為止台夺。next方法返回一個對象,它的value屬性就是當(dāng)前yield表達式的值hello痴脾,done屬性的值false颤介,表示遍歷還沒有結(jié)束。
第二次調(diào)用,Generator 函數(shù)從上次yield表達式停下的地方买窟,一直執(zhí)行到下一個yield表達式丰泊。next方法返回的對象的value屬性就是當(dāng)前yield表達式的值world,done屬性的值false始绍,表示遍歷還沒有結(jié)束瞳购。
第三次調(diào)用,Generator 函數(shù)從上次yield表達式停下的地方亏推,一直執(zhí)行到return語句(如果沒有return語句学赛,就執(zhí)行到函數(shù)結(jié)束)。next方法返回的對象的value屬性吞杭,就是緊跟在return語句后面的表達式的值(如果沒有return語句循狰,則value屬性的值為undefined)植捎,done屬性的值true桐款,表示遍歷已經(jīng)結(jié)束庶橱。
第四次調(diào)用,此時 Generator 函數(shù)已經(jīng)運行完畢童擎,next方法返回對象的value屬性為undefined滴劲,done屬性為true。以后再調(diào)用next方法顾复,返回的都是這個值班挖。
總結(jié)一下,調(diào)用 Generator 函數(shù)芯砸,返回一個遍歷器對象萧芙,代表 Generator 函數(shù)的內(nèi)部指針。以后假丧,每次調(diào)用遍歷器對象的next方法双揪,就會返回一個有著value和done兩個屬性的對象。value屬性表示當(dāng)前的內(nèi)部狀態(tài)的值包帚,是yield表達式后面那個表達式的值盟榴;done屬性是一個布爾值,表示是否遍歷結(jié)束婴噩。
第一次調(diào)用,Generator 函數(shù)開始執(zhí)行羽德,直到遇到第一個yield表達式為止几莽。next方法返回一個對象,它的value屬性就是當(dāng)前yield表達式的值hello宅静,done屬性的值false章蚣,表示遍歷還沒有結(jié)束。
第二次調(diào)用,Generator 函數(shù)從上次yield表達式停下的地方纤垂,一直執(zhí)行到下一個yield表達式矾策。next方法返回的對象的value屬性就是當(dāng)前yield表達式的值world,done屬性的值false峭沦,表示遍歷還沒有結(jié)束贾虽。
第三次調(diào)用,Generator 函數(shù)從上次yield表達式停下的地方吼鱼,一直執(zhí)行到return語句(如果沒有return語句蓬豁,就執(zhí)行到函數(shù)結(jié)束)。next方法返回的對象的value屬性菇肃,就是緊跟在return語句后面的表達式的值(如果沒有return語句地粪,則value屬性的值為undefined),done屬性的值true琐谤,表示遍歷已經(jīng)結(jié)束蟆技。
第四次調(diào)用,此時 Generator 函數(shù)已經(jīng)運行完畢斗忌,next方法返回對象的value屬性為undefined质礼,done屬性為true。以后再調(diào)用next方法飞蹂,返回的都是這個值几苍。
總結(jié)一下,調(diào)用 Generator 函數(shù)陈哑,返回一個遍歷器對象妻坝,代表 Generator 函數(shù)的內(nèi)部指針。以后惊窖,每次調(diào)用遍歷器對象的next方法刽宪,就會返回一個有著value和done兩個屬性的對象。value屬性表示當(dāng)前的內(nèi)部狀態(tài)的值界酒,是yield表達式后面那個表達式的值圣拄;done屬性是一個布爾值,表示是否遍歷結(jié)束毁欣。
yield 表達式
由于 Generator 函數(shù)返回的遍歷器對象庇谆,只有調(diào)用next方法才會遍歷下一個內(nèi)部狀態(tài),所以其實提供了一種可以暫停執(zhí)行的函數(shù)凭疮。yield表達式就是暫停標志饭耳。
遍歷器對象的next方法的運行邏輯如下。
(1)遇到y(tǒng)ield表達式执解,就暫停執(zhí)行后面的操作寞肖,并將緊跟在yield后面的那個表達式的值,作為返回的對象的value屬性值。
(2)下一次調(diào)用next方法時新蟆,再繼續(xù)往下執(zhí)行觅赊,直到遇到下一個yield表達式。
(3)如果沒有再遇到新的yield表達式琼稻,就一直運行到函數(shù)結(jié)束吮螺,直到return語句為止,并將return語句后面的表達式的值欣簇,作為返回的對象的value屬性值规脸。
(4)如果該函數(shù)沒有return語句,則返回的對象的value屬性值為undefined熊咽。
yield表達式與return語句既有相似之處莫鸭,也有區(qū)別。相似之處在于横殴,都能返回緊跟在語句后面的那個表達式的值被因。區(qū)別在于每次遇到y(tǒng)ield,函數(shù)暫停執(zhí)行衫仑,下一次再從該位置繼續(xù)向后執(zhí)行梨与,而return語句不具備位置記憶的功能。一個函數(shù)里面文狱,只能執(zhí)行一次(或者說一個)return語句粥鞋,但是可以執(zhí)行多次(或者說多個)yield表達式。正常函數(shù)只能返回一個值瞄崇,因為只能執(zhí)行一次return呻粹;Generator 函數(shù)可以返回一系列的值,因為可以有任意多個yield苏研。從另一個角度看等浊,也可以說 Generator 生成了一系列的值,這也就是它的名稱的來歷(英語中摹蘑,generator 這個詞是“生成器”的意思)筹燕。
Generator 函數(shù)可以不用yield表達式,這時就變成了一個單純的暫緩執(zhí)行函數(shù)衅鹿。
function* f() {
console.log('執(zhí)行了撒踪!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面代碼中,函數(shù)f如果是普通函數(shù)大渤,在為變量generator賦值時就會執(zhí)行制妄。但是,函數(shù)f是一個 Generator 函數(shù)兼犯,就變成只有調(diào)用next方法時,函數(shù)f才會執(zhí)行。
另外需要注意切黔,yield表達式只能用在 Generator 函數(shù)里面砸脊,用在其他地方都會報錯。
(function (){
yield 1;
})()
// SyntaxError: Unexpected number
上面代碼在一個普通函數(shù)中使用yield表達式纬霞,結(jié)果產(chǎn)生一個句法錯誤凌埂。
另外,yield表達式如果用在另一個表達式之中诗芜,必須放在圓括號里面瞳抓。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
yield表達式用作函數(shù)參數(shù)或放在賦值表達式的右邊,可以不加括號伏恐。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
next 方法的參數(shù)
yield表達式本身沒有返回值孩哑,或者說總是返回undefined。next方法可以帶一個參數(shù)翠桦,該參數(shù)就會被當(dāng)作上一個yield表達式的返回值横蜒。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
上面代碼先定義了一個可以無限運行的 Generator 函數(shù)f,如果next方法沒有參數(shù)销凑,每次運行到y(tǒng)ield表達式丛晌,變量reset的值總是undefined。當(dāng)next方法帶一個參數(shù)true時斗幼,變量reset就被重置為這個參數(shù)(即true)澎蛛,因此i會等于-1,下一輪循環(huán)就會從-1開始遞增蜕窿。
這個功能有很重要的語法意義谋逻。Generator 函數(shù)從暫停狀態(tài)到恢復(fù)運行,它的上下文狀態(tài)(context)是不變的渠羞。通過next方法的參數(shù)斤贰,就有辦法在 Generator 函數(shù)開始運行之后,繼續(xù)向函數(shù)體內(nèi)部注入值次询。也就是說荧恍,可以在 Generator 函數(shù)運行的不同階段,從外部向內(nèi)部注入不同的值屯吊,從而調(diào)整函數(shù)行為送巡。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
上面代碼中,第二次運行next方法的時候不帶參數(shù)盒卸,導(dǎo)致 y 的值等于2 * undefined(即NaN)骗爆,除以 3 以后還是NaN,因此返回對象的value屬性也等于NaN蔽介。第三次運行Next方法的時候不帶參數(shù)摘投,所以z等于undefined煮寡,返回對象的value屬性等于5 + NaN + undefined,即NaN犀呼。
如果向next方法提供參數(shù)幸撕,返回結(jié)果就完全不一樣了。上面代碼第一次調(diào)用b的next方法時外臂,返回x+1的值6坐儿;第二次調(diào)用next方法,將上一次yield表達式的值設(shè)為12宋光,因此y等于24貌矿,返回y / 3的值8;第三次調(diào)用next方法罪佳,將上一次yield表達式的值設(shè)為13逛漫,因此z等于13,這時x等于5菇民,y等于24尽楔,所以return語句的值等于42。
注意第练,由于next方法的參數(shù)表示上一個yield表達式的返回值阔馋,所以在第一次使用next方法時,傳遞參數(shù)是無效的娇掏。V8 引擎直接忽略第一次使用next方法時的參數(shù)呕寝,只有從第二次使用next方法開始,參數(shù)才是有效的婴梧。從語義上講下梢,第一個next方法用來啟動遍歷器對象,所以不用帶有參數(shù)塞蹭。
3 應(yīng)用
Generator 可以暫停函數(shù)執(zhí)行孽江,返回任意表達式的值。這種特點使得 Generator 有多種應(yīng)用場景番电。
異步操作的同步化表達
Generator 函數(shù)的暫停執(zhí)行的效果岗屏,意味著可以把異步操作寫在yield表達式里面,等到調(diào)用next方法時再往后執(zhí)行漱办。這實際上等同于不需要寫回調(diào)函數(shù)了这刷,因為異步操作的后續(xù)操作可以放在yield表達式下面,反正要等到調(diào)用next方法時再執(zhí)行娩井。所以暇屋,Generator 函數(shù)的一個重要實際意義就是用來處理異步操作,改寫回調(diào)函數(shù)
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加載UI
loader.next()
// 卸載UI
loader.next()
上面代碼中洞辣,第一次調(diào)用loadUI函數(shù)時咐刨,該函數(shù)不會執(zhí)行昙衅,僅返回一個遍歷器。下一次對該遍歷器調(diào)用next方法定鸟,則會顯示Loading界面(showLoadingScreen)绒尊,并且異步加載數(shù)據(jù)(loadUIDataAsynchronously)。等到數(shù)據(jù)加載完成仔粥,再一次使用next方法,則會隱藏Loading界面蟹但∏可以看到,這種寫法的好處是所有Loading界面的邏輯华糖,都被封裝在一個函數(shù)麦向,按部就班非常清晰。
Ajax 是典型的異步操作客叉,通過 Generator 函數(shù)部署 Ajax 操作诵竭,可以用同步的方式表達。
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
var it = main();
it.next();
面代碼的main函數(shù)兼搏,就是通過 Ajax 操作獲取數(shù)據(jù)卵慰。可以看到佛呻,除了多了一個yield裳朋,它幾乎與同步操作的寫法完全一樣。注意吓著,makeAjaxCall函數(shù)中的next方法鲤嫡,必須加上response參數(shù),因為yield表達式绑莺,本身是沒有值的暖眼,總是等于undefined。
Generator 函數(shù)的異步應(yīng)用
Generator 函數(shù)將 JavaScript 異步編程帶入了一個全新的階段
傳統(tǒng)的回調(diào)函數(shù)
回調(diào)函數(shù)本身并沒有問題纺裁,它的問題出現(xiàn)在多個回調(diào)函數(shù)嵌套诫肠。假定讀取A文件之后,再讀取B文件对扶,代碼如下区赵。
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
不難想象,如果依次讀取兩個以上的文件浪南,就會出現(xiàn)多重嵌套笼才。代碼不是縱向發(fā)展,而是橫向發(fā)展络凿,很快就會亂成一團骡送,無法管理昂羡。因為多個異步操作形成了強耦合,只要有一個操作需要修改摔踱,它的上層回調(diào)函數(shù)和下層回調(diào)函數(shù)虐先,可能都要跟著修改。這種情況就稱為”回調(diào)函數(shù)地獄”(callback hell)派敷。
Promise 對象就是為了解決這個問題而提出的蛹批。它不是新的語法功能,而是一種新的寫法篮愉,允許將回調(diào)函數(shù)的嵌套腐芍,改成鏈式調(diào)用。采用 Promise试躏,連續(xù)讀取多個文件猪勇,寫法如下。
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
可以看到颠蕴,Promise 的寫法只是回調(diào)函數(shù)的改進泣刹,使用then方法以后,異步任務(wù)的兩段執(zhí)行看得更清楚了犀被,除此以外椅您,并無新意。
Promise 的最大問題是代碼冗余寡键,原來的任務(wù)被 Promise 包裝了一下襟沮,不管什么操作,一眼看去都是一堆then昌腰,原來的語義變得很不清楚开伏。
Generator 函數(shù) 出馬了
協(xié)程
傳統(tǒng)的編程語言,早有異步編程的解決方案(其實是多任務(wù)的解決方案)遭商。其中有一種叫做”協(xié)程”(coroutine)固灵,意思是多個線程互相協(xié)作,完成異步任務(wù)劫流。
協(xié)程有點像函數(shù)巫玻,又有點像線程。它的運行流程大致如下祠汇。
第一步仍秤,協(xié)程A開始執(zhí)行。
第二步可很,協(xié)程A執(zhí)行到一半诗力,進入暫停,執(zhí)行權(quán)轉(zhuǎn)移到協(xié)程B我抠。
第三步苇本,(一段時間后)協(xié)程B交還執(zhí)行權(quán)袜茧。
第四步,協(xié)程A恢復(fù)執(zhí)行瓣窄。
上面流程的協(xié)程A笛厦,就是異步任務(wù),因為它分成兩段(或多段)執(zhí)行俺夕。
舉例來說裳凸,讀取文件的協(xié)程寫法如下。
function* asyncJob() {
// ...其他代碼
var f = yield readFile(fileA);
// ...其他代碼
}
上面代碼的函數(shù)asyncJob是一個協(xié)程劝贸,它的奧妙就在其中的yield命令登舞。它表示執(zhí)行到此處,執(zhí)行權(quán)將交給其他協(xié)程悬荣。也就是說,yield命令是異步兩個階段的分界線疙剑。
協(xié)程遇到y(tǒng)ield命令就暫停氯迂,等到執(zhí)行權(quán)返回,再從暫停的地方繼續(xù)往后執(zhí)行言缤。它的最大優(yōu)點嚼蚀,就是代碼的寫法非常像同步操作,如果去除yield命令管挟,簡直一模一樣轿曙。
協(xié)程的 Generator 函數(shù)實現(xiàn)
Generator 函數(shù)是協(xié)程在 ES6 的實現(xiàn),最大特點就是可以交出函數(shù)的執(zhí)行權(quán)(即暫停執(zhí)行)僻孝。
整個 Generator 函數(shù)就是一個封裝的異步任務(wù)导帝,或者說是異步任務(wù)的容器。異步操作需要暫停的地方穿铆,都用yield語句注明您单。Generator 函數(shù)的執(zhí)行方法如下。
function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
上面代碼中荞雏,調(diào)用 Generator 函數(shù)虐秦,會返回一個內(nèi)部指針(即遍歷器)g。這是 Generator 函數(shù)不同于普通函數(shù)的另一個地方凤优,即執(zhí)行它不會返回結(jié)果悦陋,返回的是指針對象。調(diào)用指針g的next方法筑辨,會移動內(nèi)部指針(即執(zhí)行異步任務(wù)的第一段)俺驶,指向第一個遇到的yield語句,上例是執(zhí)行到x + 2為止棍辕。
換言之痒钝,next方法的作用是分階段執(zhí)行Generator函數(shù)秉颗。每次調(diào)用next方法,會返回一個對象送矩,表示當(dāng)前階段的信息(value屬性和done屬性)蚕甥。value屬性是yield語句后面表達式的值,表示當(dāng)前階段的值栋荸;done屬性是一個布爾值菇怀,表示 Generator 函數(shù)是否執(zhí)行完畢,即是否還有下一個階段晌块。
Generator 函數(shù)的數(shù)據(jù)交換和錯誤處理
Generator 函數(shù)可以暫停執(zhí)行和恢復(fù)執(zhí)行爱沟,這是它能封裝異步任務(wù)的根本原因。除此之外匆背,它還有兩個特性呼伸,使它可以作為異步編程的完整解決方案:函數(shù)體內(nèi)外的數(shù)據(jù)交換和錯誤處理機制。
next返回值的 value 屬性钝尸,是 Generator 函數(shù)向外輸出數(shù)據(jù)括享;next方法還可以接受參數(shù),向 Generator 函數(shù)體內(nèi)輸入數(shù)據(jù)珍促。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
上面代碼中铃辖,第一個next方法的value屬性,返回表達式x + 2的值3猪叙。第二個next方法帶有參數(shù)2娇斩,這個參數(shù)可以傳入 Generator 函數(shù),作為上個階段異步任務(wù)的返回結(jié)果穴翩,被函數(shù)體內(nèi)的變量y接收犬第。因此,這一步的value屬性芒帕,返回的就是2(變量y的值)瓶殃。
Generator 函數(shù)內(nèi)部還可以部署錯誤處理代碼,捕獲函數(shù)體外拋出的錯誤副签。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出錯了');
// 出錯了
上面代碼的最后一行遥椿,Generator 函數(shù)體外,使用指針對象的throw方法拋出的錯誤淆储,可以被函數(shù)體內(nèi)的try…catch代碼塊捕獲冠场。這意味著,出錯的代碼與處理錯誤的代碼本砰,實現(xiàn)了時間和空間上的分離碴裙,這對于異步編程無疑是很重要的。
異步任務(wù)的封裝
下面看看如何使用 Generator 函數(shù),執(zhí)行一個真實的異步任務(wù)舔株。
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
上面代碼中莺琳,Generator 函數(shù)封裝了一個異步操作,該操作先讀取一個遠程接口载慈,然后從 JSON 格式的數(shù)據(jù)解析信息惭等。就像前面說過的,這段代碼非常像同步操作办铡,除了加上了yield命令辞做。
執(zhí)行這段代碼的方法如下。
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
上面代碼中寡具,首先執(zhí)行 Generator 函數(shù)秤茅,獲取遍歷器對象,然后使用next方法(第二行)童叠,執(zhí)行異步任務(wù)的第一階段烫沙。由于Fetch模塊返回的是一個 Promise 對象夭谤,因此要用then方法調(diào)用下一個next方法君躺。
可以看到颁湖,雖然 Generator 函數(shù)將異步操作表示得很簡潔灵汪,但是流程管理卻不方便(即何時執(zhí)行第一階段蹬铺、何時執(zhí)行第二階段)察纯。
4 co 模塊
co 模塊是著名程序員 TJ Holowaychuk 于 2013 年 6 月發(fā)布的一個小工具闸拿,用于 Generator 函數(shù)的自動執(zhí)行污桦。
下面是一個 Generator 函數(shù)亩歹,用于依次讀取兩個文件。
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
co 模塊可以讓你不用編寫 Generator 函數(shù)的執(zhí)行器凡橱。
var co = require('co');
co(gen);
上面代碼中小作,Generator 函數(shù)只要傳入co函數(shù),就會自動執(zhí)行稼钩。
co函數(shù)返回一個Promise對象顾稀,因此可以用then方法添加回調(diào)函數(shù)。
co(gen).then(function (){
console.log('Generator 函數(shù)執(zhí)行完成');
});
上面代碼中坝撑,等到 Generator 函數(shù)執(zhí)行結(jié)束静秆,就會輸出一行提示
為什么 co 可以自動執(zhí)行 Generator 函數(shù)?
前面說過巡李,Generator 就是一個異步操作的容器抚笔。它的自動執(zhí)行需要一種機制,當(dāng)異步操作有了結(jié)果侨拦,能夠自動交回執(zhí)行權(quán)殊橙。
兩種方法可以做到這一點。
(1)回調(diào)函數(shù)。將異步操作包裝成 Thunk 函數(shù)膨蛮,在回調(diào)函數(shù)里面交回執(zhí)行權(quán)叠纹。
(2)Promise 對象。將異步操作包裝成 Promise 對象敞葛,用then方法交回執(zhí)行權(quán)
基于 Promise 對象的自動執(zhí)行
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) return reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
然后誉察,手動執(zhí)行上面的 Generator 函數(shù)。
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
});
手動執(zhí)行其實就是用then方法制肮,層層添加回調(diào)函數(shù)冒窍。理解了這一點,就可以寫出一個自動執(zhí)行器豺鼻。
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
run(gen);
上面代碼中综液,只要 Generator 函數(shù)還沒執(zhí)行到最后一步,next函數(shù)就調(diào)用自身儒飒,以此實現(xiàn)自動執(zhí)行
co 模塊的源碼
co 就是上面那個自動執(zhí)行器的擴展谬莹,它的源碼只有幾十行,非常簡單桩了。
首先附帽,co 函數(shù)接受 Generator 函數(shù)作為參數(shù),返回一個 Promise 對象井誉。
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
});
}
在返回的 Promise 對象里面蕉扮,co 先檢查參數(shù)gen是否為 Generator 函數(shù)。如果是颗圣,就執(zhí)行該函數(shù)喳钟,得到一個內(nèi)部指針對象;如果不是就返回在岂,并將 Promise 對象的狀態(tài)改為resolved奔则。
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
});
}
接著,co 將 Generator 函數(shù)的內(nèi)部指針對象的next方法蔽午,包裝成onFulfilled函數(shù)易茬。這主要是為了能夠捕捉拋出的錯誤。
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}
最后及老,就是關(guān)鍵的next函數(shù)抽莱,它會反復(fù)調(diào)用自身。
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(
new TypeError(
'You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "'
+ String(ret.value)
+ '"'
)
);
}
上面代碼中骄恶,next函數(shù)的內(nèi)部代碼岸蜗,一共只有四行命令。
第一行叠蝇,檢查當(dāng)前是否為 Generator 函數(shù)的最后一步璃岳,如果是就返回年缎。
第二行,確保每一步的返回值铃慷,是 Promise 對象单芜。
第三行,使用then方法犁柜,為返回值加上回調(diào)函數(shù)洲鸠,然后通過onFulfilled函數(shù)再次調(diào)用next函數(shù)。
第四行馋缅,在參數(shù)不符合要求的情況下(參數(shù)非 Thunk 函數(shù)和 Promise 對象)扒腕,將 Promise 對象的狀態(tài)改為rejected,從而終止執(zhí)行萤悴。
有了前面的基礎(chǔ) 我們再來看 async
ES2017 標準引入了 async 函數(shù)瘾腰,使得異步操作變得更加方便。
async 函數(shù)是什么覆履?一句話蹋盆,它就是 Generator 函數(shù)的語法糖。
前文有一個 Generator 函數(shù)硝全,依次讀取兩個文件
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());
};
寫成async函數(shù)栖雾,就是下面這樣。
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
一比較就會發(fā)現(xiàn)伟众,async函數(shù)就是將 Generator 函數(shù)的星號(*)替換成async析藕,將yield替換成await,僅此而已!!!
async函數(shù)對 Generator 函數(shù)的改進凳厢,體現(xiàn)在以下四點:
(1)內(nèi)置執(zhí)行器账胧。
Generator 函數(shù)的執(zhí)行必須靠執(zhí)行器,所以才有了co模塊数初,而async函數(shù)自帶執(zhí)行器找爱。也就是說梗顺,async函數(shù)的執(zhí)行泡孩,與普通函數(shù)一模一樣.
(2)更好的語義。
async和await寺谤,比起星號和yield仑鸥,語義更清楚了。async表示函數(shù)里有異步操作变屁,await表示緊跟在后面的表達式需要等待結(jié)果眼俊。
(3)更廣的適用性。
co模塊約定粟关,yield命令后面只能是 Thunk 函數(shù)或 Promise 對象疮胖,而async函數(shù)的await命令后面,可以是 Promise 對象和原始類型的值(數(shù)值、字符串和布爾值澎灸,但這時等同于同步操作)院塞。
(4)返回值是 Promise。
async函數(shù)的返回值是 Promise 對象性昭,這比 Generator 函數(shù)的返回值是 Iterator 對象方便多了拦止。你可以用then方法指定下一步的操作
基本用法
async函數(shù)返回一個 Promise對象,可以使用then方法添加回調(diào)函數(shù)糜颠。當(dāng)函數(shù)執(zhí)行的時候汹族,一旦遇到await就會先返回,等到異步操作完成其兴,再接著執(zhí)行函數(shù)體內(nèi)后面的語句顶瞒。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 3000);
語法
async函數(shù)的語法規(guī)則總體上比較簡單,難點是錯誤處理機制忌警。
返回 Promise 對象
async函數(shù)返回一個 Promise 對象搁拙。
async函數(shù)內(nèi)部return語句返回的值,會成為then方法回調(diào)函數(shù)的參數(shù)
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
上面代碼中法绵,函數(shù)f內(nèi)部return命令返回的值箕速,會被then方法回調(diào)函數(shù)接收到。
async函數(shù)內(nèi)部拋出錯誤朋譬,會導(dǎo)致返回的 Promise 對象變?yōu)閞eject狀態(tài)盐茎。拋出的錯誤對象會被catch方法回調(diào)函數(shù)接收到。
async function f() {
throw new Error('出錯了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出錯了
Promise 對象的狀態(tài)變化
async函數(shù)返回的 Promise 對象徙赢,必須等到內(nèi)部所有await命令后面的 Promise 對象執(zhí)行完字柠,才會發(fā)生狀態(tài)改變,除非遇到return語句或者拋出錯誤狡赐。也就是說窑业,只有async函數(shù)內(nèi)部的異步操作執(zhí)行完,才會執(zhí)行then方法指定的回調(diào)函數(shù)枕屉。
await 命令
正常情況下常柄,await命令后面是一個 Promise 對象。如果不是搀擂,會被轉(zhuǎn)成一個立即resolve的 Promise 對象西潘。
async function f() {
return await 123;
}
f().then(v => console.log(v))
// 123
上面代碼中,await命令的參數(shù)是數(shù)值123哨颂,它被轉(zhuǎn)成 Promise 對象喷市,并立即resolve。
await命令后面的 Promise 對象如果變?yōu)閞eject狀態(tài)威恼,則reject的參數(shù)會被catch方法的回調(diào)函數(shù)接收到品姓。
sync function f() {
await Promise.reject('出錯了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出錯了
注意寝并,上面代碼中,await語句前面沒有return腹备,但是reject方法的參數(shù)依然傳入了catch方法的回調(diào)函數(shù)食茎。這里如果在await前面加上return,效果是一樣的馏谨。
只要一個await語句后面的 Promise 變?yōu)閞eject别渔,那么整個async函數(shù)都會中斷執(zhí)行。
async function f() {
await Promise.reject('出錯了');
await Promise.resolve('hello world'); // 不會執(zhí)行
}
有時惧互,我們希望即使前一個異步操作失敗哎媚,也不要中斷后面的異步操作。這時可以將第一個await放在try…catch結(jié)構(gòu)里面喊儡,這樣不管這個異步操作是否成功拨与,第二個await都會執(zhí)行。
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方法艾猜,處理前面可能出現(xiàn)的錯誤买喧。
async function f() {
await Promise.reject('出錯了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
使用注意點
第一點,前面已經(jīng)說過匆赃,await命令后面的Promise對象淤毛,運行結(jié)果可能是rejected,所以最好把await命令放在try…catch代碼塊中算柳。
第二點低淡,多個await命令后面的異步操作,如果不存在繼發(fā)關(guān)系瞬项,最好讓它們同時觸發(fā)蔗蹋。
// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
第三點,await命令只能用在async函數(shù)之中囱淋,如果用在普通函數(shù)猪杭,就會報錯
async 函數(shù)的實現(xiàn)原理
async 函數(shù)的實現(xiàn)原理,就是將 Generator 函數(shù)和自動執(zhí)行器妥衣,包裝在一個函數(shù)里皂吮。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
所有的async函數(shù)都可以寫成上面的第二種形式,其中的spawn函數(shù)就是自動執(zhí)行器称鳞。
下面給出spawn函數(shù)的實現(xiàn)涮较,基本就是前文自動執(zhí)行器的翻版稠鼻。
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); });
});
}
原文鏈接:https://blog.csdn.net/Merciwen/article/details/80963279