Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setTimeout 链式调用 #49

Open
Inchill opened this issue Nov 1, 2022 · 3 comments
Open

setTimeout 链式调用 #49

Inchill opened this issue Nov 1, 2022 · 3 comments

Comments

@Inchill
Copy link
Owner

Inchill commented Nov 1, 2022

有这样一段代码:

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
Copy link
Owner Author

Inchill commented Nov 7, 2022

顺带着看了下 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
Copy link
Owner Author

Inchill commented Nov 9, 2022

目前还是和原生 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
Copy link
Owner Author

Inchill commented Nov 12, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant