异步知多少

前言

异步相关的概念能够参考浅出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」订阅
相关文章
相关标签/搜索