1. 程式人生 > >es6--js非同步程式設計Generator、Promise、Async

es6--js非同步程式設計Generator、Promise、Async

Generator

簡介

基本概念

  • generator本身並不是用於處理非同步的,但是能夠實現!!! Generator函式是 ES6 提供的一種非同步程式設計解決方案,語法行為與傳統函式完全不同。

執行 Generator 函式會返回一個遍歷器物件,也就是說,Generator 函式還是一個遍歷器物件生成函式。返回的遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態。

跟普通函式的區別

  1. function關鍵字與函式名之間有一個星號–>"*";
  2. 函式體內部使用yield表示式,定義不同的內部狀態。
  3. Generator函式不能跟new一起使用,會報錯。
function
* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator();

上面程式碼定義了一個 Generator 函式helloWorldGenerator,它內部有兩個yield表示式(helloworld),即該函式有三個狀態: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"

foobar都是 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的狀態由p1p2p3決定,分成兩種情況。

  1. 只有p1p2p3的狀態都變成fulfilledp的狀態才會變成fulfilled。 此時p1p2p3的返回值組成一個數組,傳遞給p的回撥函式。
  2. 只要p1p2p3之中有一個被rejectedp的狀態就變成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]);

上面程式碼中,只要p1p2p3之中有一個例項率先改變狀態,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)