異步知多少

前言

異步相關的概念能夠參考淺出js異步事件。Javascript單線程的機制帶來的好處就是在代碼運行時能夠確保代碼訪問的變量不會受到其它線程的干擾。試想若是當你遍歷一個數組的時候,另一個線程修改了這個數組,那就亂了套了。setTimeout/setInterval, 瀏覽器端的ajax, Node裏的IO等的運用都是創建在正確的理解異步(e.g. Event loop, Event queue)的基礎上。javascript

異步循環

假設我有一個含文件名的數組,我想依次讀取文件直到第一次成功讀取某文件,返回文件內容。也就是若是含文件名的數組是['a.txt', 'b.txt'],那就先讀a.txt,若是成功返回a.txt內容。讀取失敗的話讀b.txt。依此類推。讀文件的話Node分別提供了同步方法readFileSync跟異步方法readFilehtml

假設咱們有2個文件:a.txt(文件內容也爲a.txt)跟b.txt(文件內容也爲b.txt)。java

同步的寫法比較簡單:node

let fs = require('fs'),
    path = require('path');

function readOneSync(files) {
    for(let i = 0, len = files.length; i < len; i++) {
        try {
            return fs.readFileSync(path.join(__dirname, files[i]), 'utf8');
        } catch(e) {
            //ignore
        }
    }
    throw new Error('all fail');
}

console.log(readOneSync(['a.txt', 'b.txt'])); //a.txt
console.log(readOneSync(['filenotexist', 'b.txt'])); //b.txt複製代碼

同步寫法最大的問題就是會阻塞事件隊列裏的其它事件處理。假設讀取的文件很是大耗時久,會致使app在此期間無響應。異步IO的話能夠有效避免這個問題。可是須要在回調裏處理調用的順序(i.e. 在上一個文件讀取的回調裏進行是否讀取下一個文件的判斷和操做)。git

let fs = require('fs'),
    path = require('path');

function readOne(files, cb) {
    function next(index) {
        let fileName = files[index];
        fs.readFile(path.join(__dirname, fileName), 'utf8', (err, data) => {
            if(err) {
                return next(index + 1);
            } else {
                return cb(data);
            }
        });
    }
    next(0);
}

readOne(['a.txt', 'b.txt'], console.log); //a.txt
readOne(['filenotexist', 'b.txt'], console.log); //b.txt複製代碼

異步的寫法須要傳一個回調函數(i.e. cb)用來對返回結果進行操做。同時定義了一個方法next用來在讀取文件失敗時遞歸調用本身(i.e. next)讀取下一個文件。github

同時發起多個異步請求

假設如今我有一個含文件名的數組,我想同時異步讀取這些文件。所有讀取成功時調用成功回調。任意一個失敗的話調用失敗回調。ajax

let fs = require('fs'),
    path = require('path');

function readAllV1(files, onsuccess, onfail) {
    let result = [];
    files.forEach(file => {
        fs.readFile(path.join(__dirname, file), 'utf8', (err, data) => {
            if(err) {
                onfail(err);
            } else {
                result.push(data);
                if(result.length === files.length) {
                    onsuccess(result);
                }
            }
        });
    });
}

readAllV1(['a.txt', 'b.txt'], console.log, console.log); //結果不肯定性複製代碼

這裏有個問題。由於讀取文件的操做是同時異步觸發的,取決於文件的讀取時間,早讀完的文件的handler會被先放入事件隊列裏。這會致使最後result數組裏的內容跟files的文件名並不是對應的。舉個例子, 假設files是['a.txt', 'b.txt'], a.txt是100M, b.txt是10kb, 2個同時異步讀取,由於b.txt比較小因此先讀完了,這時候b.txt對應的readFile裏的回調在事件隊列裏的順序會先於a.txt的。當讀取b.txt的回調運行時,result.push(data)會把b.txt的內容先塞入result中。最後返回的result就會是[${b.txt的文件內容}, ${a.txt的文件內容}]。當對返回的結果有順序要求的時候,咱們能夠簡單的修改下:api

let fs = require('fs'),
    path = require('path');

function readAllV2(files, onsuccess, onfail) {
    let result = [];
    files.forEach((file, index) => {
        fs.readFile(path.join(__dirname, file), 'utf8', (err, data) => {
            if(err) {
                onfail(err);
            } else {
                result[index] = data;
                if(result.length === files.length) {
                    onsuccess(result);
                }
            }
        });
    });
}

readAllV2(['a.txt', 'b.txt'], console.log, console.log); //結果不肯定性複製代碼

看起來好像是木有問題了。可是!數組

let arr = [];
arr[1] = 'a';
console.log(arr.length); //2複製代碼

按照readAllV2的實現,假設在a.txt還未讀完的時候,b.txt先讀完了,咱們設了result[1] = data。這時候if(result.length === files.length)是true的,直接就調用了成功回調。。因此咱們不能依賴於result.length來作檢查。瀏覽器

let fs = require('fs'),
    path = require('path');

function readAllV3(files, onsuccess, onfail) {
    let result = [], counter = 0;
    files.forEach((file, index) => {
        fs.readFile(path.join(__dirname, file), 'utf8', (err, data) => {
            if(err) {
                onfail(err);
            } else {
                result[index] = data;
                counter++;
                if(counter === files.length) {
                    onsuccess(result);
                }
            }
        });
    });
}

readAllV3(['a.txt', 'b.txt'], console.log, console.log); //[ 'a.txt', 'b.txt' ]複製代碼

若是對Promise比較熟悉的話,Promise裏有個Promise.all實現的就是這個效果。

同步跟異步回調函數不要混用,儘可能保持接口的一致性

假設咱們實現一個帶緩存的讀取文件方法。當緩存裏沒有的時候咱們去異步讀取文件,有的話直接從緩存裏面取。

let fs = require('fs'),
    path = require('path'),
    cache = {};

function readWithCacheV1(file, onsuccess, onfail) {
    if(cache[file]) {
        onsuccess(cache[file]);
    } else {
       fs.readFile(path.join(__dirname, file), 'utf8', (err, data) => {
           if(err) {
               onfail(err);
           } else {
               cache[file] = data;
               onsuccess(data);
           }
       });
    }
}複製代碼

具體看下上面的實現:

  • 當緩存裏有數據時,是同步進行調用了成功回調onsuccess。
cache['a.txt'] = 'hello'; //mock一下緩存裏的數據
readWithCacheV1('a.txt', console.log);//同步調用,要等調用完後才進入下一個statement
console.log('after you');

//輸出結果:
hello
after you複製代碼
  • 當緩存沒有數據時,是異步調用。
readWithCacheV1('a.txt', console.log);//緩存沒數據。異步調用
console.log('after you');

//輸出結果:
after you
hello複製代碼

這就形成了不一致性, 程序的執行順序不可預測容易致使bug車禍現場。要保持一致性的話能夠統一採起異步調用的形式,用setTimeout包裝下。

let fs = require('fs'),
    path = require('path'),
    cache = {};

function readWithCacheV2(file, onsuccess, onfail) {
    if(cache[file]) {
        setTimeout(onsuccess.bind(null, cache[file]),0);
    } else {
       fs.readFile(path.join(__dirname, file), 'utf8', (err, data) => {
           if(err) {
               onfail(err);
           } else {
               cache[file] = data;
               onsuccess(data);
           }
       });
    }
}複製代碼

從新跑下有緩存跟沒有緩存2種狀況:

  • 當緩存裏有數據時,經過setTimeout異步調用
    ```javascript
    cache['a.txt'] = 'hello';
    readWithCacheV2('a.txt', console.log);
    console.log('after you');

//輸出結果:
after you
hello

* 當緩存沒有數據時,

```javascript
readWithCacheV2('a.txt', console.log);
console.log('after you');

//輸出結果:
after you
hello複製代碼

Reference

Code

Notice

  • 若是您以爲該Repo讓您有所收穫,請「Star 」支持樓主。
  • 若是您想持續關注樓主的最新系列文章,請「Watch」訂閱
相關文章
相關標籤/搜索