ES6中的异步详解
异步
setTimeout/setInterval
、事件实现。传统的异步实现
作为一个前端开发者,无论是浏览器端还是Node,相信大家都使用过事件吧,通过事件肯定就能想到回调函数,它就是实现异步最常用、最传统的方式。
不过要注意,不要以为回调函数就都是异步的,如ES5的数组方法Array.prototype.forEach((ele) => {})
等等,它们也是同步执行的。回调函数只是一种处理异步的方式,属于函数式编程中高阶函数的一种,并不只在处理异步问题中使用。
举个栗子?:
- // 最常见的ajax回调
- this.ajax('/path/to/api', {
- params: params
- }, (res) => {
- // do something...
- })
你可能觉得这样并没有什么不妥,但是若有多个ajax或者异步操作需要依次完成呢?
- this.ajax('/path/to/api', {
- params: params
- }, (res) => {
- // do something...
- this.ajax('/path/to/api', {
- params: params
- }, (res) => {
- // do something...
- this.ajax('/path/to/api', {
- params: params
- }, (res) => {
- // do something...
- })
- ...
- })
- })
回调地狱就出现了。。。?
为了解决这个问题,社区中提出了Promise方案,并且该方案在ES6中被标准化,如今已广泛使用。
Promise
使用Promise的好处就是让开发者远离了回调地狱的困扰,它具有如下特点:
- 对象的状态不受外界影响:
- Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。
- 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
- Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。
- 只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。
- 如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。
- 这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
- 一旦声明Promise对象(new Promise或Promise.resolve等),就会立即执行它的函数参数,若不是函数参数则不会执行
- this.ajax('/path/to/api', {
- params: params
- }).then((res) => {
- // do something...
- return this.ajax('/path/to/api', {
- params: params
- })
- }).then((res) => {
- // do something...
- return this.ajax('/path/to/api', {
- params: params
- })
- })
- ...
看起来就直观多了,就像一个链条一样将多个操作依次串了起来,再也不用担心回调了~?
同时Promise还有许多其他API,如Promise.all
、Promise.race
、Promise.resolve/reject
等等(可以参考阮老师的文章),在需要的时候配合使用都是极好的。
API无需多说,不过这里我总结了一下自己之前使用Promise踩到的坑以及我对Promise理解不够透彻的地方,希望也能帮助大家更好地使用Promise:
1.then的返回结果:我之前天真的以为then
要想链式调用,必须要手动返回一个新的Promise才行
- Promise.resolve('first promise')
- .then((data) => {
- // return Promise.resolve('next promise')
- // 实际上两种返回是一样的
- return 'next promise'
- })
- .then((data) => {
- console.log(data)
- })
总结如下:
- 如果
then
方法中返回了一个值,那么返回一个“新的”resolved的Promise,并且resolve回调函数的参数值是这个值 - 如果
then
方法中抛出了一个异常,那么返回一个“新的”rejected状态的Promise - 如果
then
方法返回了一个未知状态(pending)的Promise新实例,那么返回的新Promise就是未知状态 - 如果
then
方法没有返回值时,那么会返回一个“新的”resolved的Promise,但resolve回调函数没有参数
2.一个Promise可设置多个then回调,会按定义顺序执行,如下
- const p = new Promise((res) => {
- res('hahaha')
- })
- p.then(console.log)
- p.then(console.warn)
这种方式与链式调用不要搞混,链式调用实际上是then方法返回了新的Promise,而不是原有的,可以验证一下:
- const p1 = Promise.resolve(123)
- const p2 = p1.then(() => {
- console.log(p1 === p2)
- // false
- })
3.then
或catch
返回的值不能是当前promise本身,否则会造成死循环:
- const promise = Promise.resolve()
- .then(() => {
- return promise
- })
4.then
或者catch
的参数期望是函数,传入非函数则会发生值穿透:
- Promise.resolve(1)
- .then(2)
- .then(Promise.resolve(3))
- .then(console.log)
- // 1
5.process.nextTick
和promise.then
都属于microtask,而setImmediate
、setTimeout
属于macrotask
- process.nextTick(() => {
- console.log('nextTick')
- })
- Promise.resolve()
- .then(() => {
- console.log('then')
- })
- setImmediate(() => {
- console.log('setImmediate')
- })
- console.log('end')
- // end nextTick then setImmediate
有关microtask及macrotask可以看这篇文章,讲得很细致。
但Promise也存在弊端,那就是若步骤很多的话,需要写一大串.then()
,尽管步骤清晰,但是对于我们这些追求极致优雅的前端开发者来说,代码全都是Promise的API(then
、catch
),操作的语义太抽象,还是让人不够满意呀~
Generator
Generator是ES6规范中对协程的实现,但目前大多被用于异步模拟同步上了。
执行它会返回一个遍历器对象,而每次调用next
方法则将函数执行到下一个yield
的位置,若没有则执行到return或末尾。
依旧是不再赘述API,对它还不了解的可以查阅阮老师的文章。
通过Generator实现异步:
- function* main() {
- const res = yield getData()
- console.log(res)
- }
- // 异步方法
- function getData() {
- setTimeout(() => {
- it.next({
- name: 'yuanye',
- age: 22
- })
- }, 2000)
- }
- const it = main()
- it.next()
先不管下面的next
方法,单看main
方法中,getData
模拟的异步操作已经看起来很像同步了。但是追求完美的我们肯定是无法忍受每次还要手动调用next
方法来继续执行流程的,为此TJ大神为社区贡献了co模块来自动化执行Generator,它的实现原理非常巧妙,源码只有短短的200多行,感兴趣可以去研究下。
- const co = require('co')
-
- co(function* () {
- const res1 = yield ['step-1']
- console.log(res1)
- // 若yield后面返回的是promise,则会等待它resolved后继续执行之后的流程
- const res2 = yield new Promise((res) => {
- setTimeout(() => {
- res('step-2')
- }, 2500)
- })
- console.log(res2)
- return 'end'
- }).then((data) => {
- console.log('end: ' + data)
- })
这样就让异步的流程完全以同步的方式展示出来啦?~
Async/Await
ES7标准中引入的async函数,是对js异步解决方案的进一步完善,它有如下特点:
- 内置执行器:不用像generator那样反复调用next方法,或者使用co模块,调用即会自动执行,并返回结果
- 返回Promise:generator返回的是iterator对象,因此还不能直接用
then
来指定回调 - await更友好:相比co模块约定的generator的yield后面只能跟promise或thunk函数或者对象及数组,await后面既可以是promise也可以是任意类型的值(Object、Number、Array,甚至Error等等,不过此时等同于同步操作)
进一步说,async函数完全可以看作多个异步操作,包装成的一个Promise对象,而await命令就是内部then命令的语法糖。
改写后代码如下:
- async function testAsync() {
- const res1 = await new Promise((res) => {
- setTimeout(() => {
- res('step-1')
- }, 2000)
- })
- console.log(res1)
- const res2 = await Promise.resolve('step-2')
- console.log(res2)
- const res3 = await new Promise((res) => {
- setTimeout(() => {
- res('step-3')
- }, 2000)
- })
- console.log(res3)
- return [res1, res2, res3, 'end']
- }
-
- testAsync().then((data) => {
- console.log(data)
- })
这样不仅语义还是流程都非常清晰,即便是不熟悉业务的开发者也能一眼看出哪里是异步操作。
总结
本文汇总了当前主流的JS异步解决方案,其实没有哪一种方法最好或不好,都是在不同的场景下能发挥出不同的优势。而且目前都是Promise与其他两个方案配合使用的,所以不存在你只学会async/await或者generator就可以玩转异步。没准以后又会出现一个新的方案,将已有的这几种方案颠覆呢 ~
说实话,学过后端的人玩JavaScript会陷入一种困境,如果让程序员自己处理可能会更符合逻辑,比如引入线程之类的,不过优化起来又是一个问题了。。。
来源:https://blog.markeyme.cn/2018/06/09/ES6%E5%BC%82%E6%AD%A5%E6%96%B9%E5%BC%8F%E5%85%A8%E9%9D%A2%E8%A7%A3%E6%9E%90/

共有 0 条评论