async await 的原理

前言

在公司的項目中,我們經常用到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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 含義 async函數是Generator函數的語法糖,它使得異步操作變得更加方便。 寫成async函數,就是下面這...
    oWSQo閱讀 2,043評論 0 2
  • async 函數 含義 ES2017 標準引入了 async 函數,使得異步操作變得更加方便。 async 函數是...
    huilegezai閱讀 1,320評論 0 6
  • 特別說明,為便于查閱,文章轉自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 739評論 0 1
  • Generator 函數的語法 簡介 基本概念 Generator 函數是 ES6 提供的一種異步編程解決方案,語...
    站在大神的肩膀上看世界閱讀 4,310評論 0 6
  • 簡介 基本概念 Generator函數是ES6提供的一種異步編程解決方案,語法行為與傳統函數完全不同。本章詳細介紹...
    呼呼哥閱讀 1,136評論 0 4

友情鏈接更多精彩內容