ES6中的异步详解

众所周知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的好处就是让开发者远离了回调地狱的困扰,它具有如下特点:

  1. 对象的状态不受外界影响:
    • Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。
    • 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
    • Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。
    • 只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。
    • 如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。
    • 这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
  3. 一旦声明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.allPromise.racePromise.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.thencatch返回的值不能是当前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.nextTickpromise.then都属于microtask,而setImmediatesetTimeout属于macrotask

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实现异步:

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异步解决方案的进一步完善,它有如下特点:

  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命令的语法糖

改写后代码如下:

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/

上一篇 FancyBox3 中文文档
下一篇 JavaScript中函数后面打星号方式声明
applek

applek管理员

个人说明在个人中心里面设置

本月创作热力图

2026年3月
最新评论
hfloke
hfloke
3月1日
新版本安装更新后,页面有问题哦
丙氨酸
丙氨酸
2月27日
测试
评论于关于本站
RiseForever
RiseForever
2月23日
听说新主题发布了,来测试下评论区。
李贰捌
李贰捌
12月25日
AI摘要打开了,对接的阿里云,测试成功,但是前台为什么不显示?
javac
javac
12月8日
redis和memcached的完整支持有排期嘛?