前言
在公司的項目中,我們經常用到async await 這樣的函數,它的作用也很奇特,可以讓異步的函數等待異步執(zhí)行的結果出來再繼續(xù)往下進行。我一直很好奇這是怎么做到的,它內部的機理是怎么樣的,就一個關鍵詞在函數前面加async,在異步操作前面加await就可以做到。他是怎么做到的呢?
再拋出幾個問題
1 出處在哪里
現在我們用的vue項目就會把我們的語法打包編輯成瀏覽器可以識別的語句,那么async,await 是從什么地方出來的呢,他是怎么實現異步變同步的呢?
2 異步錯誤處理
我們的異步操作async await 如果錯了就不會繼續(xù)執(zhí)行,如果我想讓他繼續(xù)執(zhí)行應該怎么做? try cache? 還有呢? 為什么可以呢?內部是怎么執(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í)行應該怎么做呢?
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);
我覺得這么多問題就足夠我們去思考為什么?現在我們就開始試著去理解這些現象,和內層的原理
但是想要了解 這些東西我們需要很多的基礎知識儲備,有了這些知識儲備,其實也是很好理解的?,F在讓我們開始整理我們需要知道的知識點
首先去查 async await ,查到的結果是
ES2017 標準引入了 async 函數,使得異步操作變得更加方便。 (也就是說 async 是 es7 的內容)
async 函數就是 Generator 函數的語法糖。
async 函數就是將 Generator 函數的星號(*)替換成 async,將 yield 替換成 await
那么問題來了 Genertor函數 是什么函數 加() 替換成async 加 的函數是什么函數,yield 又是什么呢???
要理解這些還要對promise 有一個基本的認識吧。
解密
Promise
1 概念
Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區(qū)最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。
所謂promise,簡單說就是一個容器,里面保存著某個未來才會結束的事件(通常是一個異步操作)的結果。從語法上說,Promise 是一個對象,從它可以獲取異步操作的消息。Promise 提供統一的 API,各種異步操作都可以用同樣的方法進行處理
new Promise(function(resolve,reject){
// 異步代碼
if(成功){
resolve();
}else{
reject();
}
})
promise對象有以下兩個特點。
(1)對象的狀態(tài)不受外界影響。promise對象代表一個異步操作,有三種狀態(tài) pending(進行中) fulfilled(已成功) rejected(已失?。V挥挟惒讲僮鞯慕Y果,可以決定當前是哪一種狀態(tài),任何其他操作都無法改變這個狀態(tài)。這也是parmise的這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變。
(2)一旦狀態(tài)改變,就不會再變,任何時候都可以得到這個結果 promise對象的狀態(tài)改變,只有兩種可能:從pending變?yōu)閒ulfilled和從pending變?yōu)閞ejected。只要這兩種情況發(fā)生,狀態(tài)就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型)。如果改變已經發(fā)生了,你再對promise對象添加回調函數,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監(jiān)聽,是得不到結果的。
Promise構造函數接受一個函數作為參數,該函數的兩個參數分別是resolve和
reject。它們是兩個函數,由 JavaScript 引擎提供,不用自己部署。
resolve函數的作用是,將Promise對象的狀態(tài)從“未完成”變?yōu)椤俺晒Α?即從 pending 變?yōu)?resolved),在異步操作成功時調用,并將異步操作的結果,作為參數傳遞出去;reject函數的作是,將Promise對象的狀態(tài)從“未完成”變?yōu)椤笆?敗”(即從 pending變?yōu)閞ejected),在異步操作失敗時調用,并將異步操作報 出的錯誤,作為參數傳遞出去。 Promise實例生成以后,可以用 then方法分別指定resolved狀態(tài)和rejected狀態(tài) 的回調函數。
function Fun(){
return new Promise((resolve,reject) => {
setTimeout(resolve,3000);
})
}
Fun().then(function(value){
console.log(123);
});
then 法可以接受兩個回調函數作為參數。第1個回調函數是Promise對象的狀 態(tài)變?yōu)閞esolved時調用,第2個回調函數是Promise對象的狀態(tài)變?yōu)閞ejected時調用 。其中,第2個函數是可選的,不一定要提供。這兩個函數都接受Promise對象傳出的值作為參數。
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方 法指定的回調函數,將在當前腳本所有同步任務執(zhí)行完才會執(zhí)行,所以resolved最后輸出。
Promise對象實現的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)改變時的回調函數。前面說過,then方法的第一個參數是resolved狀態(tài)的回調函數,第二個參數(可選)是rejected狀態(tài)的回調函數。
then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。因此可以采用鏈式寫法,即then方法后面再調用另一個then方法。
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
上面的代碼使用then方法,依次指定了兩個回調函數。第一個回調函數完成以后,會將返回結果作為參數,傳入第二個回調函數。
采用鏈式的then,可以指定一組按照次序調用的回調函數。這時,前一個回調函數,有可能返回的還是一個Promise對象(即有異步操作),這時后一個回調函數,就會等待該Promise對象的狀態(tài)發(fā)生變化,才會被調用。
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方法指定的回調函數,返回的是另一個Promise對象。這時,第二個then方法指定的回調函數,就會等待這個新的Promise對象狀態(tài)發(fā)生變化。如果變?yōu)閞esolved,就調用funcA,如果狀態(tài)變?yōu)閞ejected,就調用funcB。
如果采用箭頭函數,上面的代碼可以寫得更簡潔。
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ā)生錯誤時的回調函數。
getJSON('/posts.json').then(function(posts) {
}).catch(function(error) {
// 處理 getJSON 和 前一個回調函數運行時發(fā)生的錯誤
console.log('發(fā)生錯誤!', error);
});
上面代碼中,getJSON方法返回一個Promise對象,如果該對象狀態(tài)變?yōu)閞esolved,則會調用then方法指定的回調函數;如果異步操作拋出錯誤,狀態(tài)就會變?yōu)閞ejected,就會調用catch方法指定的回調函數,處理這個錯誤。另外,then方法指定的回調函數,如果運行中拋出錯誤,也會被catch方法捕獲。
下面是一個例子。
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
// Error: test
上面代碼中,promise拋出一個錯誤,就被catch方法指定的回調函數捕獲。注意,上面的寫法與下面兩種寫法是等價的。
// 寫法一
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ā)現reject方法的作用,等同于拋出錯誤。
如果 Promise 狀態(tài)已經變成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)的回調函數(即then的第二個參數),總是使用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方法的第二個參數。
Promise.all()
Promise.all方法用于將多個 Promise 實例,包裝成一個新的 Promise 實例
const p = Promise.all([p1, p2, p3]);
1
上面代碼中,Promise.all方法接受一個數組作為參數,p1、p2、p3都是 Promise 實例,如果不是,就會先調用Promise.resolve方法,將參數轉為 Promise 實例,再進一步處理。(Promise.all方法的參數可以不是數組,但必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例。)
p的狀態(tài)由p1、p2、p3決定,分成兩種情況。
(1)只有p1、p2、p3的狀態(tài)都變成fulfilled,p的狀態(tài)才會變成fulfilled,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p1、p2、p3之中有一個被rejected,p的狀態(tài)就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。
下面是一個具體的例子。
// 生成一個Promise對象的數組
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實例的數組,只有這6個實例的狀態(tài)都變成fulfilled,或者其中有一個變?yōu)閞ejected,才會調用Promise.all方法后面的回調函數。
注意,如果作為參數的 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,導致Promise.all()方法參數里面的兩個實例都會resolved,因此會調用then方法指定的回調函數,而不會調用catch方法指定的回調函數。
如果p2沒有自己的catch方法,就會調用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 原有的表示“集合”的數據結構,主要是數組(Array)和對象(Object),ES6 又添加了Map和Set。這樣就有了四種數據集合,用戶還可以組合使用它們,定義自己的數據結構,比如數組的成員是Map,Map的成員是對象。這樣就需要一種統一的接口機制,來處理所有不同的數據結構。
遍歷器(Iterator)就是這樣一種機制。它是一種接口,為各種不同的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就可以完成遍歷操作(即依次處理該數據結構的所有成員)。
Iterator 的作用有三個:一是為各種數據結構,提供一個統一的、簡便的訪問接口;二是使得數據結構的成員能夠按某種次序排列;三是 ES6 創(chuàng)造了一種新的遍歷命令for…of循環(huán),Iterator 接口主要供for…of消費
Iterator 的遍歷過程是這樣的。
(1)創(chuàng)建一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。
(2)第一次調用指針對象的next方法,可以將指針指向數據結構的第一個成員。
(3)第二次調用指針對象的next方法,指針就指向數據結構的第二個成員。
(4)不斷調用指針對象的next方法,直到它指向數據結構的結束位置。
每一次調用next方法,都會返回數據結構的當前成員的信息。具體來說,就是返回一個包含value和done兩個屬性的對象。其中,value屬性是當前成員的值,done屬性是一個布爾值,表示遍歷是否結束。
下面是一個模擬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函數,它是一個遍歷器生成函數,作用就是返回一個遍歷器對象。對數組[‘a’, ‘b’]執(zhí)行這個函數,就會返回該數組的遍歷器對象(即指針對象)it。
指針對象的next方法,用來移動指針。開始時,指針指向數組的開始位置。然后,每次調用next方法,指針就會指向數組的下一個成員。第一次調用,指向a;第二次調用,指向b。
next方法返回一個對象,表示當前數據成員的信息。這個對象具有value和done兩個屬性,value屬性返回當前位置的成員,done屬性是一個布爾值,表示遍歷是否結束,即是否還有必要再一次調用next方法。
總之,調用指針對象的next方法,就可以遍歷事先給定的數據結構。
調用 Iterator 接口的場合
(1)解構賦值
對數組和 Set 結構進行解構賦值時,會默認調用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)擴展運算符
擴展運算符(…)也會調用默認的 Iterator 接口。
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
上面代碼的擴展運算符內部就調用 Iterator 接口。
實際上,這提供了一種簡便機制,可以將任何部署了 Iterator 接口的數據結構,轉為數組。也就是說,只要某個數據結構部署了 Iterator 接口,就可以對它使用擴展運算符,將其轉為數組。
yield*
yield*后面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。
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 函數的語法
基本概念
Generator 函數是 ES6 提供的一種異步編程解決方案,語法行為與傳統函數完全不同。
Generator 函數有多種理解角度。語法上,首先可以把它理解成,Generator 函數是一個狀態(tài)機,封裝了多個內部狀態(tài)。
執(zhí)行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態(tài)機,還是一個遍歷器對象生成函數。返回的遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態(tài)。
形式上,Generator 函數是一個普通函數,但是有兩個特征。
一是,function關鍵字與函數名之間有一個星號;
二是,函數體內部使用yield表達式,定義不同的內部狀態(tài)(yield在英語里的意思就是“產出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代碼定義了一個 Generator 函數helloWorldGenerator,它內部有兩個yield表達式(hello和world),即該函數有三個狀態(tài):hello,world 和 return 語句(結束執(zhí)行)。
然后,Generator 函數的調用方法與普通函數一樣,也是在函數名后面加上一對圓括號。不同的是,調用 Generator 函數后,該函數并不執(zhí)行,返回的也不是函數運行結果,而是一個指向內部狀態(tài)的指針對象,也就是上一章介紹的遍歷器對象
下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態(tài)。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執(zhí)行,直到遇到下一個yield表達式(或return語句)為止。換言之,Generator 函數是分段執(zhí)行的,yield表達式是暫停執(zhí)行的標記,而next方法可以恢復執(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 }
第一次調用,Generator 函數開始執(zhí)行,直到遇到第一個yield表達式為止。next方法返回一個對象,它的value屬性就是當前yield表達式的值hello,done屬性的值false,表示遍歷還沒有結束。
第二次調用,Generator 函數從上次yield表達式停下的地方,一直執(zhí)行到下一個yield表達式。next方法返回的對象的value屬性就是當前yield表達式的值world,done屬性的值false,表示遍歷還沒有結束。
第三次調用,Generator 函數從上次yield表達式停下的地方,一直執(zhí)行到return語句(如果沒有return語句,就執(zhí)行到函數結束)。next方法返回的對象的value屬性,就是緊跟在return語句后面的表達式的值(如果沒有return語句,則value屬性的值為undefined),done屬性的值true,表示遍歷已經結束。
第四次調用,此時 Generator 函數已經運行完畢,next方法返回對象的value屬性為undefined,done屬性為true。以后再調用next方法,返回的都是這個值。
總結一下,調用 Generator 函數,返回一個遍歷器對象,代表 Generator 函數的內部指針。以后,每次調用遍歷器對象的next方法,就會返回一個有著value和done兩個屬性的對象。value屬性表示當前的內部狀態(tài)的值,是yield表達式后面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。
第一次調用,Generator 函數開始執(zhí)行,直到遇到第一個yield表達式為止。next方法返回一個對象,它的value屬性就是當前yield表達式的值hello,done屬性的值false,表示遍歷還沒有結束。
第二次調用,Generator 函數從上次yield表達式停下的地方,一直執(zhí)行到下一個yield表達式。next方法返回的對象的value屬性就是當前yield表達式的值world,done屬性的值false,表示遍歷還沒有結束。
第三次調用,Generator 函數從上次yield表達式停下的地方,一直執(zhí)行到return語句(如果沒有return語句,就執(zhí)行到函數結束)。next方法返回的對象的value屬性,就是緊跟在return語句后面的表達式的值(如果沒有return語句,則value屬性的值為undefined),done屬性的值true,表示遍歷已經結束。
第四次調用,此時 Generator 函數已經運行完畢,next方法返回對象的value屬性為undefined,done屬性為true。以后再調用next方法,返回的都是這個值。
總結一下,調用 Generator 函數,返回一個遍歷器對象,代表 Generator 函數的內部指針。以后,每次調用遍歷器對象的next方法,就會返回一個有著value和done兩個屬性的對象。value屬性表示當前的內部狀態(tài)的值,是yield表達式后面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。
yield 表達式
由于 Generator 函數返回的遍歷器對象,只有調用next方法才會遍歷下一個內部狀態(tài),所以其實提供了一種可以暫停執(zhí)行的函數。yield表達式就是暫停標志。
遍歷器對象的next方法的運行邏輯如下。
(1)遇到yield表達式,就暫停執(zhí)行后面的操作,并將緊跟在yield后面的那個表達式的值,作為返回的對象的value屬性值。
(2)下一次調用next方法時,再繼續(xù)往下執(zhí)行,直到遇到下一個yield表達式。
(3)如果沒有再遇到新的yield表達式,就一直運行到函數結束,直到return語句為止,并將return語句后面的表達式的值,作為返回的對象的value屬性值。
(4)如果該函數沒有return語句,則返回的對象的value屬性值為undefined。
yield表達式與return語句既有相似之處,也有區(qū)別。相似之處在于,都能返回緊跟在語句后面的那個表達式的值。區(qū)別在于每次遇到yield,函數暫停執(zhí)行,下一次再從該位置繼續(xù)向后執(zhí)行,而return語句不具備位置記憶的功能。一個函數里面,只能執(zhí)行一次(或者說一個)return語句,但是可以執(zhí)行多次(或者說多個)yield表達式。正常函數只能返回一個值,因為只能執(zhí)行一次return;Generator 函數可以返回一系列的值,因為可以有任意多個yield。從另一個角度看,也可以說 Generator 生成了一系列的值,這也就是它的名稱的來歷(英語中,generator 這個詞是“生成器”的意思)。
Generator 函數可以不用yield表達式,這時就變成了一個單純的暫緩執(zhí)行函數。
function* f() {
console.log('執(zhí)行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面代碼中,函數f如果是普通函數,在為變量generator賦值時就會執(zhí)行。但是,函數f是一個 Generator 函數,就變成只有調用next方法時,函數f才會執(zhí)行。
另外需要注意,yield表達式只能用在 Generator 函數里面,用在其他地方都會報錯。
(function (){
yield 1;
})()
// SyntaxError: Unexpected number
上面代碼在一個普通函數中使用yield表達式,結果產生一個句法錯誤。
另外,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表達式用作函數參數或放在賦值表達式的右邊,可以不加括號。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
next 方法的參數
yield表達式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個參數,該參數就會被當作上一個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 函數f,如果next方法沒有參數,每次運行到yield表達式,變量reset的值總是undefined。當next方法帶一個參數true時,變量reset就被重置為這個參數(即true),因此i會等于-1,下一輪循環(huán)就會從-1開始遞增。
這個功能有很重要的語法意義。Generator 函數從暫停狀態(tài)到恢復運行,它的上下文狀態(tài)(context)是不變的。通過next方法的參數,就有辦法在 Generator 函數開始運行之后,繼續(xù)向函數體內部注入值。也就是說,可以在 Generator 函數運行的不同階段,從外部向內部注入不同的值,從而調整函數行為。
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方法的時候不帶參數,導致 y 的值等于2 * undefined(即NaN),除以 3 以后還是NaN,因此返回對象的value屬性也等于NaN。第三次運行Next方法的時候不帶參數,所以z等于undefined,返回對象的value屬性等于5 + NaN + undefined,即NaN。
如果向next方法提供參數,返回結果就完全不一樣了。上面代碼第一次調用b的next方法時,返回x+1的值6;第二次調用next方法,將上一次yield表達式的值設為12,因此y等于24,返回y / 3的值8;第三次調用next方法,將上一次yield表達式的值設為13,因此z等于13,這時x等于5,y等于24,所以return語句的值等于42。
注意,由于next方法的參數表示上一個yield表達式的返回值,所以在第一次使用next方法時,傳遞參數是無效的。V8 引擎直接忽略第一次使用next方法時的參數,只有從第二次使用next方法開始,參數才是有效的。從語義上講,第一個next方法用來啟動遍歷器對象,所以不用帶有參數。
3 應用
Generator 可以暫停函數執(zhí)行,返回任意表達式的值。這種特點使得 Generator 有多種應用場景。
異步操作的同步化表達
Generator 函數的暫停執(zhí)行的效果,意味著可以把異步操作寫在yield表達式里面,等到調用next方法時再往后執(zhí)行。這實際上等同于不需要寫回調函數了,因為異步操作的后續(xù)操作可以放在yield表達式下面,反正要等到調用next方法時再執(zhí)行。所以,Generator 函數的一個重要實際意義就是用來處理異步操作,改寫回調函數
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加載UI
loader.next()
// 卸載UI
loader.next()
上面代碼中,第一次調用loadUI函數時,該函數不會執(zhí)行,僅返回一個遍歷器。下一次對該遍歷器調用next方法,則會顯示Loading界面(showLoadingScreen),并且異步加載數據(loadUIDataAsynchronously)。等到數據加載完成,再一次使用next方法,則會隱藏Loading界面??梢钥吹?,這種寫法的好處是所有Loading界面的邏輯,都被封裝在一個函數,按部就班非常清晰。
Ajax 是典型的異步操作,通過 Generator 函數部署 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函數,就是通過 Ajax 操作獲取數據。可以看到,除了多了一個yield,它幾乎與同步操作的寫法完全一樣。注意,makeAjaxCall函數中的next方法,必須加上response參數,因為yield表達式,本身是沒有值的,總是等于undefined。
Generator 函數的異步應用
Generator 函數將 JavaScript 異步編程帶入了一個全新的階段
傳統的回調函數
回調函數本身并沒有問題,它的問題出現在多個回調函數嵌套。假定讀取A文件之后,再讀取B文件,代碼如下。
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
不難想象,如果依次讀取兩個以上的文件,就會出現多重嵌套。代碼不是縱向發(fā)展,而是橫向發(fā)展,很快就會亂成一團,無法管理。因為多個異步操作形成了強耦合,只要有一個操作需要修改,它的上層回調函數和下層回調函數,可能都要跟著修改。這種情況就稱為”回調函數地獄”(callback hell)。
Promise 對象就是為了解決這個問題而提出的。它不是新的語法功能,而是一種新的寫法,允許將回調函數的嵌套,改成鏈式調用。采用 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 的寫法只是回調函數的改進,使用then方法以后,異步任務的兩段執(zhí)行看得更清楚了,除此以外,并無新意。
Promise 的最大問題是代碼冗余,原來的任務被 Promise 包裝了一下,不管什么操作,一眼看去都是一堆then,原來的語義變得很不清楚。
Generator 函數 出馬了
協程
傳統的編程語言,早有異步編程的解決方案(其實是多任務的解決方案)。其中有一種叫做”協程”(coroutine),意思是多個線程互相協作,完成異步任務。
協程有點像函數,又有點像線程。它的運行流程大致如下。
第一步,協程A開始執(zhí)行。
第二步,協程A執(zhí)行到一半,進入暫停,執(zhí)行權轉移到協程B。
第三步,(一段時間后)協程B交還執(zhí)行權。
第四步,協程A恢復執(zhí)行。
上面流程的協程A,就是異步任務,因為它分成兩段(或多段)執(zhí)行。
舉例來說,讀取文件的協程寫法如下。
function* asyncJob() {
// ...其他代碼
var f = yield readFile(fileA);
// ...其他代碼
}
上面代碼的函數asyncJob是一個協程,它的奧妙就在其中的yield命令。它表示執(zhí)行到此處,執(zhí)行權將交給其他協程。也就是說,yield命令是異步兩個階段的分界線。
協程遇到yield命令就暫停,等到執(zhí)行權返回,再從暫停的地方繼續(xù)往后執(zhí)行。它的最大優(yōu)點,就是代碼的寫法非常像同步操作,如果去除yield命令,簡直一模一樣。
協程的 Generator 函數實現
Generator 函數是協程在 ES6 的實現,最大特點就是可以交出函數的執(zhí)行權(即暫停執(zhí)行)。
整個 Generator 函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操作需要暫停的地方,都用yield語句注明。Generator 函數的執(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 }
上面代碼中,調用 Generator 函數,會返回一個內部指針(即遍歷器)g。這是 Generator 函數不同于普通函數的另一個地方,即執(zhí)行它不會返回結果,返回的是指針對象。調用指針g的next方法,會移動內部指針(即執(zhí)行異步任務的第一段),指向第一個遇到的yield語句,上例是執(zhí)行到x + 2為止。
換言之,next方法的作用是分階段執(zhí)行Generator函數。每次調用next方法,會返回一個對象,表示當前階段的信息(value屬性和done屬性)。value屬性是yield語句后面表達式的值,表示當前階段的值;done屬性是一個布爾值,表示 Generator 函數是否執(zhí)行完畢,即是否還有下一個階段。
Generator 函數的數據交換和錯誤處理
Generator 函數可以暫停執(zhí)行和恢復執(zhí)行,這是它能封裝異步任務的根本原因。除此之外,它還有兩個特性,使它可以作為異步編程的完整解決方案:函數體內外的數據交換和錯誤處理機制。
next返回值的 value 屬性,是 Generator 函數向外輸出數據;next方法還可以接受參數,向 Generator 函數體內輸入數據。
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方法帶有參數2,這個參數可以傳入 Generator 函數,作為上個階段異步任務的返回結果,被函數體內的變量y接收。因此,這一步的value屬性,返回的就是2(變量y的值)。
Generator 函數內部還可以部署錯誤處理代碼,捕獲函數體外拋出的錯誤。
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 函數體外,使用指針對象的throw方法拋出的錯誤,可以被函數體內的try…catch代碼塊捕獲。這意味著,出錯的代碼與處理錯誤的代碼,實現了時間和空間上的分離,這對于異步編程無疑是很重要的。
異步任務的封裝
下面看看如何使用 Generator 函數,執(zhí)行一個真實的異步任務。
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 函數封裝了一個異步操作,該操作先讀取一個遠程接口,然后從 JSON 格式的數據解析信息。就像前面說過的,這段代碼非常像同步操作,除了加上了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 函數,獲取遍歷器對象,然后使用next方法(第二行),執(zhí)行異步任務的第一階段。由于Fetch模塊返回的是一個 Promise 對象,因此要用then方法調用下一個next方法。
可以看到,雖然 Generator 函數將異步操作表示得很簡潔,但是流程管理卻不方便(即何時執(zhí)行第一階段、何時執(zhí)行第二階段)。
4 co 模塊
co 模塊是著名程序員 TJ Holowaychuk 于 2013 年 6 月發(fā)布的一個小工具,用于 Generator 函數的自動執(zhí)行。
下面是一個 Generator 函數,用于依次讀取兩個文件。
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 函數的執(zhí)行器。
var co = require('co');
co(gen);
上面代碼中,Generator 函數只要傳入co函數,就會自動執(zhí)行。
co函數返回一個Promise對象,因此可以用then方法添加回調函數。
co(gen).then(function (){
console.log('Generator 函數執(zhí)行完成');
});
上面代碼中,等到 Generator 函數執(zhí)行結束,就會輸出一行提示
為什么 co 可以自動執(zhí)行 Generator 函數?
前面說過,Generator 就是一個異步操作的容器。它的自動執(zhí)行需要一種機制,當異步操作有了結果,能夠自動交回執(zhí)行權。
兩種方法可以做到這一點。
(1)回調函數。將異步操作包裝成 Thunk 函數,在回調函數里面交回執(zhí)行權。
(2)Promise 對象。將異步操作包裝成 Promise 對象,用then方法交回執(zhí)行權
基于 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 函數。
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
});
手動執(zhí)行其實就是用then方法,層層添加回調函數。理解了這一點,就可以寫出一個自動執(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 函數還沒執(zhí)行到最后一步,next函數就調用自身,以此實現自動執(zhí)行
co 模塊的源碼
co 就是上面那個自動執(zhí)行器的擴展,它的源碼只有幾十行,非常簡單。
首先,co 函數接受 Generator 函數作為參數,返回一個 Promise 對象。
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
});
}
在返回的 Promise 對象里面,co 先檢查參數gen是否為 Generator 函數。如果是,就執(zhí)行該函數,得到一個內部指針對象;如果不是就返回,并將 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 函數的內部指針對象的next方法,包裝成onFulfilled函數。這主要是為了能夠捕捉拋出的錯誤。
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);
}
});
}
最后,就是關鍵的next函數,它會反復調用自身。
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函數的內部代碼,一共只有四行命令。
第一行,檢查當前是否為 Generator 函數的最后一步,如果是就返回。
第二行,確保每一步的返回值,是 Promise 對象。
第三行,使用then方法,為返回值加上回調函數,然后通過onFulfilled函數再次調用next函數。
第四行,在參數不符合要求的情況下(參數非 Thunk 函數和 Promise 對象),將 Promise 對象的狀態(tài)改為rejected,從而終止執(zhí)行。
有了前面的基礎 我們再來看 async
ES2017 標準引入了 async 函數,使得異步操作變得更加方便。
async 函數是什么?一句話,它就是 Generator 函數的語法糖。
前文有一個 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());
};
寫成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());
};
一比較就會發(fā)現,async函數就是將 Generator 函數的星號(*)替換成async,將yield替換成await,僅此而已!!!
async函數對 Generator 函數的改進,體現在以下四點:
(1)內置執(zhí)行器。
Generator 函數的執(zhí)行必須靠執(zhí)行器,所以才有了co模塊,而async函數自帶執(zhí)行器。也就是說,async函數的執(zhí)行,與普通函數一模一樣.
(2)更好的語義。
async和await,比起星號和yield,語義更清楚了。async表示函數里有異步操作,await表示緊跟在后面的表達式需要等待結果。
(3)更廣的適用性。
co模塊約定,yield命令后面只能是 Thunk 函數或 Promise 對象,而async函數的await命令后面,可以是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同于同步操作)。
(4)返回值是 Promise。
async函數的返回值是 Promise 對象,這比 Generator 函數的返回值是 Iterator 對象方便多了。你可以用then方法指定下一步的操作
基本用法
async函數返回一個 Promise對象,可以使用then方法添加回調函數。當函數執(zhí)行的時候,一旦遇到await就會先返回,等到異步操作完成,再接著執(zhí)行函數體內后面的語句。
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函數的語法規(guī)則總體上比較簡單,難點是錯誤處理機制。
返回 Promise 對象
async函數返回一個 Promise 對象。
async函數內部return語句返回的值,會成為then方法回調函數的參數
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
上面代碼中,函數f內部return命令返回的值,會被then方法回調函數接收到。
async函數內部拋出錯誤,會導致返回的 Promise 對象變?yōu)閞eject狀態(tài)。拋出的錯誤對象會被catch方法回調函數接收到。
async function f() {
throw new Error('出錯了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出錯了
Promise 對象的狀態(tài)變化
async函數返回的 Promise 對象,必須等到內部所有await命令后面的 Promise 對象執(zhí)行完,才會發(fā)生狀態(tài)改變,除非遇到return語句或者拋出錯誤。也就是說,只有async函數內部的異步操作執(zhí)行完,才會執(zhí)行then方法指定的回調函數。
await 命令
正常情況下,await命令后面是一個 Promise 對象。如果不是,會被轉成一個立即resolve的 Promise 對象。
async function f() {
return await 123;
}
f().then(v => console.log(v))
// 123
上面代碼中,await命令的參數是數值123,它被轉成 Promise 對象,并立即resolve。
await命令后面的 Promise 對象如果變?yōu)閞eject狀態(tài),則reject的參數會被catch方法的回調函數接收到。
sync function f() {
await Promise.reject('出錯了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出錯了
注意,上面代碼中,await語句前面沒有return,但是reject方法的參數依然傳入了catch方法的回調函數。這里如果在await前面加上return,效果是一樣的。
只要一個await語句后面的 Promise 變?yōu)閞eject,那么整個async函數都會中斷執(zhí)行。
async function f() {
await Promise.reject('出錯了');
await Promise.resolve('hello world'); // 不會執(zhí)行
}
有時,我們希望即使前一個異步操作失敗,也不要中斷后面的異步操作。這時可以將第一個await放在try…catch結構里面,這樣不管這個異步操作是否成功,第二個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方法,處理前面可能出現的錯誤。
async function f() {
await Promise.reject('出錯了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
使用注意點
第一點,前面已經說過,await命令后面的Promise對象,運行結果可能是rejected,所以最好把await命令放在try…catch代碼塊中。
第二點,多個await命令后面的異步操作,如果不存在繼發(fā)關系,最好讓它們同時觸發(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函數之中,如果用在普通函數,就會報錯
async 函數的實現原理
async 函數的實現原理,就是將 Generator 函數和自動執(zhí)行器,包裝在一個函數里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
所有的async函數都可以寫成上面的第二種形式,其中的spawn函數就是自動執(zhí)行器。
下面給出spawn函數的實現,基本就是前文自動執(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