手写代码实现Promise之二

317 阅读6分钟

手写代码实现Promise之二

上一篇文章已经实现了基本的Promise,这里将继续实现其他Promise的方法。

  • Promise.prototype.then
  • Promise.prototype.catch
  • Promise.prototype.finally
  • Promise.resolve
  • Promise.reject
  • Promise.all
  • Promise.race
  • Promise.allSettled
  • Promise.any

finally方法

之前学习了Promise的两个公有方法thencatch方法,现在来看第三个公有方法finally

用法:finally(callback)

对于finally方法有两个注意点:一是callback回调参数不接受任何参数,二是finally方法会传递原来的值。如下面的例子。

Promise.resolve('test').finally(() => {
    console.log('finally')
    return 'finally test'
}).then(res => {
    console.log('success: ' + res)
})
// finally
// success: test

可以看到,finally中回调函数返回了'finally test',是没什么卵用的。事实上finally方法保留了原有的结果值,并且传递给下一个Promise

它的实现方式很简单,如下

finally(callback) {
  if (typeof callback !== 'function') {
    callback = () => {}
  }
  return this.then(
    res => new MyPromise(resolve => resolve(callback())).then(() => res),
    err => new MyPromise(resolve => resolve(callback())).then(() => { throw err })
  )
}

因为我们还没实现MyPromise.resolve方法,不然可改成

finally(callback) {
  if (typeof callback !== 'function') {
    callback = () => {}
  }
  return this.then(
    res => MyPromise.resolve(callback()).then(() => res),
    err => MyPromise.resolve(callback()).then(() => { throw err })
  )
}

至此,MyPromise的三个公有方法都已实现,接下来就是静态方法的实现。

resolve方法

reoslve方法的实现也比较简单,如下。

static resolve(value) {
  return new MyPromise(resolve => resolve(value))
}

测试一下,很ok

MyPromise.resolve('test').then(res => console.log('success: ' + res));
console.log('同步')
// 同步
// success: test

等等,这样就完成了吗?再用其他例子测试一下

// ---例子1---
Promise.resolve(
    new Promise((resolve) => resolve('test'))
).then(res => {
    console.log('success: ' + res)
})
// success: test

MyPromise.resolve(
    new MyPromise((resolve) => resolve('test'))
).then(res => {
    console.log('success: ' + res)
})
// success: [object Object]

// ---例子2---
let thenable = {
    then: function(resolve, reject) {
      resolve('test');
    }
};
Promise.resolve(thenable).then(res => {
    console.log('success: ' + res)
})
// success: test

MyPromise.resolve(thenable).then(res => {
    console.log('success: ' + res)
})
// success: [object Object]

发现不对劲啊,与原生Promise还是有些区别。学习了ECMAScript 6 入门,发现Promise.resolve方法对不同参数的处理是不一样的。

  1. 参数是一个Promise实例,直接返回这个实例。
  2. 参数是一个具有then方法的对象,将这个对象转为Promise对象,然后立即执行thenable对象的then方法。
  3. 参数是一个原始值,或者是一个不具有then方法的对象,返回一个新的Promise对象,状态为resolved
  4. 不带有任何参数,直接返回一个resolved状态的Promise对象。

因此要改一下resolve方法

static resolve(value) {
  if (value instanceof MyPromise) {
    return value
  }

  if (value instanceof Object && typeof value.then === 'function') {
    return new MyPromise(value.then)
  }

  return new MyPromise(resolve => resolve(value))
}

测试一下之前的例子,就木问题了。等一下,再等一下,看下面

let thenable = {
    then: function(resolve, reject) {
        console.log(1)
        resolve('test');
        console.log(2)
    }
};
Promise.resolve(thenable).then(res => {
    console.log('success: ' + res)
})
Promise.resolve('3').then(res => {
    console.log('success: ' + res)
})
console.log(4)
// 4
// 1
// 2
// success: 3
// success: test


MyPromise.resolve(thenable).then(res => {
    console.log('success: ' + res)
})
MyPromise.resolve('3').then(res => {
    console.log('success: ' + res)
})
// 1
// 2
// 4
// success: test
// success: 3

thenable对象的then方法应该后于同步代码执行。因此最后对resolve方法改一下

static resolve(value) {
  if (value instanceof MyPromise) {
    return value
  }

  if (value instanceof Object && typeof value.then === 'function') {
    return MyPromise.resolve().then(() => new MyPromise(value.then))
  }

  return new MyPromise(resolve => resolve(value))
}

至此,总算是对MyPromise.resolve方法封装好了。

reject方法

reject方法比较简单,不需要像resolve方法那样条件判断。

static reject(value) {
  return new MyPromise((_, reject) => reject(value))
}

all方法

Promise.all()方法接受一个数组作为参数,如果数组的元素不是Promise实例,就会调用Promise.resolve方法,将其转为 Promise 实例,再进一步处理。

现在来实现一下。

static all(promises) {
  if (!Array.isArray(promises)) {
    throw new TypeError('it is not a array')
  }

  return new MyPromise((resolve, reject) => {
    let len = promises.length
    let total = len
    let result = []

    function resolver(i, value) {
      result[i] = value
      // 数组中所有Promise实例都处理完,则可以改变新MyPromise的状态
      if (--total === 0) {
        resolve(result)
      }
    }

    for (let i = 0; i < len; i++) {
      // 只要有一个实例的状态是失败,那么新MyPromise就是失败
      MyPromise.resolve(promises[i]).then(value => resolver(i, value), reject)
    }
  })
}

可以尝试测试一下木有问题。事实上,Promise.all()方法的参数不一定要接受数组,但必须具有Iterator接口。

我们知道,默认具有Iterator接口的数据结构有

  • Array
  • Set
  • Map
  • String
  • TypedArray
  • 函数的arguments对象
  • NodeList对象

我们可以使用扩展运算符对具有Iterator接口的数据结构转换为数组

let arr = [...iterable];

那么我们可以对Promise.all()方法改一下,如下。

static all(promises) {
  if (typeof promises[Symbol.iterator] !== 'function') {
    throw new TypeError(promises + ' is not iterable')
  }

  return new MyPromise((resolve, reject) => {
    promises = [...promises]

    let len = promises.length
    let total = len
    let result = []

    if (len === 0) {
      resolve(result)
    } else {
      function resolver(i, value) {
        result[i] = value
        // 数组中所有Promise实例都处理完,则可以改变新MyPromise的状态
        if (--total === 0) {
          resolve(result)
        }
      }

      for (let i = 0; i < len; i++) {
        // 只要有一个实例的状态是失败,那么新MyPromise就是失败
        MyPromise.resolve(promises[i]).then(value => resolver(i, value), reject)
      }
    }
  })
}

测试一下,没有问题

MyPromise.all([1, 2]).then(res => console.log(res))
// [1, 2]

MyPromise.all([
  MyPromise.resolve('1'), 
  MyPromise.resolve('2')
])
.then(res => console.log(res))
// [1, 2]

MyPromise.all('12').then(res => console.log(res))
// ["1", "2"]

MyPromise.all(new Set([1, MyPromise.resolve(2)])).then(res => console.log(res))
// [1, 2]

MyPromise.all(new Map([['1', 1]])).then(res => console.log(res))
// [["1", 1]]

MyPromise.all(document.getElementsByTagName('body')).then(res => console.log(res))
// [body]

race方法

race方法和all方法类似,就直接来写

static race(promises) {
  if (typeof promises[Symbol.iterator] !== 'function') {
    throw new TypeError(promises + ' is not iterable')
  }
  return new MyPromise((resolve, reject) => {
    // 具有Iterator接口的数据结构都可以用for...of循环来遍历
    for (let promise of promises) {
      MyPromise.resolve(promise).then(resolve, reject)
    }
  })
}

allSettled方法

该方法在 ES2020 引入

其实allraceallSettledany方法接受的参数一样,处理方式都是类似的,只是对结果的处理不一致。allSettled方法要求的是所有数组中的参数实例都返回结果后,该方法包装返回的实例才会结束,并且结果最终都会是fulfilled。那么直接来实现

static allSettled(promises) {
  if (typeof promises[Symbol.iterator] !== 'function') {
    throw new TypeError(promises + ' is not iterable')
  }

  promises = [...promises]

  return new MyPromise((resolve) => {
    let len = promises.length
    let total = len
    let result = []

    function resolver(i) {
      return function(value) {
        result[i] = { status: MyPromise.FULFILLED, value }
        // 当所有实例都处理完
        if (--total === 0) {
          resolve(result)
        }
      }
    }
    function rejecter(i) {
      return function(reason) {
        result[i] = { status: MyPromise.REJECTED, reason }
        if (--total === 0) {
          resolve(result)
        }
      }
    }

    for (let i = 0; i < len; i++) {
      MyPromise.resolve(promises[i]).then(resolver(i), rejecter(i))
    }
  })
}

测试一下,木问题

let arr = [
    new MyPromise(resolve => {
        setTimeout(() => resolve(1), 100)
    }),
    new MyPromise((resolve, reject) => {
        setTimeout(() => reject(2), 500)
    })
]
MyPromise.allSettled(arr).then(res => {
    console.log(res)
})
console.log('同步')

// 同步
// [{"status":"fulfilled","value":1},{"status":"rejected","reason":2}]

any方法

该方法在ES2021 引入

该方法同样接受一组Promise实例,包装一个新的Promise实例返回。只要有一个实例成功,那么新的Promise实例状态变为fulfilled;如果所有实例都失败,则为rejectedrejected状态时抛出的错误是一个AggregateError实例。

static any(promises) {
  if (typeof promises[Symbol.iterator] !== 'function') {
    throw new TypeError(promises + ' is not iterable')
  }

  promises = [...promises]


  return new MyPromise((resolve, reject) => {
    let len = promises.length
    let total = len
    let result = []

    function rejecter(i) {
      return function(reason) {
        result[i] = reason
        if (--total === 0) {
          reject(new AggregateError(result, 'All promises were rejected'))
        }
      }
    }

    for (let i = 0; i < len; i++) {
      MyPromise.resolve(promises[i]).then(resolve, rejecter(i))
    }
  })
}

测试一下

let resolved = MyPromise.resolve('success')
let rejected = MyPromise.reject(1)
let alsoRejected = MyPromise.reject(2)

MyPromise.any([rejected, resolved, alsoRejected]).then(res => {
  console.log(res)
})
// success

MyPromise.any([rejected, alsoRejected]).catch(res => {
  console.log(res)
  console.log(res.errors)
})
// AggregateError: All promises were rejected
// [1, 2]

致此,把所有Promise方法都实现了。

总结

通过这一波学习,对Promise有了更好的理解。

事实上最终封装出来的MyPromise与原生Promise还是有本质的区别,比如说:

// 例I
setTimeout(() => {
    console.log(2)
})
Promise.resolve(1).then(res => console.log(res))

// 例II
setTimeout(() => {
    console.log(2)
})
MyPromise.resolve(1).then(res => console.log(res))

如果了解了事件队列,我们都知道例I应该输出1—>2。我们希望例II也应和例I一样,可没想到,例II输出的却是2—>1。这是应为我们封装的MyPromise是基于setTimeout来实现异步的。而原生Promise中,一个promise就是一个PromiseJob,放入Job Queue,它的处理是和Event Loop Queue区分开的。

代码地址github.com/Zeng-J/myPr…

参考资料

github.com/huanshen/Pr…

es6.ruanyifeng.com

www.zhihu.com/question/57…