异步任务
实现 Fetch 的超时中断、重传
正确使用 async/await
在上面的例子中,通过异步任务(通过 url 获取图片)的成功与否,是由第三方确定的。这当然可以通过 then 创建微任务解决,但是容易产生 then 的回调地狱。async await 让代码更可读。
实现异步任务的并发控制
class SuperTask {
constructor(limit = 2) {
this.limit = limit
this.runningCount = 0
this.tasks = []
}
add(task) {
return new Promise((resolve, reject) => {
this.tasks.push({ task, resolve, reject })
this._run()
})
}
_run() {
while (this.runningCount < this.limit && this.tasks.length) {
const { task, resolve, reject } = this.tasks.shift()
this.runningCount++
task()
.then(resolve, reject)
.finally(() => {
this.runningCount--
this._run()
})
this._run()
}
}
}
const superTask = new SuperTask()
function timeout(time) {
return new Promise((resolve) => {
setTimeout(resolve, time)
})
}
function addTask(time, name) {
superTask.add(() => timeout(time)).then(() => console.log(`Task ${name} had been executed`))
}
addTask(10000, 1)
addTask(5000, 2)
addTask(3000, 3)
superTask.add(() =>
Promise.resolve()
.then(() => console.log(1))
.then(() => console.log(`1-1`))
.then(() => console.log(`1-2`))
)
addTask(2000, 4)
上述的代码通过维护一个任务队列来确保异步任务队列中,只会同时运行指定数量的异步任务。这种问题的场景是,例如,前端需要将多张图片上传至 CDN,但是由于后端做了控制,并发的请求超过指定数量会丢弃掉一部分(QPS 很小?)所以前端可以是实现一个并发控制。
手写 Promise
尝试实现一个 Promise
class Promise {
static #PENDING = 'pending'
static #FULFILLED = 'fulfilled'
static #REJECTED = 'rejected'
constructor(executor) {
// executor 必须是个函数
if (typeof executor !== 'function') {
throw new TypeError('resolver must be a function')
}
this.status = Promise.#PENDING
this.value = undefined
this.onResolveCallback = []
this.onRejectCallback = []
const resolve = (val) => {
if (this.status === Promise.#PENDING) {
// 这里的判断其实不应该判断是否为 Promise 的实例
// 而是应该判断是否为 thenable 的 “鸭子类型”
if (typeof val?.then === 'function' && Object.getOwnPropertyNames(val).includes('then')) {
// if (val instanceof Promise) {
val.then(
(data) => resolve(data),
(err) => reject(err)
)
return
}
this.status = Promise.#FULFILLED
this.value = val
this.onResolveCallback.forEach((callback) => callback())
}
}
const reject = (err) => {
if (this.status === Promise.#PENDING) {
this.status = Promise.#REJECTED
this.value = err
this.onResolveCallback.forEach((callback) => callback())
}
}
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
then(onFulfill, onReject) {
// 链式调用
return new Promise((resolve, reject) => {
switch (this.status) {
case Promise.#FULFILLED:
// 用 marco task 模拟 micro task
// 要判断onFulfill 是否为函数,同时,抛出错误时reject
setTimeout(() => {
if (typeof onFulfill !== 'function') {
resolve(this.value)
} else {
try {
resolve(onFulfill(this.value))
} catch (e) {
reject(e)
}
}
})
break
// 后面这里同理 判断传入的参数是否为函数
case Promise.#REJECTED:
setTimeout(() => resolve(onReject(this.value)))
break
case Promise.#PENDING:
this.onResolveCallback.push(() => {
setTimeout(() => {
resolve(onFulfill(this.value))
})
})
this.onRejectCallback.push(() => {
setTimeout(() => {
resolve(onReject(this.value))
})
})
break
}
})
}
}
通过手写 Promise,更加深刻地体会控制反转的意义。我认为,使用回调编写异步代码,会产生”回调地狱“,正是因为我们必须把 接下来作什么 (continuation)一次性告诉第三方。不把 continuation 传给第三方,而是使用第三方的能力,声明式地编写接下来做什么,这种编程范式,就是 Promise ——期约。
这里提到的第三方的能力,既是
resolve
,reject
这种能够决议一个期约的能力,也是then(onFulfill, onReject)
这种承诺会帮我们在状态发生改变后调用对应的处理函数的能力。这样一来,我们既可以决定何时做(何时决议)也可以决定做什么(onFulfill、onReject 对应的处理函数)
Promise 有关
- 手写 retry 函数
function retry(fn, times, delay) {
return new Promise((resolve, reject) => {
const attempt = () => {
if (times === 0) {
reject('tried enough times')
} else {
fn()
.then((res) => {
resolve(res)
})
.catch(() => {
times--
settimeout(attempt, delay)
})
}
}
})
}
Promise#finally
坑点- 不能获得 promise 的结果 (这也非常符合逻辑,一旦 Promise 在
Promise#then
中被决议 就无需也不能在Promise#finally
再次修改
Promise.resolve(1).finally((res) => console.log(res)) // undefined
- 可以抛出错误,让下一轮的拒绝态接收
Promise.resolve(1)
.finally(() => {
throw 'oops'
})
.then(undefined, (rej) => {
console.log(rej, 'onrejected')
})- 不能获得 promise 的结果 (这也非常符合逻辑,一旦 Promise 在
使用 Proxy 检测数组
function foo(obj) {
let p = new Proxy(obj, {
get(...args) {
console.log(`hhh`)
// 这里推荐不要使用 arguments 数组 因为其已被移出web标准
// 推荐使用 rest parameter 和 spread operator
return Reflect.get(...args)
},
})
return p
}
const f = foo([1, 2, 3])
console.log(f[0]) // 'hhh' 1