异步和 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)
}
},
})
在执行异步操作时,我们传入了回调函数,当异步操作完成后,就会执行我们传入的回调函数。
从上面这个例子可以看出回调函数存在两个问题:
- 如果在回调中又需要做异步操作,会使得代码嵌套比较深,可读性下降
- 由于使用时是传一个回调函数进去,不太符合人们习惯的第一步、第二步、第三步这种顺序执行的感觉
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 会按任务队列中的顺序去执行每个任务,先执行最早放到队列中的任务,然后执行这个任务对应的微任务队列,然后再执行下一个任务,然后再执行下一个任务对应的微任务队列,如下图编号顺序所示。
需要注意,在执行 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 的返回值是一个对象或者函数的场景,而主要考虑下面这些功能点:
.then()
和.catch()
返回的是一个新的 promise.then(func)
中的 func 支持返回 promise- 当 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 。