fe-notes icon indicating copy to clipboard operation
fe-notes copied to clipboard

setTimeout 链式调用

Open Inchill opened this issue 3 years ago • 3 comments

有这样一段代码:

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)
})

Inchill avatar Nov 01 '22 13:11 Inchill

顺带着看了下 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: []
}

Inchill avatar Nov 07 '22 07:11 Inchill

目前还是和原生 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,但是自己实现的不会打印

Inchill avatar Nov 09 '22 09:11 Inchill

参照了 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 })
    })
  }
}

Inchill avatar Nov 12 '22 07:11 Inchill