es6--js非同步程式設計Generator、Promise、Async
Generator
簡介
基本概念
- generator本身並不是用於處理非同步的,但是能夠實現!!! Generator函式是 ES6 提供的一種非同步程式設計解決方案,語法行為與傳統函式完全不同。
執行 Generator 函式會返回一個遍歷器物件,也就是說,Generator 函式還是一個遍歷器物件生成函式。返回的遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態。
跟普通函式的區別
function
關鍵字與函式名之間有一個星號–>"*";- 函式體內部使用
yield
表示式,定義不同的內部狀態。 - Generator函式不能跟new一起使用,會報錯。
function * helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面程式碼定義了一個 Generator 函式helloWorldGenerator
,它內部有兩個yield
表示式(hello
和world
),即該函式有三個狀態:hello,world 和 return 語句(結束執行)。
呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,也就是遍歷器物件(iterator)。
下一步,必須呼叫遍歷器物件的next
方法,使得指標移向下一個狀態。也就是說,每次呼叫next
方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield
表示式(或return
語句)為止。換言之,Generator 函式是分段執行的,yield
表示式是暫停執行的標記,而next
方法可以恢復執行。
//如果需要呼叫,就必須使用yield建立的物件進行呼叫
function* helloWorldGenerator() {
yield console.log("1");
yield console.log("2");
return 'ending';
}
var hw = helloWorldGenerator();
hw.next(); //1
hw.next(); //2
hw.next(); //{value: "ending", done: true}
ES6 沒有規定,
function
關鍵字與函式名之間的星號,寫在哪個位置。這導致下面的寫法都能通過。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
yield 表示式
由於 Generator 函式返回的遍歷器物件,只有呼叫next
方法才會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函式。yield
表示式就是暫停標誌。
遍歷器物件的next
方法的執行邏輯如下。
(1)遇到yield
表示式,就暫停執行後面的操作,並將緊跟在yield
後面的那個表示式的值,作為返回的物件的value
屬性值。
(2)下一次呼叫next
方法時,再繼續往下執行,直到遇到下一個yield
表示式。
(3)如果沒有再遇到新的yield
表示式,就一直執行到函式結束,直到return
語句為止,並將return
語句後面的表示式的值,作為返回的物件的value
屬性值。
(4)如果該函式沒有return
語句,則返回的物件的value
屬性值為undefined
。
yield
表示式與return
語句既有相似之處
都能返回緊跟在語句後面的那個表示式的值。
不同之處
每次遇到yield
,函式暫停執行,下一次再從該位置繼續向後執行,而return
語句不具備位置記憶的功能。一個函式裡面,只能執行一次(或者說一個)return
語句,但是可以執行多次(或者說多個)yield
表示式。正常函式只能返回一個值,因為只能執行一次return
;Generator 函式可以返回一系列的值,因為可以有任意多個yield
。
注意:
yield
表示式只能用在 Generator 函式裡面,用在其他地方都會報錯。
另外,yield
表示式如果用在另一個表示式之中,必須放在圓括號裡面。
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield 123)); // OK
與 Iterator 介面的關係
由於 Generator 函式就是遍歷器生成函式本身就具備iterator特性,因此可以把 Generator 賦值給物件的Symbol.iterator
屬性,從而使得本沒有遍歷器介面Iterator的該物件具有 Iterator 介面。
Object.prototype[Symbol.iterator] = function* (){
for(let i in this){
yield this[i];
}
}
//--------------Genertaor函式本身會返回具備Iterator介面的物件
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
注意:
-
for…of本身會自動呼叫具備Iterator介面物件的next()方法,因為Array、Map、Set、arguments、String、NodeList都是繼承與物件下面的。
-
原生Object不具備Iterator結構,需要從其他地方借,比如上面說
Object.prototype[Symbol.iterator]: Array.prototype[Symbol.iterator]
程式碼解讀:
- 給Object賦上Generator介面,那麼就會自動得到Iterator遍歷器的next方法,這個next方法在for…of裡會自動呼叫並執行當前暫停的yield:
Object.prototype[Symbol.iterator] = function* () {
for (let i in this) {
yield this[i];
}
}
let myObj = { foo: 3, bar: 7 };
console.log(myObj);
function* iterEntries(obj) {
let keys = Object.keys(obj);//返回["foo","bar"]
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];//通過yeild返回屬性和屬性值
}
}
for (let [key, value] of iterEntries(myObj)) {//解構yield返回的屬性和值
console.log(key, value);//使用完後for..of自動觸發下一次的next()
}
next 方法的引數
yield
本身沒有值,next
方法可以帶一個引數,該引數就會被當作上一個yield
的值,這個值後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 函式從暫停狀態到恢復執行,它的上下文狀態(context)是不變的。通過next
方法的引數,就有辦法在 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}
//這裡沒有傳參,則上一個y的yield沒有值,那麼2*undefined就是NaN,那麼y就是NaN,yield y/3就是NaN啦
a.next() // Object{value:NaN, done:true},NaN參與的計算都是NaN
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
for…of 迴圈
for...of
迴圈可以自動遍歷 Generator 函式時生成的Iterator
物件,且此時不再需要呼叫next
方法。
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
function* fibonacci() {
let [prev, curr] = [1, 1];
while(true){
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fibonacci()) {
if (n > 10000000) break;
console.log(n);
}
Generator.prototype.return()
Generator 函式返回的遍歷器物件,還有一個return
方法,可以返回給定的值,並且終結遍歷 Generator 函式。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true },中介並使後面的呼叫無效
g.next() // { value: undefined, done: true }
yield*
如果在 Generator 函式內部,呼叫另一個 Generator 函式,預設情況下是沒有效果的。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
foo();
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "y"
foo
和bar
都是 Generator 函式,在bar
裡面呼叫foo
,是不會有效果的。
這個就需要用到yield*
表示式,用來在一個 Generator 函式裡面執行另一個 Generator 函式。
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同於
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同於
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
再來看一個對比的例子。
function* inner() {
yield 'hello!';
}
function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一個遍歷器物件
gen.next().value // "close"
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
上面例子中,outer2
使用了yield*
,outer1
沒使用。結果就是,outer1
返回一個遍歷器物件,outer2
返回該遍歷器物件的內部值。
從語法角度看,如果yield
表示式後面跟的是一個遍歷器物件,需要在yield
表示式後面加上星號,表明它返回的是一個遍歷器物件。這被稱為yield*
表示式。
- 作為物件屬性的 Generator 函式
如果一個物件的屬性是 Generator 函式,可以簡寫成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
那麼這個和非同步有什麼關係呢?
- 就是因為yield的暫停機制,可以在後一個得到結果後選擇是否立馬繼續呼叫下一個yield後面的程式碼,這樣,準確而有序的順序加上可控的執行時間不正是非同步所需要的嗎?
Promise
什麼是Promise?
你不知道的JavaScript中卷: 未來值–>設想一下這樣一個場景:我走到快餐店的櫃檯,點了一個芝士漢堡。我交給收銀員1.47美元,通過付款下單,發出了一個隊某值"漢堡"的請求。我已經啟動了一次交易; 但是,通常我不能立馬得到一個芝士漢堡。收銀員會交給我一個東西作為替代:一張帶有訂單號的收據。訂單號就是一個IOU(I owe you)Promise,保證最終我能或得這個漢堡。 所以我好好的保留這個收據,這代表我的漢堡。在等待的過程中我可以做很多其他的事情,比如打個電話邀請其他朋友來一起聚一聚,看一份報紙,和周圍的人打個招呼等; 這個過程裡我的頭腦裡都會渴望著這個芝士漢堡,儘管還沒有拿到手,但是芝士漢堡就像佔位符一樣存在我的大腦裡。從本質上講,這個佔位符使得這個值不再依賴時間,這是個未來值。 終於,我聽到收銀員喊了"訂單113號",然後愉快地拿著收據走到收銀臺領取了自己的芝士漢堡。但是也有一種情況,就是在等待這個時間裡,芝士恰好在前一個人的時候用完了,那麼我不得不得選擇另外一種或者其他的解決辦法,也就是未來值的特性:可能成功,也可能失敗!
-
Promise有三種狀態pending(正在),fulfilled(成功),reject(已經失敗);但是關注的結果只有成功和失敗,因為正在進行的Promise物件是無法被操作的。正作為開弓沒有回頭箭。
-
Promise和Ajax的程式碼結構很類似,鏈式反應,有成功或者錯誤的返回結果,主體進行值或者某些處理,然後後面會得到結果,選擇是否還有進一步的執行,就像這樣的虛擬碼:
Promise((resolve,reject)=>{
//一系列需要等待結果的操作
//Promise是同步的,在script標籤裡會立即執行,飯後通過裡面的操作返回給後面的操作方法。
//resolve代表成功,會把結果返回到data,而reject返回到err
//如果Promise.resolve(1).then(data=>{})那麼then裡會立刻接收到值為1的值,並進行'{}'裡的程式碼,同樣的如果直接呼叫reject就會直接返回給後面的err
//resolve是Promise會預設傳遞進來的引數,主動呼叫觸發成功和失敗,但是通常不會主動觸發, 都是讓程式執行結果去決定該返回什麼結果。
//如果主動呼叫,resolve和reject裡的引數就是後面data和err接收到的值
})
.then((data,err)=>{
//Promise裡的操作成功,會把裡面返回的結果傳遞到data裡,前提是有返回結果,不然就是一個執行成功的狀態,可以出發then裡的程式碼,但沒有資料的互動。
//err是沒有按照預期的執行方向走,返回了異常,可能是程式碼出錯,也可能是其他方面的錯誤。
//或者這裡的兩個引數改成response/resoleved和reject會更加專業,它們代表反饋/接受 和 拒絕
})
.then()
.catch()
//當然,鏈式反應可以繼續進行下去,也可以用catch捕獲錯誤,這裡只是做簡單介紹,不做詳細介紹
- then的接受和拒絕 then會接收前面返回的Promise物件,這個物件一定會包括成功或錯誤兩個結果之一,那麼then裡可以怎麼處理,或者說怎麼寫對結果的處理呢?
//第一種,直接將兩種狀態解構出來
then((res,rej)=>{
function(res){
//對成功的處理
}
function(rej){
//對失敗的處理
}
})
//第二種,隱式的解構
then(
function(res){
//對成功的處理
},
function(rej){
//對失敗的處理
}
)
包裝Promise的作用:避免回撥地獄,過多的巢狀和回撥會使程式有極差的易讀性和維護性。
Promise.prototype.catch()
Promise.prototype.catch
方法是.then(null, rejection)
的別名,用於指定發生錯誤時的回撥函式。
p1()
.then(function(data){
console.log(data)
})
.catch(function(err){
console.log(err)
})
//reject不能結束Promise
//>5,走reject
協同者Promise.all()
Promise.all
方法用於將多個 Promise 例項,包裝成一個新的 Promise 例項。
const p = Promise.all([p1, p2, p3]);
p
的狀態由p1
、p2
、p3
決定,分成兩種情況。
- 只有
p1
、p2
、p3
的狀態都變成fulfilled
,p
的狀態才會變成fulfilled
。 此時p1
、p2
、p3
的返回值組成一個數組,傳遞給p
的回撥函式。 - 只要
p1
、p2
、p3
之中有一個被rejected
,p
的狀態就變成rejected
,此時第一個被reject
的例項的返回值,會傳遞給p
的回撥函式。
promises
是包含 3 個 Promise 例項的陣列,只有這 3 個例項的狀態都變成fulfilled
,或者其中有一個變為rejected
,才會呼叫Promise.all
方法後面的回撥函式。
如果作為引數的 Promise 例項,自己定義了catch
方法,那麼它一旦被rejected
,並不會觸發Promise.all()
的catch
方法,如果沒有引數沒有定義自己的catch,就會呼叫Promise.all()
的catch
方法。
//如果所有promise物件都成功了,返回成功的所有結果組成的陣列
let p = Promise.all([
new Promise((resolve,reject)=>{
resolve("1")
}),
new Promise((resolve,reject)=>{
resolve("2")
}),
new Promise((resolve,reject)=>{
resolve("3")
})
])
.then( data =>{
console.log(data); //["1","2","3"]
})
.catch( err =>{
console.log(err);
})
//如果又一個沒有成功,則返回第一個失敗的,後面不管什麼結果都返回那個失敗的結果,這叫豬隊友法則
let p2 = Promise.all([
new Promise((resolve,reject)=>{
resolve("1")
}),
new Promise((resolve,reject)=>{
reject("2")
}),
new Promise((resolve,reject)=>{
reject("3")
})
])
.then( data =>{
console.log(data);
})
.catch( err =>{
console.log(err); //2
})
競爭者Promise.race()
Promise.race
方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。
const p = Promise.race([p1, p2, p3]);
上面程式碼中,只要p1
、p2
、p3
之中有一個例項率先改變狀態,p
的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p
的回撥函式。
let p = Promise.race([
new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("100")
},100)
}),
new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("200")
},200)
}),
new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("99")
},99)
})
])
.then( data =>{
console.log(data);//99
})
.catch( err =>{
console.log(err)