几种js异步编程方法

前言

JS需要异步处理的地方实在是比较多,比如定时器/ajax/io操作等等,在当今前端技术日新月异的情况下,异步编程成了核心技能之一,在这里我只是罗列一下几种我用过的异步编程方式并稍加对比。本次编写的代码全部在node 7+版本中运行

同步和异步

首先我们要弄清同步和异步到底是个什么玩意儿,其实我的理解就是他们对代码的“执行顺序”控制程度不一样。为什么这样说呢?因为同步在一段代码调用之后,是不管有没有结果返回的,立马就执行到下一步去了。而异步,是会等待那个调用的,直到返回了结果再往下执行。
举个例子:假设有个抢红包的调用,它是需要一段时间才能满足抢红包结束的

var result = function(){
    if(抢红包结束) return 5
}
console.log(result())

如果是同步,这段代码就不管result的死活了直接往下走,输出undefined,如果写成异步风格的代码,那就不一样了。

回调函数

在前端的远古时代,回调是处理异步的不二选择,为什么,因为它的写法简单,没有多余的api。就拿刚刚那个抢红包的例子来说,我用一个定时器替代它:

var result = function(){
    setTimeout(()=>{
        return 5;
    },1000)
}
console.log(result())

用回调函数处理怎么弄呢?很简单,让result的参数为一个回调函数就可以了,于是代码变成下面这样

var result = function(callback){
    setTimeout(()=>{
        callback(5)
    },1000)
}
result(console.log)

现在我们用一个真实的io调用替代抢红包,新建一个numbers.txt,在里面写若干个红包金额,代码如下:

const fs = require('fs');

const readFileAsArray = function (file, cb) {
    fs.readFile(file, (err, data) => {
        if (err) return cb(err);
        const lines = data.toString().trim().split('\n');
        cb(null, lines);
    })
}

readFileAsArray('./numbers.txt', (err, lines) => {
    if (err) throw err;
    const numbers = lines.map(Number);
    console.log(`分别抢到了${numbers}块红包`);
})

代码输出为:

>分别抢到了10,11,12,13,14,15块红包

从代码中我们可以看到,定义了一个readFileAsArray函数,传两个参:文件名和回调函数,然后调用这个函数,把回调函数写入第二个参数里,就可以控制代码执行顺序了。
不过,回调的缺点就是写多了,层层嵌套,又会造成回调地狱的坑爹情况,代码变得难以维护和阅读。所以我们需要更好的解决办法。

Promise

借用ydjs的一句话:Promise实现了控制反转。什么意思呢?原来这个顺序的控制是在代码那边而不是程序员控制,现在有了Promise,控制权就由人来掌握了,通过一系列Promise的方法如then/catch/all/race等控制异步流程。Promise文档
还是刚刚那个抢红包的例子,这次用Promise来写就是这样的:

const fs = require('fs');

const readFileAsArray = function (file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, (err, data) => {
            if (err) {
                reject(err);
            }
            const lines = data.toString().split('\n');
            resolve(lines);
        })
    })
}

readFileAsArray('./numbers.txt').then(
    lines => {
        const numbers = lines.map(Number);
        console.log(`分别抢到了${numbers}块红包`);
    }
).catch(error => console.error(error));

结果和使用回调函数一样,但是在这里已经把控制权交给了程序员,代码也变得更好理解。虽然Promise有单值/不可取消等缺点,不过在现在大部分的情况下实现异步还是够用的。想深入了解的朋友可以去看看《你不知道的JS》中卷第三章。

await/async

Promise的api太多了,有没有简化的办法呢?答案是肯定有的,ES7推出了一个语法糖:await/async,它的内部封装了Promise和Generator的组合使用方式,至于Generator是什么,这里不再赘述,有兴趣的朋友们可以去自行研究。
于是,刚刚那段代码就变成了:

const fs = require('fs');

const readFileAsArray = function (file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, (err, data) => {
            if (err) {
                reject(err);
            }
            const lines = data.toString().split('\n');
            resolve(lines);
        })
    })
}

async function result() {
    try {
        const lines = await readFileAsArray('./numbers.txt');
        const numbers = lines.map(Number);
        console.log(`分别抢到了${numbers}块红包`);
    } catch (err) {
        console.log("await出错!");        
        console.log(err);
    }
}

result();

这样做的结果是不是让代码可读性更高了!而且也屏蔽了Promise和Generator的细节。

event

另一个实现异步的方式是event,回调(promise、await/async)和event的关系就像计划经济和市场经济一样,一个是人为的强制性的控制,一个是根据需求和供给这只看不见的手控制。
还是同一个例子,用event写就是这样:

const EventEmitter = require('events');
const fs = require('fs');
class MyEventEmitter extends EventEmitter {
    executeAsy(asyncFunc, args) {
        this.emit("开始");
        console.time('执行耗时');
        asyncFunc(args, (err, data) => {
            if (err) return this.emit('error', err);
            this.emit('data', data);
            console.timeEnd('执行耗时');
            this.emit("结束");
        });
    }
}

const myEventEmitter = new MyEventEmitter();

myEventEmitter.on('开始', () => {
    console.log('开始执行了');
})
myEventEmitter.on('data', (data) => {
    console.log(`分别抢到了${data}块红包`);
})
myEventEmitter.on('结束', () => {
    console.log('结束执行了');
})
myEventEmitter.on('error', (err) => {
    console.error(err);
})

myEventEmitter.executeAsy(fs.readFile, './numbers.txt');

这种事件驱动非常灵活,也不刻意去控制代码的顺序,一旦有事件的供给(emit),它就会立刻消费事件(on),不过正是因为这样,它的缺点也很明显:让程序的执行流程很不清晰。

event+promise+await/async

纯粹的计划经济也不好,纯粹的市场经济也不好。好的方式是什么?当然是结合起来啦!
所以就有了结合event和promise的写法:

const EventEmitter = require('events');
const fs = require('fs');
class MyEventEmitter extends EventEmitter {
    async executeAsy(asyncFunc, args) {
        this.emit("开始");
        try {
            console.time('执行耗时');
            const data = await asyncFunc(args);
            this.emit('data', data);
            console.timeEnd('执行耗时');
            this.emit('结束');
        } catch (err) {
            console.log("出错了!");
            this.emit('error', err);
        }

    }
}

const readFileAsArray = function (file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, (err, data) => {
            if (err) {
                reject(err);
            }
            const lines = data.toString().split('\r\n');
            resolve(lines);
        })
    })
}
const myEventEmitter = new MyEventEmitter();

myEventEmitter.on('开始', () => {
    console.log('开始执行了');
})
myEventEmitter.on('data', (data) => {
    console.log(`分别抢到了${data}块红包`);
})
myEventEmitter.on('结束', () => {
    console.log('结束执行了');
})
myEventEmitter.on('error', (err) => {
    console.error(err);
})

myEventEmitter.executeAsy(readFileAsArray, './numbers.txt');

这种结合的方式基本上可以应付现今的异步场景了,缺点嘛。。。就是代码量比较多

rxjs

js越发壮大,jser们终于站起来了,看着其他语言使用着rx这个强大的工具,我们怎么能少,一种大一统管理异步的方案:rxjs就这样来到了世上。
简单介绍下rxjs和异步的关系:它可以把数据转化成一股流,无论这个数据是同步得到的还是异步得到的,是单值还是多值。
比如用Rx.Observable.of来包装单值同步数据,
用Rx.Observable.of来包装单值同步数据,
用Rx.Observable.fromPromise来包装单值异步数据,
以及用Rx.Observable.fromEvent来包装多值异步数据:

const fs = require('fs');
const Rx = require('rxjs');
const EventEmitter = require('events');

class MyEventEmitter extends EventEmitter {
    async executeAsy(asyncFunc, args) {
        this.emit("开始");
        try {
            console.time('执行耗时');
            const data = await asyncFunc(args);
            this.emit('data', data);
            console.timeEnd('执行耗时');
            this.emit('结束');
        } catch (err) {
            console.log("出错了!");
            this.emit('error', err);
        }

    }
}

const readFileAsArray = function (file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, (err, data) => {
            if (err) {
                reject(err);
            }
            const lines = data.toString().split('\r\n');
            resolve(lines);
        })
    })
}
const myEventEmitter = new MyEventEmitter();

myEventEmitter.executeAsy(readFileAsArray, './numbers.txt');

let dataObservable = Rx.Observable.fromEvent(myEventEmitter, 'data')

let subscription = dataObservable.subscribe((data) => {
    console.log(`分别抢到了${data}块红包`);
}, err => {
    console.error(err);
}, compelete => {
    console.info("compelete!");
})

rxjs还有很多重要的概念,比如生产者Observe和消费者Observable、推拉模型、各种方便的操作符和函数式编程等等

关于异步的未来展望

ES8已经着手Observable和Observe的实现了,node也在着手异步生命周期钩子Async Hooks来方便程序们来调试异步程序,我相信,未来js的异步编程会变得越来越容易,功能也会越来越强大~

你可能感兴趣的:(几种js异步编程方法)