几种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等控制异步流程。<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise文档</a>
还是刚刚那个抢红包的例子,这次用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的异步编程会变得越来越容易,功能也会越来越强大~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容