介绍
写过JS代码的同学应该都知道,JS是单线程的,当出现异步逻辑时,就需要使用一些技巧来实现。最常见的方法就是使用回调方法。
回调方法
比如我们要实现一个功能:1s后运行逻辑,再过3s运行另外一段逻辑。使用回调方法可以这样写:
// 方法一 ,嵌套回调// 模拟异步逻辑function delay(time, callback) { setTimeout(function() { callback(time); }, time);}// 过1000ms后输出日志,再过3000ms后输出日志。delay(1000, function(time1) { console.log('%s ms后运行', time1); delay(3000, function(time2) { console.log('%s ms后运行', time2); });});
运行上面的代码,可以得到我们想要的结果:1s后输出了日志,再过3s又输出日志。但是如果逻辑复杂下去,会出现很深的回调方法嵌套问题,使得代码不可维护。为了使异步代码更清晰,就出现了Promise。
Promise
还是拿上面的例子来实现:
// 方法二 promisefunction sleep(time) { return function() { return new Promise((resolve) => setTimeout(resolve, time)); };}sleep(1000)().then(function() { console.log('1000ms后运行');}).then(sleep(3000)).then(function() { console.log('3000ms后运行');});
运行代码后,还是可以得到和使用回调方法实现时一样的效果。分析Promise实现的代码,可以发现,它对嵌套回调进行了改进,将原先横向发展的代码,改成了纵向发展,但是并没有解决本质问题。使用Promise的代码,异步逻辑变成了一堆then方法,语义还是不够清晰。那么有没有更好的写法呢?
在ES6中,提出了generator方法,可以做到使用同步代码来实现异步功能。下面我们来重点介绍下generator。generator
generator介绍
generator是ES6中新提出的函数类型,最大的特点就是函数的执行可以被控制。
举一个最简单的generator例子:function *idMarker() { var i = 1; while(true) { yield i++; }}var ids = idMarker();ids.next().value; // 1ids.next().value; // 2ids.next().value; // 3
上面的例子中,当运行了generator函数idMarker()
后,函数体并没有开始运行,而是直接返回了一个迭代器ids
。当迭代器运行next()
后,会返回第一次运行到yield
或者return
时的返回值以格式{value:1,done:false}
进行返回。其中value就是yield后面的值,done表示当前迭代器还没有结束迭代。从上面的代码可以看出,generator函数的运行过程可以在外边被控制,也就是说使用generator可以做到以下功能的实现:
运行A逻辑;通过yield语句,暂停A,并开始异步逻辑B;等B运行完成,再继续运行A;
generator实现异步逻辑
从上面的例子可以看出,使用generator可以解决Promise的问题,代码如下:
// 方法三 generatorfunction sleep(time) { return new Promise((resolve) => setTimeout(function() { resolve(time); }, time));}function run(gen) { return new Promise(function(resolve, reject) { gen = gen(); onFulfilled(); function onFulfilled(res) { var ret = gen.next(res); next(ret); } function next(ret) { if (ret.done) return resolve(ret.value); ret.value.then(onFulfilled); } });}function *syncFn() { var d1 = yield sleep(1000); console.log('%s ms后运行', d1); var d2 = yield sleep(3000); console.log('%s ms后运行', d2);}run(syncFn);
下面我们来分析下上面代码的逻辑:
首先运行run(syncFn);
, 会运行到gen = gen();
。这个时候gen变成了generator的迭代器。 通过运行onFulfilled
中的gen.next(res)
,代码开始运行syncFn
中的sleep(1000)
。此时,gen.next(res)
会返回syncFn
中yield获得的值,即sleep
方法返回的promise对象,并在next
方法中对该promise设置了then(onFulfilled)
。 也就是说,当sleep(1000)
返回的promise运行结束后,会运行then
中的onFulfilled
。而onFulfilled
会继续运行syncFn
的迭代器。这样子,虽然syncFn
中的异步逻辑,就会逐步执行了。 co
co是一个使用generator和yield来解决异步嵌套的工具库。它的实现类似于上面例子中的run方法。向co传入一个generator方法后,就会开始逐步执行其中的异步逻辑。
举个例子,我们需要读取三个文件并将文件内容依次打印出来。使用co的写法就是:var fs = require('fs');var co = require('co');function readFile(path) { return function (cb) { fs.readFile(path, {encoding: 'utf8'}, cb); };}co(function* () { var dataA = yield readFile('a.js'); console.log(dataA); var dataB = yield readFile('b.js'); console.log(dataB); var dataC = yield readFile('c.js'); console.log(dataC);}).catch(function (err) { console.log(err);});
yield 与 yield*
yield
语句还可以使用yield*
,两则存在细微的差别。举个例子:
// 数组function* GenFunc() { yield [1, 2]; yield* [3, 4]; yield "56"; yield* "78";}var gen = GenFunc();console.log(gen.next().value); // [1, 2]console.log(gen.next().value); // 3console.log(gen.next().value); // 4// generator函数function* Gen1() { yield 2; yield 3;}function* Gen2() { yield 1; yield* Gen1(); yield 4;}var g2 = Gen2();console.log(g2.next().value); // 1console.log(g2.next().value); // 2console.log(g2.next().value); // 3console.log(g2.next().value); // 4// 对象function* GenFunc() { yield {a: '1', b: '2'}; yield* {a: '1', b: '2'};}var gen = GenFunc();console.log(gen.next()); // { value: { a: '1', b: '2' }, done: false }console.log(gen.next()); // TypeError: undefined is not a function
从上面几个例子可以看出,yield 与 yield 的区别在于:yield 只是返回右侧对象的值,而 yield 则将函数委托(delegate)到另一个生成器( Generator)或可迭代的对象(如字符串、数组和类数组 arguments,以及 ES6 中的 Map、Set 等)。也就是说,yield*会逐个调用右侧可迭代对象的next方法。
async await
上面讲完了如何使用generator来解决异步代码的问题,可以看出,使用generator后,异步逻辑的代码基本和同步逻辑的代码差不多了。但是,generator的运行需要co来支持,所有最后又出现了async
和await
。async
和await
其实就是generator的语法糖。举个例子:
// generatorfunction *syncFn() { var d1 = yield sleep(1000); console.log('%s ms后运行', d1); var d2 = yield sleep(3000); console.log('%s ms后运行', d2);}
上面的代码使用async改写就是:
// asyncfunction async syncFn() { var d1 = await sleep(1000); console.log('%s ms后运行', d1); var d2 = await sleep(3000); console.log('%s ms后运行', d2);}
可以看出,async的语法其实就是将generator中的*
替换成async
,将yield
替换成await
。
async和await的优势
有了generator,为什么还要提出async呢?因为async有以下几点优势:
async的语义更清晰
async方法自带执行器,运行时和普通方法一样。而generator的运行依赖于co
await后面的方法可以是任意方法。而co现在了yield后面的方法必须是Promise
总结
总结一下异步代码的发展过程:
【回调函数】最基本的解决方法,将异步结束函数以参数的方式传递到异步函数中,也就是使用回调函数的方式来实现异步逻辑。
【Promise】为了解决回调函数的横向发展问题,定义了Promise。
【generator】Promise虽然解决了异步代码横向发展问题,可是使用Promise语义不够清晰,代码会呈现纵向发展趋势,所以,ES6中出现了generator函数来解决异步代码问题。
【async】generator函数基本上解决了异步代码问题,但是generator函数的运行却被外部控制着。最后提出了async,实现了generator + co的功能,而且语义更加清晰。