众所周知 JS 是单线程的, 这种设计让 JS 避免了多线程的各种问题, 但同时也让 JS 同一时刻只能执行一个任务, 若这个任务执行时间很长的话(如死循环), 会导致 JS 直接卡死, 在浏览器中的表现就是页面无响应, 用户体验非常之差.
因此, 在 JS 中有两种任务执行模式: 同步 (Synchronous) 和异步(Asynchronous). 类似函数调用, 流程控制语句, 表达式计算等就是以同步方式运行的, 而异步主要由
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
等等(可以参考阮老师的文章 http://es6.ruanyifeng.com/#docs/promise ), 在需要的时候配合使用都是极好的.
API 无需多说, 不过这里我总结了一下自己之前使用 Promise 踩到的坑以及我对 Promise 理解不够透彻的地方, 希望也能帮助大家更好地使用 Promise:
then 的返回结果:
如果 then 方法中返回了一个值, 那么返回一个 "新的"resolved 的 Promise, 并且 resolve 回调函数的参数值是这个值
如果 then 方法中抛出了一个异常, 那么返回一个 "新的"rejected 状态的 Promise
如果 then 方法返回了一个未知状态 (pending) 的 Promise 新实例, 那么返回的新 Promise 就是未知状态
如果 then 方法没有返回值时, 那么会返回一个 "新的"resolved 的 Promise, 但 resolve 回调函数没有参数
我之前天真的以为 then 要想链式调用, 必须要手动返回一个新的 Promise 才行
- Promise.resolve('first promise')
- .then((data) => {
- // return Promise.resolve('next promise')
- // 实际上两种返回是一样的
- return 'next promise'
- })
- .then((data) => {
- console.log(data)
- })
一个 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
- })
then 或 catch 返回的值不能是当前 promise 本身, 否则会造成死循环:
- const promise = Promise.resolve()
- .then(() => {
- return promise
- })
then 或者 catch 的参数期望是函数, 传入非函数则会发生值穿透:
- Promise.resolve(1)
- .then(2)
- .then(Promise.resolve(3))
- .then(console.log)
- // 1
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 规范中对协程的实现, 但目前大多被用于异步模拟同步上了.
执行它会返回一个遍历器对象 http://es6.ruanyifeng.com/#docs/iterator , 而每次调用 next 方法则将函数执行到下一个 yield 的位置, 若没有则执行到 return 或末尾.
依旧是不再赘述 API, 对它还不了解的可以查阅阮老师的文章 http://es6.ruanyifeng.com/#docs/generator#简介 .
通过 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 大神 https://github.com/tj 为社区贡献了 co 模块 https://github.com/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 就可以玩转异步. 没准以后又会出现一个新的方案, 将已有的这几种方案颠覆呢 ~
在这不断变化, 发展的时代, 我们前端要放开自己的眼界, 拥抱变化, 持续学习, 才能成长, 写出优质的代码~
来源: https://juejin.im/entry/5b1e50b4e51d45068a6cb563