JS异步解决方案
前言
异步最早的解决方案是回调函数,如ajax,事件的回调,setinterval/settimeout中的回调。但是回调函数有回调地狱的问题;
为了解决回调地狱的问题,社区提出了promise解决方案,es6将其写进了语言标准。promise一定程度上解决了回调地狱的问题,但是promise也存在一些问题,如错误不能被try catch,而且使用promise的链式调用,其实并没有从根本上解决回调地狱的问题,只是换了一种写法。
es6中引入 generator 函数,generator是一种异步编程解决方案,generator 函数是协程在 es6 的实现,最大特点就是可以交出函数的执行权,generator 函数可以看出是异步任务的容器,需要暂停的地方,都用yield语句注明。但是 generator 使用起来较为复杂。
es7又提出了新的异步解决方案:async/await,async是 generator 函数的语法糖,async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步逻辑的代码看起来像同步一样。
同步和异步
关于同步和异步的概念,这里给出阮一峰老师的文章参考。其中也不乏一些异步的解决办法例子,如《js异步编程的4种方法》,。
异步解决方案
回调函数
1 // node读取文件 2 fs.readfile('url', 'utf-8', function(err, data) { 3 // code 4 });
使用的场景有:ajax请求,事件回调函数,node api,定时器等。
优点: 简单。 缺点: 异步回调嵌套会导致代码难以维护,并且不方便统一处理错误,不能 try,catch
和存在回调地狱问题。
// 回调地狱问题,以读取a文本内容,再根据a文本内容读取b再根据b的内容读取c..为例 fs.readfile('a', 'utf-8', function(err, data) { fs.readfile('b', 'utf-8', function(err, data) { fs.readfile('c', 'utf-8', function(err, data) { fs.readfile('d', 'utf-8', function(err, data) { // code }); }); }); });
promise
promise 一定程度上解决了回调地狱的问题,promise 最早由社区提出和实现,es6 将其写进了语言标准,统一了用法,原生提供了promise对象。《ecmascript 6 入门》
我们来观察一下promise是如何解决回调地狱问题的。
function read(url) { return new promise((resolve, reject) => { fs.readfile(url, 'utf-8', function(err, data) { if (err) reject(err); resolve(data); }); }); } read('a').then(data => { return read('b'); }).then(data => { return read('c'); }).then(data => { return read('d'); }).catch(err => {
console.log(err);
});
promise的优点: 1.一旦状态改变,就不会再变,任何时候都可以得到这个结果。2.可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
promise的缺点:1.无法取消 promise。 2.当处于pending状态时,无法得知目前进展到哪一个阶段。 3.错误不能被 trycatch
这里举一个在promise之前,读取a,b,c三个文件内容,都读取成功后,再输出最终的结果例子。依赖于发布订阅模式
let pubsup = { arry: [], emit: function() { this.arry.foreach(fn => fn()); }, on: function(fn) { this.arry.push(fn); } }; let data = []; pubsub.on(() => { if (data.length == 3) { console.log(data); } }); fs.readfile('a', 'utf-8', function(err, data) { data.push(data); pubsup.emit(); }); fs.readfile('b', 'utf-8', function(err, data) { data.push(data); pubsup.emit(); }); fs.readfile('c', 'utf-8', function(err, data) { data.push(data); pubsup.emit(); });
顺便提一下朴灵的深入浅出nodejs里面介绍不同场景下异步获取数据进行操作的实践,非常值得看。
promise给我们提供了 promise.all
的方法,对于这个需求,我们可以使用 promise.all
来实现。
/** * 将 fs.readfile 包装成promise接口 */ function read(url) { return new promise((resolve, reject) => { fs.readfile(url, 'utf-8', function(err, data) { if (err) reject(err); resolve(data); }); }); } /** * promise.all 可以实现多个异步并行执行,同一时刻获取最终结果的问题 */ promise.all([ read(a), read(b), read(c) ]).then(data => { console.log(data); }).catch(err => console.log(err));
generator
generator 函数是 es6 提供的一种异步编程解决方案,整个 generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。
generator 函数一般配合 yield 或 promise 使用。generator函数返回的是迭代器。对生成器和迭代器不了解的同学参考下阮一峰的es6入门
function* generator() { let a = yield 111; console.log(a); let b = yield 222; console.log(b); let c = yield 333; console.log(c); let d = yield 444; console.log(d); } let t = generator(); // next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值 t.next(1); // 第一次调用next函数时,传递的参数无效 t.next(2); // 输出2 t.next(3); // 3 t.next(4); // 4 t.next(5); // 5
仍然以上文的 readfile (先读取a文本内容,再根据a文本内容读取b再根据b的内容读取c)为例,使用 generator + co库来实现:
const fs = require('fs'); const co = require('co'); const bluebird = require('bluebird'); const readfile = bluebird.promiseify(fs.readfile); function* read() { yield readfile('a', 'utf-8'); yield readfile('b', 'utf-8'); yield readfile('c', 'utf-8'); } co(read()).then(data => { // code }).catch(err => { // code });
await/async
es7中引入了 async/await 概念。async 其实是一个语法糖,它的实现就是将 generator函数和自动执行器(co),包装在一个函数中。
async/await 的优点是代码清晰,不用像 promise 写很多 then 链,就可以处理回调地狱的问题。并且错误可以被try catch。
仍然以上文的readfile (先读取a文本内容,再根据a文本内容读取b再根据b的内容读取c) 为例,使用 async/await 来实现:
const fs = require('fs'); const co = require('co'); const bluebird = require('bluebird'); const readfile = bluebird.promiseify(fs.readfile); async function read() { await readfile('a', 'utf-8'); await readfile('b', 'utf-8'); await readfile('c', 'utf-8'); } co(read()).then(data => { // code }).catch(err => { // code });
我们用async/await来实现同样的效果。
function read(url) { return new promise((resolve, reject) => { fs.readfile(url, 'utf-8', function(err, data) { if (err) reject(err); resolve(data); }) }); } async function readasync() { const data = await promise.all([ read(a), read(b), read(c) ]); return data; } readsync().then(data => { console.log(data); })
所以js的异步发展史,可以认为是从 callback -> promise -> generator -> async/await。async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步逻辑的代码看起来像同步一样