淺談async·await

前言

這篇文章主要是梳理一下本身對阮一峯大神寫的關於async/await文章,有寫得不對的地方以及理解得不對的地方,各位大佬請指錯!promise

對比

簡單對比傳統異步promise異步async異步異步

下文都會以setTimeout來進行異步展現,方便理解。async

傳統的回調函數

setTimeout(callback,1000);

function callback(){
    console.log("拿到結果了!");
}

setTimeout函數傳入了兩個參數(1000/callback),setTimeout被調用的時候,主線程不會等待1秒,而是先執行別的任務。callback這個函數就是一個回調函數,即當1秒後,主線程會從新調用callback(這裏也再也不囉嗦去說event Loop方面的知識了);oop

那麼,當咱們異步函數須要嵌套的時候呢。好比這種狀況:性能

setTimeout(function(){
    console.log("第一個異步回調了!")
    setTimeout(function(){
        console.log("第二個異步回調了!")
        setTimeout(function(){
            console.log("第三個異步回調了!")
            setTimeout(function(){
                console.log("第四個異步回調了!")
                setTimeout(function(){
                    console.log("第五個異步回調了!")
                },1000);
            },1000);
        },1000);
    },1000);
},1000);

OK,想死不?線程

咱們用promise來處理設計

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, "finish");
  });
}

timeout(2000)
  .then(value => {
    console.log("第一層" + value);
    return timeout(2000);
  })
  .then(value => {
    console.log("第二層" + value);
    return timeout(2000);
  })
  .then(value => {
    console.log("第三層" + value);
    return timeout(2000);
  })
  .then(value => {
    console.log("第四層" + value);
    return timeout(2000);
  })
  .then(value => {
    console.log("第五層" + value);
    return timeout(2000);
  })
  .catch(err => {
    console.log(err);
  });

OK,好看點了!指針

可是仍是不方便!code

咱們用async/await來處理:

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, "finish");
  });
}
async function asyncTimeSys(){
    await timeout(1000);
    console.log("第一層異步結束!")
    await timeout(1000);
    console.log("第二層異步結束!")
    await timeout(1000);
    console.log("第三層異步結束!")
    await timeout(1000);
    console.log("第四層異步結束!")
    await timeout(1000);
    console.log("第五層異步結束!")
    return "all finish";
}
asyncTimeSys().then((value)=>{
    console.log(value);
});

OK,舒服了!

在這個asyncTimeSys函數裏面,全部的異步操做,寫的跟同步函數沒有什麼兩樣!

async的原型

async函數究竟是什麼?其實他就是Genarator函數(生成器函數)的語法糖而已!

  • 內置執行器。

Generator 函數的執行必須靠執行器,因此纔有了co模塊,而async函數自帶執行器。也就是說,async函數的執行,與普通函數如出一轍。徹底不像 Generator 函數,須要調用next方法,或者用co模塊,才能真正執行,獲得最後結果。

  • 更好的語義。

async和await,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。

  • 更廣的適用性。

co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async函數的await命令後面,能夠是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)。

  • 返回值是 Promise。

async函數的返回值是 Promise 對象,這比 Generator 函數的返回值是 Iterator 對象方便多了。你能夠用then方法指定下一步的操做。

進一步說,async函數徹底能夠看做多個異步操做,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。

其實,async函數就是一個由Generator封裝的異步環境,其內部是經過交換函數執行權,以及thunk函數來實現的!

用Generator函數封裝異步請求

OK,咱們簡單的封裝一個:

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, "finish");
  });
}

function *times(){
    let result =yield timeout(1000);
    return "second next"
}

let gen = times();    //拿到生成器函數,gen能夠理解爲指針
let firstYield = gen.next(); //firstYield此時爲gen指針指向的第一個yield右邊的表達式,此時timeout(1000)被執行
console.log(firstYield);    //   firstYield = {value:Pomise,done:false};

//接下來就是將firstYield中的value裏的promise拿出來,做爲正常的Promise調用,以下:
firstYield.value.then(()=>{
    //當timeout異步結束以後,執行如下代碼,再將gen指針執行下一個yield,因爲如下沒有yield了,因此gen.next()的value爲return裏的東西
    console.log("timeout finish");
    console.log(gen.next());    //{value: "second next", done: true}
}).catch((err)=>{

});

這樣封裝有什麼用呢,yield返回回來的東西,仍是得像promise那樣調用。

咱們先來看看同步的代碼,先讓它長得像async和await那樣子:

function* times() {
  yield console.log(1);
  yield console.log(2);
  yield console.log(3);
  return "second next";
}

let gen = times();

let result = gen.next();

while (!result.done) {
    result = gen.next();
}

OK,很是像了,可是,這是同步的。異步請求必須得等到第一個yield執行完成以後,才能去執行第二個yield。咱們若是改爲異步,確定會形成無限循環。

那麼,times生成器裏面若是都是異步的話,咱們應該怎麼調用呢?

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, "finish");
  });
}

function *times(){
    yield timeout(2000);
    yield timeout(2000);
    yield timeout(2000);
    return "finish all!";
}

let gen = times();

let gen1 = gen.next();
gen1.value.then(function(data){
    console.log(data+" one");

    let gen2 = gen.next();
    gen2.value.then(function(data){
        console.log(data+" two");

        let gen3 = gen.next();
        gen3.value.then(function(data){
            console.log(data+" three");



        })

    })

});

仔細觀察能夠發現,其實每個value的.then()方法都會傳入一個相同的回調函數,這意味着咱們可使用遞歸來流程化管理整個異步流程;

改造一下這個上邊的代碼;

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, "finish");
  });
}

function* times() {
  yield timeout(2000);
  yield timeout(2000);
  yield timeout(2000);
  return "finish all!";
}


function run(fn){
    let gen = fn();

    function next(){
        console.log("finish");
        let result = gen.next();
        if(result.done) return;
        result.value.then(next);
    }
    next();
}

run(times);

OK,如今咱們可使用run函數,使得生成器函數times裏的異步請求,一步接着一步往下執行。

那麼,這個run函數裏邊的next究竟是什麼呢,它實際上是一個thunk函數

thunk函數

Thunk函數的誕生是源於一個編譯器設計的問題:求值策略,即函數的參數到底應該什麼時候求值。

看下邊的代碼,請思考何時進行求值:

var x = 1;
function f(m) {
    return m * 2;
}
f(x + 5);

試問:x+5這個表達式應該何時求值

  • 傳值調用(call by value),即在進入函數體之間,先計算x+5的值,再將這個值(6)傳入函數f,例如c語言,這種作法的好處是實現比較簡單,可是有可能會形成性能損失。
  • 傳名調用(call by name),即直接將表達式(x+5)傳入函數體,只在用到它的時候求值。

OK,thunk函數到底是什麼:

編譯器的傳名調用實現,每每就是將參數放到一個臨時函數之中,再將這個臨時函數轉入函數體,這個臨時函數就叫作Thunk函數。

將上邊的代碼進行改造:

var thunk = function () {
    return x + 5;
};

function f(thunk) {
    return thunk() * 2;
}

js中的傳名調用是什麼呢,與真正的thunk有什麼區別呢?

JavaScript 語言是傳值調用,它的 Thunk 函數含義有所不一樣。在 JavaScript 語言中,Thunk 函數替換的不是表達式,而是多參數函數,將其替換成一個只接受回調函數做爲參數的單參數函數。

網上對於thunk的演示都是使用的fs模塊的readFile方法來進行演示

// 正常版本的readFile(多參數版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(單參數版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

其實,任何函數,只要參數有回調函數,就能寫成Thunk函數的形式。下面是一個簡單的Thunk函數轉換器。

讓咱們用setTimeout來進行一次演示:

//正常版本的setTimeout;
setTimeout(function(data){
    console.log(data);
},1000,"finish");

//thunk版本的setTimeout
let thunk = function(time){
    return function(callback){
        return setTimeout(callback,time,"finish");
    }
}
let setTimeoutChunk = thunk(1000);
setTimeoutChunk(function(data){
    console.log(data);
});

如今回頭看一看用Generator函數封裝異步請求這一節中最後一個實例中,咱們封裝的timeout函數,他其實就是一個thunk函數,我在那一節中沒有給你們說明這一條:

  • yield命令後面的必須是 Thunk 函數。

爲何Generator裏面必須使用thunk函數呢,由於咱們須要確保傳入的值只有一個,利用其回調函數,來進行遞歸自動控制Generator函數的流程,接收和交還程序的執行權;

相關文章
相關標籤/搜索