setTimeout 链式调用
有这样一段代码:
let t1 = setTimeout(() => {
console.log(1)
let t2 = setTimeout(() => {
console.log(2)
let t3 = setTimeout(() => {
console.log(3)
}, 3000)
}, 2000)
}, 1000)
这段代码将会在 1s、3s 和 6s 时分别打印 1、2、3。当定时器过多时,这种嵌套会导致代码臃肿。为了解决这个问题,思路就是如何实现定时器链式调用。
提起链式调用就会想到 ES6 中的 Promise.then,所以问题就是怎么把每个定时器转换为 Promise。默认情况下,每一个 .then() 方法还会返回一个新生成的 promise 对象,这个对象可被用作链式调用。
将一个定时器转换为 Promise 其实就是常见的 sleep 方法:
let sleep = function (time = 0) {
return new Promise(resolve => setTimeout(resolve, time))
}
接下来要做的事情就是自定义 .then 方法的返回值,为了实现定时器链式调用需要直接返回 sleep。
let t = sleep(1000).then(() => {
console.log(1)
return sleep(2000)
}).then(() => {
console.log(2)
return sleep(3000)
}).then(() => {
console.log(3)
})
顺带着看了下 PromiseA+ 规范,写了如下的 Promise:
class _Promise {
static PENDING = 'pending'
static RESOLVED = 'resolved'
static REJECTED = 'rejected'
static resolve (value) {
if (!value) return new _Promise(res => { res() })
if (value instanceof _Promise) return value
return new _Promise(resolve => resolve(value))
}
static reject (reason) {
return new _Promise((resolve, reject) => reject(reason))
}
constructor (executor) {
// initialize
this.state = _Promise.PENDING
// 成功的值
this.value = undefined
// 失败的原因
this.reason = undefined
// 支持异步
this.resolveQueue = []
this.rejectQueue = []
// success
let resolve = (value) => {
if (this.state === _Promise.PENDING) {
this.state = _Promise.RESOLVED
this.value = value
this.resolveQueue.forEach(cb => cb(value))
}
}
// failure
let reject = (reason) => {
if (this.state === _Promise.PENDING) {
this.state = _Promise.REJECTED
this.reason = reason
this.rejectQueue.forEach(cb => cb(reason))
}
}
// immediate execute
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
then (onResolved, onRejected) {
return new _Promise((resolve, reject) => {
const resolvePromise = res => {
try {
if (typeof onResolved !== 'function') {
resolve(res)
} else {
const value = onResolved(res)
value instanceof _Promise ? value.then(resolve, reject) : resolve(value)
}
} catch (err) {
reject(err)
}
}
const rejectPromise = err => {
try {
if (typeof onRejected !== 'function') {
reject(err)
} else {
const value = onRejected(err)
value instanceof _Promise ? value.then(resolve, reject) : reject(value)
}
} catch (error) {
reject(error)
}
}
if (this.state === _Promise.RESOLVED) {
setTimeout(() => resolvePromise(this.value), 0)
}
if (this.state === _Promise.REJECTED) {
setTimeout(() => rejectPromise(this.reason), 0)
}
if (this.state === _Promise.PENDING) {
if (onResolved && typeof onResolved === 'function') {
this.resolveQueue.push(() => {
setTimeout(() => resolvePromise(this.value), 0)
})
}
if (onRejected && typeof onRejected === 'function') {
this.rejectQueue.push(() => {
setTimeout(() => rejectPromise(this.reason), 0)
})
}
}
})
}
catch (onRejected) {
return this.then(null, onRejected)
}
finally (callback) {
return this.then(value => {
return _Promise.resolve(callback()).then(() => value)
}, reason => {
return _Promise.resolve(callback()).then(() => { throw reason })
})
}
}
这里用 setTimeout 模拟了微任务 then 方法,写了下面的测试例子:
setTimeout(() => {
console.log('after then')
}, 0)
new _Promise((resolve) => {
console.log('new')
resolve('then')
}).then(val => console.log(val))
console.log('after new')
// 打印顺序是
// new
// after new
// after then 因为是用定时器模拟的,所以如果定时器时间为 0 的话,定时器先于 then 方法执行
// then
为了更真实地模拟微任务,可以使用 scope.queueMicrotask(function) 代替 setTimeout(fn, 0)。
这里只是模拟的 Promise,在真实的 Promise 里,遇到如下 case:
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('foo')
}, 7000)
})
myPromise
.then(value => { return value + ' and bar'; })
.then(value => { return value + ' and bar again'; })
.then(value => { return value + ' and again'; })
.then(value => { return value + ' and again'; })
.then(value => { console.log(value) })
.then(() => {
throw 'custom error'
})
.finally(() => {
console.log(1111)
})
.catch(err => { console.log(err) })
.then(() => {
console.log(2222)
})
// 打印顺序为
// foo and bar and bar again and again and again
// 1111
// custom error
// 2222
在 finally 之后的 catch 和 then 方法都执行了,但是在 _Promise 中 finally 之后的代码都没有被执行,说明自己实现的 _Promise 还是存在问题的。
打印了一下 myPromise 和 t,发现 myPromise 最后结果是 resolved 的 promise 对象,而 t 还处于 pending 状态,问题就出在这里。
myPromise Promise { 'foo' }
_Promise {
state: 'pending',
value: undefined,
reason: undefined,
resolveQueue: [],
rejectQueue: []
}
目前还是和原生 Promise 存在不一致的问题,比如下面的 6 就不会被打印,原生 Promise 是会打印的。
let sleep = function (time = 0) {
return new _Promise(resolve => setTimeout(resolve, time))
}
let start = Date.now(), end = 0
let t = sleep(1000).then(() => {
console.log(1)
return sleep(2000)
}).then(() => {
console.log(2)
return sleep(3000)
}).then(() => {
console.log(3)
end = Date.now()
console.log('total ms', end - start)
}).then(() => {
throw 'custom error'
}).catch(e => {
console.log('e1 =', e)
}).finally(() => {
console.log(4)
}).then(() => {
console.log(5)
}).catch(e => {
console.log('e2 =', e)
}).then(() => {
console.log(6)
})
// 原生 promise 会打印 6,但是自己实现的不会打印
参照了 promise 库的写法,完善了循环调用和状态只能改变一次,现在的 Promise 执行就符合预期了。
class _Promise {
static PENDING = 'pending'
static RESOLVED = 'resolved'
static REJECTED = 'rejected'
static resolve (value) {
if (value instanceof _Promise) return value
return new _Promise(resolve => resolve(value))
}
static reject (reason) {
return new _Promise((_, reject) => reject(reason))
}
constructor (executor) {
// initialize
this.state = _Promise.PENDING
// 成功的值
this.value = undefined
// 失败的原因
this.reason = undefined
// 支持异步
this.resolveQueue = []
this.rejectQueue = []
// success
let resolve = (value) => {
if (this.state === _Promise.PENDING) {
this.state = _Promise.RESOLVED
this.value = value
this.resolveQueue.forEach(cb => cb(value))
}
}
// failure
let reject = (reason) => {
if (this.state === _Promise.PENDING) {
this.state = _Promise.REJECTED
this.reason = reason
this.rejectQueue.forEach(cb => cb(reason))
}
}
// immediate execute
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
then (onResolved, onRejected) {
// 对 onResolved,onRejected 作规范化处理,统一转换为函数
onResolved = typeof onResolved === 'function' ? onResolved : value => value
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
/**
* 用于处理循环调用,保证同一时刻只能调用一次 resolve 或 reject
* @param {*} promise2
* @param {*} x
* @param {*} resolve
* @param {*} reject
* @returns
*/
const resolvePromise = (promise2, x, resolve, reject) => {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
let called
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
try {
let then = x.then
if (typeof then === 'function') {
then.call(x, y => {
if (called) return
called = true
resolvePromise(promise2, y, resolve, reject)
}, r => {
if (called) return
called = true
reject(r)
})
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
resolve(x) // 普通值直接 resolve
}
}
let promise2 = new _Promise((resolve, reject) => {
const handleResolved = () => {
try {
const x = onResolved(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}
const handleRejected = () => {
try {
const x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}
if (this.state === _Promise.RESOLVED) {
queueMicrotask(handleResolved)
}
if (this.state === _Promise.REJECTED) {
queueMicrotask(handleRejected)
}
if (this.state === _Promise.PENDING) {
this.resolveQueue.push(() => queueMicrotask(handleResolved))
this.rejectQueue.push(() => queueMicrotask(handleRejected))
}
})
return promise2
}
catch (onRejected) {
return this.then(undefined, onRejected)
}
finally (callback = () => {}) {
return this.then(value => {
return _Promise.resolve(callback()).then(() => value)
}, reason => {
return _Promise.resolve(callback()).then(() => { throw reason })
})
}
}