async await 的原理

前言

在公司的項目中拆讯,我們經(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冈止,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子候齿,更是在濱河造成了極大的恐慌熙暴,老刑警劉巖闺属,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異周霉,居然都是意外死亡掂器,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進店門俱箱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來国瓮,“玉大人,你說我怎么就攤上這事狞谱∧四。” “怎么了?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵跟衅,是天一觀的道長孵睬。 經(jīng)常有香客問我,道長伶跷,這世上最難降的妖魔是什么掰读? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮叭莫,結(jié)果婚禮上蹈集,老公的妹妹穿的比我還像新娘。我一直安慰自己雇初,他們只是感情好雾狈,可當(dāng)我...
    茶點故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著抵皱,像睡著了一般善榛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上呻畸,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天移盆,我揣著相機與錄音,去河邊找鬼伤为。 笑死咒循,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绞愚。 我是一名探鬼主播叙甸,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼位衩!你這毒婦竟也來了裆蒸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤糖驴,失蹤者是張志新(化名)和其女友劉穎僚祷,沒想到半個月后佛致,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡辙谜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年俺榆,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片装哆。...
    茶點故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡罐脊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜕琴,到底是詐尸還是另有隱情爹殊,我是刑警寧澤,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布奸绷,位于F島的核電站梗夸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏号醉。R本人自食惡果不足惜反症,卻給世界環(huán)境...
    茶點故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望畔派。 院中可真熱鬧铅碍,春花似錦、人聲如沸线椰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽憨愉。三九已至烦绳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間配紫,已是汗流浹背径密。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留躺孝,地道東北人享扔。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像植袍,于是被迫代替她去往敵國和親惧眠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,494評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 含義 async函數(shù)是Generator函數(shù)的語法糖于个,它使得異步操作變得更加方便氛魁。 寫成async函數(shù),就是下面這...
    oWSQo閱讀 1,987評論 0 2
  • async 函數(shù) 含義 ES2017 標準引入了 async 函數(shù),使得異步操作變得更加方便呆盖。 async 函數(shù)是...
    huilegezai閱讀 1,257評論 0 6
  • 特別說明,為便于查閱贷笛,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 685評論 0 1
  • Generator 函數(shù)的語法 簡介 基本概念 Generator 函數(shù)是 ES6 提供的一種異步編程解決方案应又,語...
    站在大神的肩膀上看世界閱讀 4,165評論 0 6
  • 簡介 基本概念 Generator函數(shù)是ES6提供的一種異步編程解決方案,語法行為與傳統(tǒng)函數(shù)完全不同乏苦。本章詳細介紹...
    呼呼哥閱讀 1,070評論 0 4