findxc/blog

异步和 Promise

Opened this issue · 0 comments

为什么不能只有同步

因为 JS 是单线程的,如果只有同步,那比如发一个请求,就会一直等待请求结束,这期间界面无法响应用户其它交互,这显然是不可接受的。

采用异步的方式,我们发完请求后会去处理其它工作,等请求结果回来后再继续处理它。

那问题来了,这个异步语法设计成什么样会比较好呢?(什么是好:写起来方便,可读性、可维护性高)

// 比如我在这里请求登录
requestLogin()

// 然后在请求成功后,代码的执行又回到这里来,这个语法怎么设计比较好?
if(response.ok) {
  console.log('登录成功')
}

回调函数的方式

最初就是以回调函数的方式来写异步代码。现在在一些第三方 API 文档上也能看到,比如微信小程序提供的 wx.login

wx.login({
  success(res) {
    if (res.code) {
      // 如果有 code 就使用 code 找后端请求登录
      wx.request({
        url: 'https://example.com/login',
        data: {
          code: res.code,
        },
        success(res) {
          console.log(res.data)
        },
      })
    } else {
      console.log('登录失败!' + res.errMsg)
    }
  },
})

在执行异步操作时,我们传入了回调函数,当异步操作完成后,就会执行我们传入的回调函数。

从上面这个例子可以看出回调函数存在两个问题:

  1. 如果在回调中又需要做异步操作,会使得代码嵌套比较深,可读性下降
  2. 由于使用时是传一个回调函数进去,不太符合人们习惯的第一步、第二步、第三步这种顺序执行的感觉

Promise 的链式写法

后来,你们都知道的,就出了 Promise 这个东西,我们把上面的 wx.login 用 Promise 的方式来写一遍:

wx.login()
  .then(res => {
    if (!res.code) {
      throw Error(res.errMsg)
    }
    // 如果有 code 就使用 code 找后端请求登录
    return wx.request({
      url: 'https://example.com/login',
      data: {
        code: res.code,
      },
    })
  })
  .then(() => {
    console.log('登录成功')
  })
  .catch(err => {
    console.error(err)
  })

由于可以 .then 后面继续 .then ,就不存在嵌套深的问题了,代码逻辑也更加顺序了。同时,支持使用一个 catch 来捕获异步过程中任意位置报错这一点也让代码变得更简洁了。

所以在 Promise 出来后,大家异步都倾向于用 Promise 了。

至于再后面出现的 async await 我们就不继续讨论了,本文的重点还是 Promise 。

Promise 的 .then 是微任务

直接来看一个很常见的面试题:

setTimeout(() => {
  console.log('aaa')

  new Promise(resolve => {
    console.log('aaa')
    resolve(111)
    console.log('aaa')
  })
    .then(res => {
      console.log('aaa')
      return new Promise(resolve => {
        console.log('aaa')
        resolve(res + 1)
      })
    })
    .then(res => {
      console.log('aaa')
    })

  new Promise(resolve => {
    console.log('aaa')
    resolve(5555)
  }).then(res => {
    console.log('aaa')
  })

  console.log('aaa')
}, 0)

setTimeout(() => {
  console.log('aaa')
}, 0)

按执行顺序把 aaa 替换为从 1 开始的数字,然后在控制台看看结果是不是符合预期。

这里面涉及到的知识点就是事件循环和任务队列,我们不深究,网上有很多这方面的文章了,如果想去看规范这里有: Event loops - HTML Standard

简单点说就是 JS 会按任务队列中的顺序去执行每个任务,先执行最早放到队列中的任务,然后执行这个任务对应的微任务队列,然后再执行下一个任务,然后再执行下一个任务对应的微任务队列,如下图编号顺序所示。

9BF99AED-4CDB-4092-888A-D318464E26A1

需要注意,在执行 task 以及 task 关联的微任务队列这个过程中产生的微任务,也会继续放到本次微任务队列中去。也就是说,如果某个微任务一直在产生新的微任务,那就永远也执行不到下个 task 。

我们常用的 setTimeout 算 task ,而 Promise 的 .then 算微任务。微任务的设计目的是让你可以尽快执行某段代码,因为如果是 task 会排到更后面。

现在再看上面那段代码你应该能理解代码的执行结果了。

queueMicrotask

queueMicrotask 就是一个 API ,就是添加一个微任务的意思。下面是一个示例。

const run = () => {
  console.log('in')
	// 添加一个微任务到当前 task 的微任务队列末尾
  queueMicrotask(run)
}

setTimeout(() => {
  console.log('start')
  run()
}, 0)

setTimeout(() => {
  // 永远也不可能执行到这里来,因为上一个 task 的微任务一直执行不完
  console.log('end')
}, 0)

自己实现一个 Promise

因为 Promise 其实细节很多,完整规范见 Promises/A+ ,这里我们会省略掉 2.3.3 ,也就是 .then(func) 中 func 的返回值是一个对象或者函数的场景,而主要考虑下面这些功能点:

  1. .then().catch() 返回的是一个新的 promise
  2. .then(func) 中的 func 支持返回 promise
  3. 当 promise 状态变更时,它的 .then 和 .catch 生成的新的 promise 状态也要跟着变更

比如下面这个例子,最终 a 、 b 、 c 、 d 都是 rejected ,而 e 和 f 是 fulfilled 。

a = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('oh no')
  }, 100)
})
b = a.then(v => v)
c = b.then(v => v)
d = c.then(v => v)
e = d.catch(v => v)
f = e.catch(v => v)

第 1 点和第 3 点其实是相关的,在 .then() 里面我们定义一个新的 promise ,然后返回即可,然后为了在当前 promise 状态变更时能让通过 .then() 产生的新的 promise 状态也变更,我们把这个新的 promise 的 resolve 和 reject 有存起来,大概思路如下:

then(onFulfilled, onRejected) {
  // 先创建一个新的 promise ,后面会直接返回它
  // 这里定义了 resolve 和 reject 是为了在 callback 里面来修改 promise 的 state
  let resolve, reject
  const newPromise = new Promise((_resolve, _reject) => {
    resolve = _resolve
    reject = _reject
  })

  const callback = () => {
    // 因为 .then 和 .catch 都算是微任务,所以这里我们用 queueMicrotask 来执行
    queueMicrotask(() => {
      // TODO
      // 当前 promise 状态变更时会执行这个 callback
      // 在这里通过 resolve 和 reject 可以改变 newPromise 的状态
    })
  }

  if (this.state === pending) {
    this.callbacks.push(callback)
  } else {
    callback()
  }

  return newPromise
}

然后第二点,如果 .then(func) 中的 func 支持返回的是 promise ,那其实我们就 newResult.then(resolve, reject) 这样就能在返回的 promise 状态变更时改变 newPromise 的状态了。

if (newResult instanceof Promise) {
  // 这种时候是这个返回的 promise 的 state 决定 newPromise 的 state
  newResult.then(resolve, reject)
} else {
  resolve(newResult)
}

完整代码见 https://github.com/findxc/my-promise/blob/master/Promise.js

确实自己写一遍会对 Promise 的各种细节更加了解。虽然平时业务开发都触及不到这些细节 =.= 恩,当你无聊的时候, have a try 。