Node.js異步處理的各類寫法

異步的「坑」

最近一段時間參與開發了一個Node.js後臺項目,做爲一個PHP開發者,上手項目自己並不難,可是開發的過程卻並不順利,不順利的主要緣由在於思路上沒有轉變,沒有從同步的思惟轉換到異步的思惟。html

所謂同步,就是程序(進程/線程)在一個任務的處理過程當中,不會插入處理其餘任務,即便遇到IO等不佔CPU的操做,也會一直等待其結束纔會繼續往下處理。node

所謂異步,就是程序(進程/線程)在一個任務的處理過程當中,會插入處理其餘任務,如遇到IO操做,當前任務會將程序(進程/線程)的控制權釋放給其餘任務,等IO操做結果返回後再繼續往下處理。程序員

簡單地講,同步不會釋放控制權,異步會釋放控制權。編程

衆所周知,Node.js採用的是單線程的異步模型,在具體代碼的寫法上天然和PHP等同步模型不同。在具體項目開發的過程當中,各類異步操做相關的關鍵字層出不窮,如:.then()function* ... yieldasync...await等等。爲了寫一個相似同步的操做,好比:「在執行完A步驟拿到結果以後再執行B步驟」這麼一個簡單的需求,卻要通過大量的反覆調試驗證才能解決。究其緣由,就是對於這些異步操做的場景和關鍵字的含義理解不到位,異步操做所提供的選擇太多了。api

下面就結合代碼實例,理一理這些異步操做方案究竟怎麼使用。promise

異步的各類寫法

任務說明:項目根目錄下有三個文件Jay.txtAngela.txtHenry.txt,依次讀取這三個文件的內容並打印。異步

下面使用各類異步處理的方法來完成此任務。async

回調函數

const fs = require('fs');

fs.readFile('Jay.txt', 'utf8', function (err, data) {
    if (err) throw err;
    console.log(data);
    fs.readFile('Angela.txt', 'utf8', function (err, data) {
        if (err) throw err;
        console.log(data);
        fs.readFile('Henry.txt', 'utf8', function (err, data) {
            if (err) throw err;
            console.log(data);
        });
    });
});

console.log("finish");
一、函數 fs.readFile() 用於異步讀取文件的全部內容,該函數自己沒有返回值。讀取的文件內容異步返回後經過回調函數處理。
二、函數 fs.readFile()的第二個參數是可選參數,若是指定了編碼方式,則返回對應編碼方式的字符串;若是沒有指定,則返回文件的二進制內容,對應類型爲Buffer,能夠經過 buf.toString() 方法轉換成對應的字符串。
三、回調函數的第一個參數必須是 錯誤對象,若是沒有錯誤則錯誤對象的值爲 null

執行程序:函數

$ node 0A_callback_01.js
finish
Hello, I'm Jay.
Hello, I'm Angela.
Hello, I'm Henry.

程序最早返回finish,是由於函數fs.readFile()是異步處理的,在調用後會直接繼續往下處理,在文件內容返回後經過註冊的回調函數處理。學習

串行和並行

一般,人們老是分不清同步異步串行並行以前的區別,樸素地認爲:同步就是串行,異步就是並行。這麼講彷佛對又彷佛不對。

同步異步是從程序(進程/線程)執行方式的角度來看的,文章開頭已經簡單講過同步異步的概念和區別。若是在程序的執行的過程當中不發生任務切換,即:作當前任務的一件事情,等待這件事情完成後,再作當前任務的下一件事情,直到當前任務完成,這種方式就是同步。若是在程序的執行的過程當中發生任務切換,即:作當前任務的一件事情,不等待這件事情作完,直接轉去作其餘任務,再作當前任務的下一件事情,如此往復,直到當前任務完成,這種方式就是異步

串行並行是從任務(事情)的角度來看的,若是多個任務(事情)不能同時作,而是作完一個才能作下一個,則就將這幾個任務(事情)稱做是串行的。若是多個任務(事情)能夠同時作,則就將這幾個任務(事情)稱做是並行的。

同步就是串行這句話在必定程度上是正確的,由於同步程序作完一件事情,纔會作下一件事情,從兩件事情上看,是不會同時作的,因此同步程序只能串行地作事情。

異步就是並行這句話就不是那麼回事了,異步程序能夠選擇串行地作事情,也能夠選擇並行地作事情,是串行作仍是並行作取決於具體的業務場景。對於一個任務下的兩件事情A和B,若是B依賴於A的結果,則須要串行;若是B不依賴於A的結果,則能夠並行。仍是以本文的讀取三個文件爲例,上面的代碼示例就是串行執行的,依次讀取"Jay.txt""Angela.txt""Henry.txt"的內容並打印出來。若是要改爲並行執行該怎麼作呢?簡單作個改造就行,以下。

const fs = require('fs');

fs.readFile('Jay.txt', 'utf8', function (err, data) {
    if (err) throw err;
    console.log(data);
});

fs.readFile('Angela.txt', 'utf8', function (err, data) {
        if (err) throw err;
        console.log(data);
});

fs.readFile('Henry.txt', 'utf8', function (err, data) {
    if (err) throw err;
    console.log(data);
});

console.log("finish");

執行程序:

finish
Hello, I'm Angela.
Hello, I'm Jay.
Hello, I'm Henry.
從結果也能夠看出,因爲三個文件是並行讀取的,因此哪一個先讀完是 隨機的,和代碼寫的順序無關。 按順序寫的代碼就會按順序執行,這是典型的 同步編程思惟,要儘快轉變過來,不然遲早有一天會「翻車」的。

Promise對象

Promise對象可以表示一個異步操做的狀態和結果,使用其提供的.then()方法能夠將多個多個異步操做「串聯」起來,.then()方法自己也返回一個Promise對象。

一樣是按順序讀取三個文件的任務,示例以下:

var readFilePromise = require('fs-readfile-promise');

readFilePromise('Jay.txt', 'utf8')
    .then(function(data) {
        console.log(data);
    })
    .then(function() {
        return readFilePromise('Angela.txt', 'utf8');
    })
    .then(function(data) {
        console.log(data);
    })
    .then(function() {
        return readFilePromise('Henry.txt', 'utf8');
    })
    .then(function(data) {
        console.log(data);
    })
    .catch(function(err) {
        console.log(err);
    });

console.log("finish");

執行程序:

$ node 0B_promise_01.js
finish
Hello, I'm Jay.
Hello, I'm Angela.
Hello, I'm Henry.

Promise對象有pending(初始)fulfilled(成功)rejected(失敗)三種狀態。當異步操做成功時,Promise對象從pending狀態變爲fulfilled狀態,並將成功結果傳遞給.then()方法的第一個參數(也叫onfulfilled函數);當異步操做失敗時,Promise對象從pending狀態變爲rejected狀態,並將失敗信息傳遞給.then()方法的第二個參數(也叫onrejected函數),若是沒有指定第二個參數,則將失敗信息傳遞給.catch()方法的參數(一樣也叫onrejected函數)。

上面的程序,能夠將上個文件的處理和下個文件的讀取合併到一個.then()當中,示例以下:

var readFilePromise = require('fs-readfile-promise');

readFilePromise('Jay.txt', 'utf8')
    .then(function(data) {
        console.log(data);
        return readFilePromise('Angela.txt', 'utf8');
    })
    .then(function(data) {
        console.log(data);
        return readFilePromise('Henry.txt', 'utf8');
    })
    .then(function(data) {
        console.log(data);
    })
    .catch(function(err) {
        console.log(err);
    });

console.log("finish");
.then()當中,能夠返回一個Promise對象,能夠返回一個基礎類型的值(數字、字符串、布爾值),也能夠什麼都不返回(直接 return;),甚至連 return語句均可以省略。這幾種場景下的處理方式參考: .then()方法的 返回值說明

Generator函數

Generator函數(生成器函數)使用 function* 關鍵字定義,函數中使用yield關鍵字進行流程控制,yield後面能夠跟任何表達式(普通同步表達式、Promise對象、Generator函數)。須要特別注意的是,yield關鍵字必須放在Generator函數當中,不然運行時會報錯!

Generator函數的返回值叫作 Generator對象(生成器對象),Generator對象有一個 .next() 方法,每執行一次.next()方法,就會迭代執行至Generator函數的下一個yield語句位置,並返回一個對象。該對象包含兩個屬性:valuedonevalue存儲了yield後面表達式的值;done是一個布爾值,表示Generator函數是否執行完畢。

一樣是按順序讀取三個文件的任務,示例以下:

var readFilePromise = require('fs-readfile-promise');

function* generator() {
    yield readFilePromise('Jay.txt', 'utf8');
    yield readFilePromise('Angela.txt', 'utf8');
    yield readFilePromise('Henry.txt', 'utf8');
}

let gen = generator();

gen.next().value.then(function(data) {
    console.log(data);
    gen.next().value.then(function(data) {
        console.log(data);
        gen.next().value.then(function(data) {
            console.log(data);
            gen.next();  // 返回:{ value: undefined, done: true },表示生成器函數執行結束
        });
    });
});

console.log("finish");

執行程序:

$ node 0C_generator_01.js
finish
Hello, I'm Jay.
Hello, I'm Angela.
Hello, I'm Henry.

本例中,generator()是一個生成器函數,其返回值gen是一個生成器對象。gen.next()返回的對象結構以下。

{ value: Promise { <pending> }, done: false }

其中,gen.next().value是一個Promise,表示yield後面的readFilePromise()函數所返回的是一個Promise對象。

須要注意的是,生成器函數自己包含的各個異步操做並不能按照順序串行執行,想要實現串行執行的話,仍是須要配合Promise對象及其.then()函數來實現,如本例所示。

co函數庫

co函數庫是幹什麼的?co函數庫是Generator函數的一種執行器。簡單來說,co函數庫用來將上一節中手動執行Generator函數的過程自動化,這樣一來,就使得採用同步思惟寫異步代碼的想法成爲現實。做爲曾經是徹底同步思惟的程序員終於看到了曙光。

一樣是按順序讀取三個文件的任務,示例以下:

var co = require('co');
var readFilePromise = require('fs-readfile-promise');

// generator()是一個生成器函數
function* generator() {
    let data = yield readFilePromise('Jay.txt', 'utf8');
    console.log(data);

    data = yield readFilePromise('Angela.txt', 'utf8');
    console.log(data);

    data = yield readFilePromise('Henry.txt', 'utf8');
    console.log(data);
}

let gen = generator();  // gen是一個生成器對象
co(generator()).then(function() {
    console.log('Generator function is finished!');
});

console.log("finish");

執行程序:

$ node 0D_co_01.js
finish
Hello, I'm Jay.
Hello, I'm Angela.
Hello, I'm Henry.
Generator function is finished!
固然,用好 co的前提是有一些注意事項須要知道的:
一、 co函數配套使用的 Generator函數中, yield後面的異步操做須要返回一個Promise對象,不然就沒法實現指望的 同步效果;
二、 co函數自己會返回一個Promise對象,因此如本例所示,是能夠在其後使用 .then()方法增長回調函數的。

async函數

co庫函數已經將Generator函數的執行簡化了不少,還能更簡單一點嗎?答案是:有,那就是async函數。

async函數與Generator函數相比,能夠簡單地理解爲:將Generator函數中的*改成async,將yield改成await,就成了async函數

async函數與Generator函數相比:

  • async函數自己內置了執行器,無需再像Generator函數同樣須要引入額外的執行器(如:co執行器);
  • async...awaitfunction*...yield相比,語義更加清晰明瞭:async表示函數中有異步操做,await表示須要等待異步操做返回結果;
  • await後面除了能夠跟Promise對象以外,也能夠跟基礎類型的值,如:數字、字符串、布爾值,而yield後面必需要跟Promise對象;
  • async函數的返回值也是

一樣是按順序讀取三個文件的任務,示例以下:

var readFilePromise = require('fs-readfile-promise');

async function asyncReadFile() {
    let data = await readFilePromise('Jay.txt', 'utf8');
    console.log(data);

    data = await readFilePromise('Angela.txt', 'utf8');
    console.log(data);

    data = await readFilePromise('Henry.txt', 'utf8');
    console.log(data);

    return "Async function is finished!"
}

asyncReadFile().then(function(data) {
    console.log(data);
});

console.log("finish");

執行程序:

$ node 0E_async_await_01.js
finish
Hello, I'm Jay.
Hello, I'm Angela.
Hello, I'm Henry.
Async function is finished!
從本例能夠看出,除了 asyncawait這兩個關鍵字以外,總體代碼的格式與函數調用方式和同步代碼徹底同樣。因而可知, 異步代碼寫法的終極目標就是 讓異步代碼寫起來和同步代碼同樣簡單方便。然而,悲催的是,做爲一個JS新人須要花很久才能把這些逐漸弄清楚,學習成本不可謂不高。
相關文章
相關標籤/搜索