1. 程式人生 > >自學-ES6篇-非同步操作和Async函式

自學-ES6篇-非同步操作和Async函式

非同步程式設計對JavaScript語言太重要。Javascript語言的執行環境是“單執行緒”的,如果沒有非同步程式設計,根本沒法用,非卡死不可。

ES6誕生以前,非同步程式設計的方法,大概有下面四種。

  • 回撥函式
  • 事件監聽
  • 釋出/訂閱
  • Promise 物件
回撥函式

ES6將JavaScript非同步程式設計帶入了一個全新的階段,ES7的Async函式更是提出了非同步程式設計的終極解決方案。

1、基本概念

非同步

所謂"非同步",簡單說就是一個任務分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段

比如,有一個任務是讀取檔案進行處理,任務的第一段是向作業系統發出請求,要求讀取檔案。然後,程式執行其他任務,等到作業系統返回檔案,再接著執行任務的第二段(處理檔案)。這種不連續的執行,就叫做非同步。

相應地,連續的執行就叫做同步。由於是連續執行,不能插入其他任務,所以作業系統從硬碟讀取檔案的這段時間,程式只能乾等著。

回撥函式

JavaScript語言對非同步程式設計的實現,就是回撥函式。所謂回撥函式,就是把任務的第二段單獨寫在一個函式裡面,等到重新執行這個任務的時候,就直接呼叫這個函式。它的英語名字callback,直譯過來就是"重新呼叫"。

讀取檔案進行處理,是這樣寫的。

fs.readFile('/etc/passwd', function (err, data) {
  if (err) throw err;
  console.log(data);
});

上面程式碼中,readFile函式的第二個引數,就是回撥函式,也就是任務的第二段。等到作業系統返回了/etc/passwd

這個檔案以後,回撥函式才會執行。

一個有趣的問題是,為什麼Node.js約定,回撥函式的第一個引數,必須是錯誤物件err(如果沒有錯誤,該引數就是null)?原因是執行分成兩段,在這兩段之間丟擲的錯誤,程式無法捕捉,只能當作引數,傳入第二段。


Promise

回撥函式本身並沒有問題,它的問題出現在多個回撥函式巢狀。假定讀取A檔案之後,再讀取B檔案,程式碼如下。

fs.readFile(fileA, function (err, data) {
  fs.readFile(fileB, function (err, data) {
    // ...
  });
});

不難想象,如果依次讀取多個檔案,就會出現多重巢狀。程式碼不是縱向發展,而是橫向發展,很快就會亂成一團,無法管理。這種情況就稱為"回撥函式噩夢"(callback hell)。

Promise就是為了解決這個問題而提出的。它不是新的語法功能,而是一種新的寫法,允許將回調函式的巢狀,改成鏈式呼叫。採用Promise,連續讀取多個檔案,寫法如下。

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);
});

上面程式碼中,我使用了fs-readfile-promise模組,它的作用就是返回一個Promise版本的readFile函式。Promise提供then方法載入回撥函式,catch方法捕捉執行過程中丟擲的錯誤。

可以看到,Promise 的寫法只是回撥函式的改進,使用then方法以後,非同步任務的兩段執行看得更清楚了,除此以外,並無新意。

Promise 的最大問題是程式碼冗餘,原來的任務被Promise 包裝了一下,不管什麼操作,一眼看去都是一堆 then,原來的語義變得很不清楚。

那麼,有沒有更好的寫法呢?


2、Generator函式

協程

  • 第一步,協程A開始執行。
  • 第二步,協程A執行到一半,進入暫停,執行權轉移到協程B。
  • 第三步,(一段時間後)協程B交還執行權。
  • 第四步,協程A恢復執行。

傳統的程式語言,早有非同步程式設計的解決方案(其實是多工的解決方案)。其中有一種叫做"協程"(coroutine),意思是多個執行緒互相協作,完成非同步任務。

協程有點像函式,又有點像執行緒。它的執行流程大致如下。

上面流程的協程A,就是非同步任務,因為它分成兩段(或多段)執行。

舉例來說,讀取檔案的協程寫法如下。

function *asyncJob() {
  // ...其他程式碼
  var f = yield readFile(fileA);
  // ...其他程式碼
}

上面程式碼的函式asyncJob是一個協程,它的奧妙就在其中的yield命令。它表示執行到此處,執行權將交給其他協程。也就是說,yield命令是非同步兩個階段的分界線。

協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行。它的最大優點,就是程式碼的寫法非常像同步操作,如果去除yield命令,簡直一模一樣。


Generator函式的概念

Generator函式是協程在ES6的實現,最大特點就是可以交出函式的執行權(即暫停執行)。

整個Generator函式就是一個封裝的非同步任務,或者說是非同步任務的容器。非同步操作需要暫停的地方,都用yield語句註明。Generator函式的執行方法如下。

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函式不同於普通函式的另一個地方,即執行它不會返回結果,返回的是指標物件。呼叫指標g的next方法,會移動內部指標(即執行非同步任務的第一段),指向第一個遇到的yield語句,上例是執行到x + 2為止。

換言之,next方法的作用是分階段執行Generator函式。每次呼叫next方法,會返回一個物件,表示當前階段的資訊(value屬性和done屬性)。value屬性是yield語句後面表示式的值,表示當前階段的值;done屬性是一個布林值,表示Generator函式是否執行完畢,即是否還有下一個階段。


Generator函式的資料交換和錯誤處理

Generator函式可以暫停執行和恢復執行,這是它能封裝非同步任務的根本原因。除此之外,它還有兩個特性,使它可以作為非同步程式設計的完整解決方案:函式體內外的資料交換和錯誤處理機制。

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 函式,執行一個真實的非同步任務。

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命令。

執行這段程式碼的方法如下。

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

上面程式碼中,首先執行Generator函式,獲取遍歷器物件,然後使用next 方法(第二行),執行非同步任務的第一階段。由於Fetch模組返回的是一個Promise物件,因此要用then方法呼叫下一個next 方法。

可以看到,雖然 Generator 函式將非同步操作表示得很簡潔,但是流程管理卻不方便(即何時執行第一階段、何時執行第二階段)。


3、Thunk函式
引數的求值策略

Thunk函式早在上個世紀60年代就誕生了。

那時,程式語言剛剛起步,計算機學家還在研究,編譯器怎麼寫比較好。一個爭論的焦點是"求值策略",即函式的引數到底應該何時求值。

var x = 1;

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

f(x + 5)

上面程式碼先定義函式f,然後向它傳入表示式x + 5。請問,這個表示式應該何時求值?

一種意見是"傳值呼叫"(call by value),即在進入函式體之前,就計算x + 5的值(等於6),再將這個值傳入函式f 。C語言就採用這種策略。

f(x + 5)
// 傳值呼叫時,等同於
f(6)
另一種意見是"傳名呼叫"(call by name),即直接將表示式x + 5傳入函式體,只在用到它的時候求值。Haskell語言採用這種策略。
f(x + 5)
// 傳名呼叫時,等同於
(x + 5) * 2
傳值呼叫和傳名呼叫,哪一種比較好?回答是各有利弊。傳值呼叫比較簡單,但是對引數求值的時候,實際上還沒用到這個引數,有可能造成效能損失。
function f(a, b){
  return b;
}

f(3 * x * x - 2 * x - 1, x);
上面程式碼中,函式f的第一個引數是一個複雜的表示式,但是函式體內根本沒用到。對這個引數求值,實際上是不必要的。因此,有一些計算機學家傾向於"傳名呼叫",即只在執行時求值。

Thunk函式的含義

編譯器的"傳名呼叫"實現,往往是將引數放到一個臨時函式之中,再將這個臨時函式傳入函式體。這個臨時函式就叫做Thunk函式。

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

f(x + 5);

// 等同於

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

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

上面程式碼中,函式f的引數x + 5被一個函式替換了。凡是用到原引數的地方,對Thunk函式求值即可。

這就是Thunk函式的定義,它是"傳名呼叫"的一種實現策略,用來替換某個表示式。


javascript語言Thunk函式

JavaScript語言是傳值呼叫,它的Thunk函式含義有所不同。在JavaScript語言中,Thunk函式替換的不是表示式,而是多引數函式,將其替換成單引數的版本,且只接受回撥函式作為引數。

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

// Thunk版本的readFile(單引數版本)
var readFileThunk = Thunk(fileName);
readFileThunk(callback);

var Thunk = function (fileName){
  return function (callback){
    return fs.readFile(fileName, callback);
  };
};

上面程式碼中,fs模組的readFile方法是一個多引數函式,兩個引數分別為檔名和回撥函式。經過轉換器處理,它變成了一個單引數函式,只接受回撥函式作為引數。這個單引數版本,就叫做Thunk函式。

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

// ES5版本
var Thunk = function(fn){
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      return fn.apply(this, args);
    }
  };
};

// ES6版本
var Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};

使用上面的轉換器,生成fs.readFile的Thunk函式。

var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);

下面是另一個完整的例子。

function f(a, cb) {
  cb(a);
}
let ft = Thunk(f);

let log = console.log.bind(console);
ft(1)(log) // 1

Thunkify模組

生產環境的轉換器,建議使用Thunkify模組。

首先是安裝。

$ npm install thunkify

使用方式如下。

var thunkify = require('thunkify');
var fs = require('fs');

var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
  // ...
});

Thunkify的原始碼與上一節那個簡單的轉換器非常像。

function thunkify(fn){
  return function(){
    var args = new Array(arguments.length);
    var ctx = this;

    for(var i = 0; i < args.length; ++i) {
      args[i] = arguments[i];
    }

    return function(done){
      var called;

      args.push(function(){
        if (called) return;
        called = true;
        done.apply(null, arguments);
      });

      try {
        fn.apply(ctx, args);
      } catch (err) {
        done(err);
      }
    }
  }
};
它的原始碼主要多了一個檢查機制,變數called確保回撥函式只執行一次。這樣的設計與下文的Generator函式相關。請看下面的例子。
function f(a, b, callback){
  var sum = a + b;
  callback(sum);
  callback(sum);
}

var ft = thunkify(f);
var print = console.log.bind(console);
ft(1, 2)(print);
// 3

上面程式碼中,由於thunkify只允許回撥函式執行一次,所以只輸出一行結果。

Generator函式的流程管理

你可能會問, Thunk函式有什麼用?回答是以前確實沒什麼用,但是ES6有了Generator函式,Thunk函式現在可以用於Generator函式的自動流程管理。

Generator函式可以自動執行。

function* gen() {
  // ...
}

var g = gen();
var res = g.next();

while(!res.done){
  console.log(res.value);
  res = g.next();
}

上面程式碼中,Generator函式gen會自動執行完所有步驟。

但是,這不適合非同步操作。如果必須保證前一步執行完,才能執行後一步,上面的自動執行就不可行。這時,Thunk函式就能派上用處。以讀取檔案為例。下面的Generator函式封裝了兩個非同步操作。

var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);

var gen = function* (){
  var r1 = yield readFile('/etc/fstab');
  console.log(r1.toString());
  var r2 = yield readFile('/etc/shells');
  console.log(r2.toString());
};

上面程式碼中,yield命令用於將程式的執行權移出Generator函式,那麼就需要一種方法,將執行權再交還給Generator函式。

這種方法就是Thunk函式,因為它可以在回撥函式裡,將執行權交還給Generator函式。為了便於理解,我們先看如何手動執行上面這個Generator函式。

var g = gen();

var r1 = g.next();
r1.value(function(err, data){
  if (err) throw err;
  var r2 = g.next(data);
  r2.value(function(err, data){
    if (err) throw err;
    g.next(data);
  });
});

上面程式碼中,變數g是Generator函式的內部指標,表示目前執行到哪一步。next方法負責將指標移動到下一步,並返回該步的資訊(value屬性和done屬性)。

仔細檢視上面的程式碼,可以發現Generator函式的執行過程,其實是將同一個回撥函式,反覆傳入next方法的value屬性。這使得我們可以用遞迴來自動完成這個過程。


Thunk函式的自動流程管理 Thunk函式真正的威力,在於可以自動執行Generator函式。下面就是一個基於Thunk函式的Generator執行器。
function run(fn) {
  var gen = fn();

  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
  }

  next();
}

function* g() {
  // ...
}

run(g);

上面程式碼的run函式,就是一個Generator函式的自動執行器。內部的next函式就是Thunk的回撥函式。next函式先將指標移到Generator函式的下一步(gen.next方法),然後判斷Generator函式是否結束(result.done屬性),如果沒結束,就將next函式再傳入Thunk函式(result.value屬性),否則就直接退出。

有了這個執行器,執行Generator函式方便多了。不管內部有多少個非同步操作,直接把Generator函式傳入run函式即可。當然,前提是每一個非同步操作,都要是Thunk函式,也就是說,跟在yield命令後面的必須是Thunk函式。

var g = function* (){
  var f1 = yield readFile('fileA');
  var f2 = yield readFile('fileB');
  // ...
  var fn = yield readFile('fileN');
};

run(g);

上面程式碼中,函式g封裝了n個非同步的讀取檔案操作,只要執行run函式,這些操作就會自動完成。這樣一來,非同步操作不僅可以寫得像同步操作,而且一行程式碼就可以執行。

Thunk函式並不是Generator函式自動執行的唯一方案。因為自動執行的關鍵是,必須有一種機制,自動控制Generator函式的流程,接收和交還程式的執行權。回撥函式可以做到這一點,Promise 物件也可以做到這一點。


4、co模組
基本用法

co模組是著名程式設計師TJ Holowaychuk於2013年6月釋出的一個小工具,用於Generator函式的自動執行。

比如,有一個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函式的執行器。

var co = require('co');
co(gen);

上面程式碼中,Generator函式只要傳入co函式,就會自動執行。

co函式返回一個Promise物件,因此可以用then方法添加回調函式。

co(gen).then(function (){
  console.log('Generator 函式執行完成');
});

上面程式碼中,等到Generator函式執行結束,就會輸出一行提示。

co模組的原理

為什麼co可以自動執行Generator函式?

前面說過,Generator就是一個非同步操作的容器。它的自動執行需要一種機制,當非同步操作有了結果,能夠自動交回執行權。

兩種方法可以做到這一點。

(1)回撥函式。將非同步操作包裝成Thunk函式,在回撥函式裡面交回執行權。

(2)Promise 物件。將非同步操作包裝成Promise物件,用then方法交回執行權。

co模組其實就是將兩種自動執行器(Thunk函式和Promise物件),包裝成一個模組。使用co的前提條件是,Generator函式的yield命令後面,只能是Thunk函式或Promise物件。

上一節已經介紹了基於Thunk函式的自動執行器。下面來看,基於Promise物件的自動執行器。這是理解co模組必須的。


基於Promise物件的自動執行

還是沿用上面的例子。首先,把fs模組的readFile方法包裝成一個Promise物件。

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());
};

然後,手動執行上面的Generator函式。

var g = gen();

g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
});

手動執行其實就是用then方法,層層添加回調函式。理解了這一點,就可以寫出一個自動執行器。

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函式還沒執行到最後一步,next函式就呼叫自身,以此實現自動執行。

co模組的原始碼

co就是上面那個自動執行器的擴充套件,它的原始碼只有幾十行,非常簡單。

首先,co函式接受Generator函式作為引數,返回一個 Promise 物件。

function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
  });
}

在返回的Promise物件裡面,co先檢查引數gen是否為Generator函式。如果是,就執行該函式,得到一個內部指標物件;如果不是就返回,並將Promise物件的狀態改為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 物件的狀態改為 rejected,從而終止執行。


處理併發的非同步操作

co支援併發的非同步操作,即允許某些操作同時進行,等到它們全部完成,才進行下一步。

這時,要把併發的操作都放在陣列或物件裡面,跟在yield語句後面。

// 陣列的寫法
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res);
}).catch(onerror);

// 物件的寫法
co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  };
  console.log(res);
}).catch(onerror);

下面是另一個例子。

co(function* () {
  var values = [n1, n2, n3];
  yield values.map(somethingAsync);
});

function* somethingAsync(x) {
  // do something async
  return y
}

上面的程式碼允許併發三個somethingAsync非同步操作,等到它們全部完成,才會進行下一步。

5、async函式
含義

ES7提供了async函式,使得非同步操作變得更加方便。async函式是什麼?一句話,async函式就是Generator函式的語法糖。

前文有一個Generator函式,依次讀取兩個檔案。

var fs = require('fs');

var readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) 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());
};

寫成async函式,就是下面這樣。

var asyncReadFile = async function (){
  var f1 = await readFile('/etc/fstab');
  var f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

一比較就會發現,async函式就是將Generator函式的星號(*)替換成async,將yield替換成await,僅此而已。

async函式對 Generator 函式的改進,體現在以下四點。

(1)內建執行器。Generator函式的執行必須靠執行器,所以才有了co模組,而async函式自帶執行器。也就是說,async函式的執行,與普通函式一模一樣,只要一行。

var result = asyncReadFile();

上面的程式碼呼叫了asyncReadFile函式,然後它就會自動執行,輸出最後結果。這完全不像Generator函式,需要呼叫next方法,或者用co模組,才能得到真正執行,得到最後結果。

(2)更好的語義。asyncawait,比起星號和yield,語義更清楚了。async表示函式裡有非同步操作,await表示緊跟在後面的表示式需要等待結果。

(3)更廣的適用性。 co模組約定,yield命令後面只能是Thunk函式或Promise物件,而async函式的await命令後面,可以是Promise物件和原始型別的值(數值、字串和布林值,但這時等同於同步操作)。

(4)返回值是Promise。async函式的返回值是Promise物件,這比Generator函式的返回值是Iterator物件方便多了。你可以用then方法指定下一步的操作。

進一步說,async函式完全可以看作多個非同步操作,包裝成的一個Promise物件,而await命令就是內部then命令的語法糖。


語法

async函式的語法規則總體上比較簡單,難點是錯誤處理機制。

(1)async函式返回一個Promise物件。

async函式內部return語句返回的值,會成為then方法回撥函式的引數。

async function f() {
  return 'hello world';
}

f().then(v => console.log(v))
// "hello world"

上面程式碼中,函式f內部return命令返回的值,會被then方法回撥函式接收到。

async函式內部丟擲錯誤,會導致返回的Promise物件變為reject狀態。丟擲的錯誤物件會被catch方法回撥函式接收到。

async function f() {
  throw new Error('出錯了');
}

f().then(
  v => console.log(v),
  e => console.log(e)
)
// Error: 出錯了

(2)async函式返回的Promise物件,必須等到內部所有await命令的Promise物件執行完,才會發生狀態改變。也就是說,只有async函式內部的非同步操作執行完,才會執行then方法指定的回撥函式。

下面是一個例子。

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
(3)正常情況下,await命令後面是一個Promise物件。如果不是,會被轉成一個立即resolve的Promise物件。
async function f() {
  return await 123;
}

f().then(v => console.log(v))
// 123

上面程式碼中,await命令的引數是數值123,它被轉成Promise物件,並立即resolve

await命令後面的Promise物件如果變為reject狀態,則reject的引數會被catch方法的回撥函式接收到。

async function f() {
  await Promise.reject('出錯了');
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出錯了

注意,上面程式碼中,await語句前面沒有return,但是reject方法的引數依然傳入了catch方法的回撥函式。這裡如果在await前面加上return,效果是一樣的。

只要一個await語句後面的Promise變為reject,那麼整個async函式都會中斷執行。

async function f() {
  await Promise.reject('出錯了');
  await Promise.resolve('hello world'); // 不會執行
}

上面程式碼中,第二個await語句是不會執行的,因為第一個await語句狀態變成了reject

為了避免這個問題,可以將第一個await放在try...catch結構裡面,這樣第二個await就會執行。

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))
// 出錯了
// hello world

如果有多個await命令,可以統一放在try...catch結構中。

async function main() {
  try {
    var val1 = await firstStep();
    var val2 = await secondStep(val1);
    var val3 = await thirdStep(val1, val2);

    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err);
  }
}
(4)如果await後面的非同步操作出錯,那麼等同於async函式返回的Promise物件被reject
async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error('出錯了');
  });
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出錯了

上面程式碼中,async函式f執行後,await後面的Promise物件會丟擲一個錯誤物件,導致catch方法的回撥函式被呼叫,它的引數就是丟擲的錯誤物件。具體的執行機制,可以參考後文的“async函式的實現”。

防止出錯的方法,也是將其放在try...catch程式碼塊之中。

async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出錯了');
    });
  } catch(e) {
  }
  return await('hello world');
}

async函式的實現

async 函式的實現,就是將 Generator 函式和自動執行器,包裝在一個函式裡。

async function fn(args){
  // ...
}

// 等同於

function fn(args){
  return spawn(function*() {
    // ...
  });
}

所有的

相關推薦

自學-ES6-非同步操作Async函式

非同步程式設計對JavaScript語言太重要。Javascript語言的執行環境是“單執行緒”的,如果沒有非同步程式設計,根本沒法用,非卡死不可。 ES6誕生以前,非同步程式設計的方法,大概有下面四種。 回撥函式事件監聽釋出/訂閱Promise 物件回撥函式 ES6

ES6學習15(非同步操作Async函式

非同步程式設計對JavaScript語言太重要。Javascript語言的執行環境是“單執行緒”的,如果沒有非同步程式設計,根本沒法用,非卡死不可。 ES6誕生以前,非同步程式設計的方法,大概有下面四種。 回撥函式 事件監聽 釋出/訂閱 Promise 物

ECMAScript 6 非同步操作Async函式

眾所周知的,Javascript是一種單執行緒的語言,所有的程式碼必須按照所謂的“自上而下”的順序來執行。本特性帶來的問題就是,一些將來的、未知的操作,必須非同步實現(關於非同步,我會在另一篇文章裡進行討論)。本文將討論一個比較常見的非同步解決方案——Promise,時至本文最後更新的日子,Promise的應

ECMAScript 6 學習筆記----非同步操作Async函式

1.基本概念 Javascript語言的執行環境是“單執行緒”的,如果沒有非同步程式設計,根本沒法用,非卡死不可。 ES6誕生以前,非同步程式設計的方法,大概有下面四種。 回撥函式事件監聽釋出/訂閱Promise 物件 ES6將JavaScript非同步程式設計帶入了

自學-ES6-letconts命令

// ES6嚴格模式 'use strict'; if (true) { function f() {} } // 不報錯並且ES6規定,塊級作用域之中,函式宣告語句的行為類似於let,在塊級作用域之外不可引用。function f() { console.log('I am outside!'); }

自學-ES6-函式的擴充套件

const pipeline = (...funcs) => val => funcs.reduce((a, b) => b(a), val); const plus1 = a => a + 1; const mult2 = a => a * 2; const addThe

ES6-Generator函式async函式

ES6-非同步函式 Generator是一個狀態機,它封裝了多個內部狀態。執行它將生成一個遍歷器物件。 Generator有2個特徵: 宣告時使用function* 函式內部使用yield宣告內部狀態 呼叫函式後,函式並不執行,而且返回的也不是函式

springboot2.0 非同步操作,@Async失效,無法進入非同步

springboot非同步操作可以使用@EnableAsync和@Async兩個註解,本質就是多執行緒和動態代理。 一、配置一個執行緒池   @Configuration @EnableAsync//開啟非同步 public class ThreadPoolConfig { @B

【Vue】ES6,Array.find()findIndex()函式的用法

ES6為Array增加了find(),findIndex函式。 find()函式用來查詢目標元素,找到就返回該元素,找不到返回undefined。 findIndex()函式也是查詢目標元素,找到就返回元素的位置,找不到就返回-1。 他們的都是一個查找回調函式。 [1, 2, 3,

秒殺多執行緒第三 原子操作 Interlocked系列函式

上一篇《多執行緒第一次親密接觸 CreateThread與_beginthreadex本質區別》中講到一個多執行緒報數功能。為了描述方便和程式碼簡潔起見,我們可以只輸出最後的報數結果來觀察程式是否執行出錯。這也非常類似於統計一個網站每天有多少使用者登入,每個使用者登入用一個執

ES6,Array.find()findIndex()函式的用法

ES6為Array增加了find(),findIndex函式。 find()函式用來查詢目標元素,找到就返回該元素,找不到返回undefined。 findIndex()函式也是查詢目標元素,找到就返回元素的位置,找不到就返回-1。 他們的都是一個查找回調函式。

自學-ES6-陣列的擴充套件

Array.of(3, 11, 8) // [3,11,8] Array.of(3) // [3] Array.of(3).length // 1這個方法的主要目的,是彌補陣列建構函式Array()的不足。因為引數個數的不同,會導致Array()的行為有差異。Array() // [] Array(3) //

es6 javascript 非同步操作

非同步程式設計對 JavaScript 語言太重要。 Javascript 語言的執行環境是“ 單執行緒” 的, 如果沒有非同步程式設計, 根本沒法用, 非卡死不可。ES6 誕生以前, 非同步程式設計的方法, 大概有下面四種。回撥函式事件監聽釋出 / 訂閱Promise 物件

Laravel學習-資料庫操作查詢構造器

最近小編在學習號稱世界最牛框架–Laravel。其實學習框架也就是學習框架的思想! 我想在我的部落格中記錄我在laravel學習中的一些心得,歡迎大家關注我的其他Github部落格和簡書,互相交流! 版本:Laravel 5.2 資料庫:m

es6 async函式例項:按順序完成非同步操作

async函式例項:按順序完成非同步操作 實際開發中,經常遇到一組非同步操作,需要按照順序完成。比如,依次遠端讀取一組 URL,然後按照讀取的順序輸出結果。 ES6 Promise 的寫法如下。 function logInOrder(urls){// 遠端讀取所有U

es6非同步之awaitasync非同步

手上的專案寫好的介面已經對接完了。最近閒下來了,上家公司的主管跟我提起了前端和node.js。node必定是以後前端的一項重要技術。這次不再看視訊從基礎開始了,直接看自己需要的東西。 說到非同步就會想到promise,在es6中的await和async也是非同步操作。 a

es6 async函式與其他非同步處理方法的比較

async函式與其他非同步處理方法的比較 我們通過一個例子,來看 async函式與 Promise、Generator函式的比較。 假定某個 DOM 元素上面,部署了一系列的動畫,前一個動畫結束,才能開始後一個。如果當中有一個動畫出錯,就不再往下執行,返回上一個成功執

ES6中的迭代器、Generator函式以及Generator函式非同步操作

最近在寫RN相關的東西,其中涉及到了redux-saga ,saga的實現原理就是ES6中的Generator函式,而Generator函式又和迭代器有著密不可分的關係。所以本篇部落格先學習總結了iterator相關的東西,然後又介紹了Generator相關的內容,最後介紹了使用Generator進行非同步程

MySQL數據庫學習【第二】基本操作存儲引擎

my.cnf 默認 ctu 管理系 int 建立 系統 管理 種類型 一、知識儲備 數據庫服務器:一臺計算機(對內存要求比較高) 數據庫管理系統:如mysql,是一個軟件 數據庫:oldboy_stu,相當於文件夾 表:student,scholl,class_list,相

Python入門(五)之文件操作字符編碼

Python 文件操作和字符編碼 1、文件操作 1、文件操作流程: 打開文件,得到文件句柄並賦值給一個變量===> file = open("yesterday",encoding="utf-8") 通過句柄對文件進行操作 關閉文件 ==> file.close() 1.2、打開文件的