NodeJs並發異步的回調處理


這里說並發異步,並不准確,應該說連續異步。NodeJs單線程異步的特性,直接導致多個異步同時進行時,無法確定最后的執行結果來回調。舉個簡單的例子:

for(var i = 0; i < 5; i++) {
    fs.readFile('file', 'utf-8', function(error, data){});
}

連續發起了5次讀文件的異步操作,很簡單,那么問題來了,我怎么確定所有異步都執行完了呢?因為要在它們都執行完后,才能進行之后的操作。相信有點經驗的同學都會想到使用記數的方式來進行,但如何保證記數正確又是一個問題。仔細想想:

回調是一個函數,每個異步操作時將計數器+1,當每個異步結束時將計數器-1,通過判斷計數器是否為0來確定是否執行回調。這個邏輯很簡單,需要一個相對於執行時和回調時的全局變量作為計數器,而且要在傳給異步方法是執行+1的操作,而且之后將返回一個用來回調的函數,有點繞,不過看看Js函數的高級用法:

var pending = (function() {
    var count = 0;
    return function() {
        count++;
        return function() {
            count--;
            if (count === 0) {
                // 全部執行完畢
            }
        }
    }
});

當pending調用時,即pending(),比如:

var done = pending();

這時計數變量count即被初始化為0,返回的函數附給了done,這時如果執行done(),會是什么?是不是直接執行pending返回的第一個函數,即:pending()(),這個執行又是什么,首先將計數變量count+1,又返回了一個函數,這個函數直接當做callback傳給異步的方法,當執行這個callback的時候,首先是將計數變量count-1,再判斷count是否為0,如果為0即表示所有的異步執行完成了,從而達到連續的異步,同一回調的操作。

關鍵就在兩個return上,簡單的說:

第一個return的函數是將count+1,接着返回需要回調的函數

第二個return的函數就是需要回調的函數,如果它執行,就是將count-1,然后判斷異步是否全部執行完成,完成了,就回調

看個實際點的例子,讀取多個文件的異步回調:

var fileName = ['1.html', '2.html', '3.html'];

var done = pending(function(fileData) {
    console.log('done');
    console.log(fielData);
});

for(var i = 0; i < fileName.lenght; i++) {
    fs.readFile(fileName[i], 'utf-8', done(fileName[i]));
}

其中的done,即用pending方法包起了我們想回調執行的方法,當計數器為0時,就會執行它,那我們得改進一下pending方法:

var pending = (function(callback) {
    var count = 0;
    var returns = {};

    console.log(count);
    return function(key) {
        count++;
        console.log(count);
        return function(error, data) {
            count--;
            console.log(count);
            returns[key] = data;
            if (count === 0) {
                callback(returns);
            }
        }
    }
});

callback即為我們的回調函數,當var done = pending(callback)時,done其實已為第一個return的函數,它有一個參數,可以當做返回的值的下標,所以在循環體中done(fileName[i]),把文件名傳了進去。這個done()是直接執行的,它將count+1后,返回了要傳給異步方法的回調函數,如前面所說,這個回調函數里會根據計數變量來判斷是否執行我們希望執行的回調函數,而且把文件的內容傳給了它,即returns。好了,運行一下,相信能夠准確的看到運行結果。

0
1
2
3
2
1
0
done
{"1.html": "xxx", "2.html": "xxx", "3.html": "xxx"}

從計數上明顯能看出,從0-3再到0,之后就是我們的回調函數輸出了done和文件的內容。

這個問題解決了,我們要思考一下,如何讓這樣的方法封裝重用,不然,每次都寫pending不是很不科學嗎?

下面看看UnJs(我的一個基於NodeJs的Web開發框架)的處理方式,應用於模板解析中的子模板操作:

unjs.asyncSeries = function(task, func, callback) {
    var taskLen = task.length;
    if (taskLen <= 0) {
        return;
    }

    var done = unjs.pending(callback);
    for(var i = 0; i < taskLen; i++) {
        func(task[i], done);
    }
}

asyncSeries有三個參數,意思是:

task: 需要處理的對象,比如需要讀取的文件,它是一個列表,如果不是列表,或列表長度為0,它將不會執行

func: 異步方法,比如fs.readFile,就是通過它傳進去的

callback: 我們希望回調的方法

done和前面同理,它傳給了func,但並沒有執行,因為希望應用端能可控制參數,所以讓應用端去執行。

再看看處理子模板時的操作:

var subTemplate = [];
var patt = /\{\% include \'(.+)\' \%\}/ig;
while(sub = patt.exec(data)) {
    var subs = sub;
    subTemplate.push([subs[0], subs[1]]);
}

unjs.asyncSeries(subTemplate, function(item, callback) {
    fs.readFile('./template/' + item[1], 'utf-8', callback(item[0]));
}, function(data) {
    for(var key in data) {
        html = html.replace(key, data[key]);
    }
});

subTemplate這個列表,是根據對子模板的解析生成的數據,它是一個二維的數組,每個子項的第一個值為子模板的調用文本,即:{% include 'header.html' %}這樣的字符串,第二個參數為子模板文件名,即:header.html

asyncSeries的第二個參數是的callback,實際上是第三個參數,也就是我們希望執行的回調函數經過pending處理的回調方法,如前面所說,在asyncSeries內部,它並沒有運行,而是到這里運行的,即:callback(item[0]),帶上了參數,因為后面還要根據這個參數將父模板中調用子模板的字符串替換為對應子模板的內容。

這樣子,只要需要連續異步時,就可以使用asyncSeries方法來處理了。因為異步的關系,程序的流程有點繞,可能開始不太好理解,即使熟悉了,也有可能突然想不明白,沒關系,比如,第二個參數中的callback實際是第三個參數生成的,開始可能你就會想,這個callback倒底是啥。還有就是pending的兩個return,也是不太好理解的,需要多想想。

好了,連續異步的回調使用Js函數的高級特性完成了。但NodeJs的異步性着實讓程序的控制很成問題,諸如還有連續異步,但要傳值的操作等,這些都是可以通過這樣的思路,變化一下即可實現的。

希望本文對被NodeJs的異步操作搞得頭暈的同學們有些許的幫助。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM