Markey's home

ES6异步方式全面解析

2018/06/09 Share

ES6异步方式全面解析

众所周知JS是单线程的,这种设计让JS避免了多线程的各种问题,但同时也让JS同一时刻只能执行一个任务,若这个任务执行时间很长的话(如死循环),会导致JS直接卡死,在浏览器中的表现就是页面无响应,用户体验非常之差。

因此,在JS中有两种任务执行模式:同步(Synchronous)和异步(Asynchronous)。类似函数调用、流程控制语句、表达式计算等就是以同步方式运行的,而异步主要由setTimeout/setInterval、事件实现。


传统的异步实现

作为一个前端开发者,无论是浏览器端还是Node,相信大家都使用过事件吧,通过事件肯定就能想到回调函数,它就是实现异步最常用、最传统的方式。

不过要注意,不要以为回调函数就都是异步的,如ES5的数组方法Array.prototype.forEach((ele) => {})等等,它们也是同步执行的。回调函数只是一种处理异步的方式,属于函数式编程中高阶函数的一种,并不只在处理异步问题中使用。

举个栗子🌰:

1
2
3
4
5
6
// 最常见的ajax回调
this.ajax('/path/to/api', {
params: params
}, (res) => {
// do something...
})

你可能觉得这样并没有什么不妥,但是若有多个ajax或者异步操作需要依次完成呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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的好处就是让开发者远离了回调地狱的困扰,它具有如下特点:

  1. 对象的状态不受外界影响:

    • Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。
    • 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。

    • Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。
    • 只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。
    • 如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。
    • 这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
  3. 一旦声明Promise对象(new Promise或Promise.resolve等),就会立即执行它的函数参数,若不是函数参数则不会执行

上面的代码可以改写成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.allPromise.racePromise.resolve/reject等等(可以参考阮老师的文章),在需要的时候配合使用都是极好的。

API无需多说,不过这里我总结了一下自己之前使用Promise踩到的坑以及我对Promise理解不够透彻的地方,希望也能帮助大家更好地使用Promise:

  1. then的返回结果

    • 如果then方法中返回了一个值,那么返回一个“新的”resolved的Promise,并且resolve回调函数的参数值是这个值
    • 如果then方法中抛出了一个异常,那么返回一个“新的”rejected状态的Promise
    • 如果then方法返回了一个未知状态(pending)的Promise新实例,那么返回的新Promise就是未知状态
    • 如果then方法没有返回值时,那么会返回一个“新的”resolved的Promise,但resolve回调函数没有参数

      我之前天真的以为then要想链式调用,必须要手动返回一个新的Promise才行

      1
      2
      3
      4
      5
      6
      7
      8
      9
      Promise.resolve('first promise')
      .then((data) => {
      // return Promise.resolve('next promise')
      // 实际上两种返回是一样的
      return 'next promise'
      })
      .then((data) => {
      console.log(data)
      })
  2. 一个Promise可设置多个then回调,会按定义顺序执行,如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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. thencatch返回的值不能是当前promise本身,否则会造成死循环

    1
    2
    3
    4
    const promise = Promise.resolve()
    .then(() => {
    return promise
    })
  4. then或者catch的参数期望是函数,传入非函数则会发生值穿透

    1
    2
    3
    4
    5
    Promise.resolve(1)
    .then(2)
    .then(Promise.resolve(3))
    .then(console.log)
    // 1
  5. process.nextTickpromise.then都属于microtask,而setImmediatesetTimeout属于macrotask

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    process.nextTick(() => {
    console.log('nextTick')
    })
    Promise.resolve()
    .then(() => {
    console.log('then')
    })
    setImmediate(() => {
    console.log('setImmediate')
    })
    console.log('end')
    // end nextTick then setImmediate

    有关microtaskmacrotask可以看这篇文章,讲得很细致。

但Promise也存在弊端,那就是若步骤很多的话,需要写一大串.then(),尽管步骤清晰,但是对于我们这些追求极致优雅的前端开发者来说,代码全都是Promise的API(thencatch),操作的语义太抽象,还是让人不够满意呀~


Generator

Generator是ES6规范中对协程的实现,但目前大多被用于异步模拟同步上了。

执行它会返回一个遍历器对象,而每次调用next方法则将函数执行到下一个yield的位置,若没有则执行到return或末尾。

依旧是不再赘述API,对它还不了解的可以查阅阮老师的文章

通过Generator实现异步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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多行,感兴趣可以去研究下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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异步解决方案的进一步完善,它有如下特点:

  1. 内置执行器:不用像generator那样反复调用next方法,或者使用co模块,调用即会自动执行,并返回结果
  2. 返回Promise:generator返回的是iterator对象,因此还不能直接用then来指定回调
  3. await更友好:相比co模块约定的generator的yield后面只能跟promise或thunk函数或者对象及数组,await后面既可以是promise也可以是任意类型的值(Object、Number、Array,甚至Error等等,不过此时等同于同步操作)

进一步说,async函数完全可以看作多个异步操作,包装成的一个Promise对象,而await命令就是内部then命令的语法糖

改写后代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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就可以玩转异步。没准以后又会出现一个新的方案,将已有的这几种方案颠覆呢 ~

在这不断变化、发展的时代,我们前端要放开自己的眼界,拥抱变化,持续学习,才能成长,写出优质的代码😜~

CATALOG
  1. 1. ES6异步方式全面解析
    1. 1.0.1. 传统的异步实现
    2. 1.0.2. Promise
    3. 1.0.3. Generator
    4. 1.0.4. Async/Await
    5. 1.0.5. 总结