关于JS你必须知道的事——Promise

234 阅读11分钟

前言

理解什么是Promise,以及Promise有哪些作用毫无疑问是我们学习JavaScript这门语言的必经之路。

Promise是什么?

Promise是异步编程的一种解决方案,从语法上来将,它是一个对象, 代表着一个异步操作最终完成或失败,从语意上来讲,它是承诺,承诺过一段时间给你一个结果。promise有三钟状态,分别是pending,fulfiled,rejected。

由于它的原型存在then,catch,finally会返回一个新的promise所以可以允许我们链式调用,解决了传统的回调地狱的问题。

由于它本身存在all方法,所以可以支持多个并发请求,获取并发请求中数据。

Promise特性

  1. promise的状态一经改变就不能再改变。
const promise = new Promise((resolve, reject) => {
  resolve("success1");
  reject("error");
  resolve("success2");
});
promise.then(res => {
    console.log("then: ", res);
  }).catch(err => {
    console.log("catch: ", err);
  })

结果:

"then: success1"

构造函数中的 resolvereject 只有第一次执行有效,多次调用没有任何作用 。

  1. Promise中,返回任意一个非 promise 的值都会被包裹成 promise 对象,例如return 2会被包装为return Promise.resolve(2)
Promise.resolve(1)
  .then(res => {
    console.log(res);
    return 2;
  })
  .catch(err => {
    return 3;
  })
  .then(res => {
    console.log(res);
  });

结果:

1
2

return 2会被包装成resolve(2)

then,catch方法

.then与.catch的不同点

1.then用于执行请求成功的回调函数 .then 方法可以接收两个参数,第一个就是处理成功的函数,第二个是处理失败的函数,在某种程度上可以认为.then的第二个参数就是.catch的简便写法。

Promise.reject('err!!!')
  .then((res) => {
    console.log('success', res)
  }, (err) => {
    console.log('error', err)
  }).catch(err => {
    console.log('catch', err)
  })

这里的执行结果是:

'error' 'error!!!'

它进入的是then()中的第二个参数里面,而如果把第二个参数去掉,就进入了catch()

Promise.reject('error!!!')
.then((res) => {
console.log('success', res)
}).catch(err => {
console.log('catch', err)
})

执行结果:

catch' 'error!!!'

如果是下面这样的话

Promise.resolve()
  .then(function success (res) {
    throw new Error('error!!!')
  }, function fail1 (err) {
    console.log('fail1', err)
  }).catch(function fail2 (err) {
    console.log('fail2', err)
  })

由于Promise调用的是resolve(),因此.then()执行的应该是success()函数,可是success()函数抛出的是一个错误,它会被后面的catch()给捕获到,而不是被fail1函数捕获。

因此执行结果为:

fail2 Error: error!!!

2.catch用于执行请求失败的回调函数 catch不管连接在哪里,都能捕获上层不能捕获的错误。

const promise = new Promise((resolve, reject) => {
  reject("error");
  resolve("success2");
});
promise
.then(res => {
    console.log("then1: ", res);
  }).then(res => {
    console.log("then2: ", res);
  }).catch(err => {
    console.log("catch: ", err);
  }).then(res => {
    console.log("then3: ", res);
  })

结果:

"catch: " "error"
"then3: " undefined

catch不管被连接到哪里,都能捕获上层未捕捉过的错误。

至于then3也会被执行,那是因为catch()也会返回一个Promise,且由于这个Promise没有返回值,所以打印出来的是undefined

.catch和.then的相同点

1.catch与.then都都会返回一个新的promise

const promise1 = new Promise((resolve, reject) => {
  console.log('promise1')
  resolve('resolve1')
})
const promise2 = promise1.then(res => {
  console.log(res)
})
console.log('1', promise1);
console.log('2', promise2);

结果

'promise1'
'1' Promise{<resolved>: 'resolve1'}
'2' Promise{<pending>}
'resolve1'

2.then与.catch 方法return一个error对象并不会抛出错误,所以不会被后面的catch捕获

Promise.resolve().then(() => {
  return new Error('error!!!')
}).then(res => {
  console.log("then: ", res)
}).catch(err => {
  console.log("catch: ", err)
})

猜猜这里的结果输出的是什么 🤔️ ?

你可能想到的是进入.catch然后被捕获了错误。

结果并不是这样的,它走的是.then里面

"then: " "Error: error!!!"

因为返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的return new Error('error!!!')也被包裹成了return Promise.resolve(new Error('error!!!'))

当然如果你抛出一个错误的话,可以用下面👇两的任意一种:

return Promise.reject(new Error('error!!!'));
// or
throw new Error('error!!!')

3.then与.catch返回的不能是调用它们的promise本身,否则会造成死循环

const promise = Promise.resolve().then(() => {
  return promise;
})
promise.catch(console.err)

.then.catch 返回的值不能是 promise 本身,否则会造成死循环。

Uncaught (in promise) TypeError: Chaining cycle detected for promise #

4.then与.catch的参数指望是函数,传入非函数会发生值透传

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

第一个then和第二个then中传入的都不是函数,一个是数字类型,一个是对象类型,因此发生了透传,将resolve(1) 的值直接传到最后一个then里。

所以输出结果为:

1

5.promise的.then和.catch可以被调用多次,但是如果Promise的状态发生了变化,并且有了一个值,那么后续每次调用.then与.catch都会直接拿到该值。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('timer')
    resolve('success')
  }, 1000)
})
const start = Date.now();
promise.then(res => {
  console.log(res, Date.now() - start)
})
promise.then(res => {
  console.log(res, Date.now() - start)
})

执行结果:

'timer'
'success' 1001
'success' 1002

当然,如果你足够快的话,也可能两个都是1001

Promise.then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。

.finally方法

  1. .finally方法不管promise对象最后的状态是成功还是失败如何都会执行。
Promise.resolve('1')
   .then(res => {
   console.log(res)
   })
   .finally(() => {
   console.log('finally')
   })

结果

`'1'`
`'finally'`
  1. .finally方法不接受任何参数,因此也无法通过finally方法得知promise的状态
  2. finally最终返回的默认会是上一个Promise的对象值,不过如果抛出的是一个异常则返回异常的peomise对象
Promise.resolve('2')
  .finally(() => {
    console.log('finally2')
  	return '我是finally2返回的值'
  })
  .then(res => {
    console.log('finally2后面的then函数', res)
  })

结果

'finally2'
'finally2后面的then函数' '2'
Promise.resolve('1')
  .finally(() => {
    console.log('finally1')
    throw new Error('我是finally中抛出的异常')
  })
  .then(res => {
    console.log('finally后面的then函数', res)
  })
  .catch(err => {
    console.log('捕获错误', err)
  })

执行结果为:

'finally1'
'捕获错误' Error: 我是finally中抛出的异常

但是如果改为return new Error('我是finally中抛出的异常'),打印出来的就是'finally后面的then函数 1'

.all, .race方法

.all接收一组异步任务,然后并行执行异步任务,并且在所有的异步任务执行完成后才执行回调。

function runAsync (x) {
    const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
    return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log(res))

结果

1
2
3
[1, 2, 3]

有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据。

.all()后面的.then()里的回调函数接收的就是所有异步操作的结果。而且这个结果中数组的顺序和Promise.all()接收到的数组顺序一致!!!

有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。

.race也是执行一组异步任务,然后并行执行异步任务,只保留第一个执行完成的一步操作,其他方法仍在执行,不过执行结果会被抛弃

function runAsync (x) {
  const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
  return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
  .then(res => console.log('result: ', res))
  .catch(err => console.log(err))

结果

1
'result: ' 1
2
3

这个race有什么用呢?使用场景还是很多的,比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作

all和.race传入的数组如果有会抛出异常的异步任务,那么只有最先抛出的任务会被捕获。并且是被then的第二个参数或者.catch捕获,并且不会影响数组中其他异步任务的执行

手写简易Promise

1.promise是个类

2.promise,在执行这个类时会传入一个执行器,这个执行器会立即执行

3.Promise有三种状态

4.状态只能有pending转为fulfiled或pending转为rejected,一旦状态发生变化,便不可二次修改

5.then方法内部做状态判断,成功状态调用成功回调函数,失败状态调用失败回调函数

6.为了实现异步逻辑,在then方法中可以进行判断,如果时pending状态的promise,可以先将其回调函数存起来,在状态发生变化时调用

7.为了实现链式调用,可以在then方法返回一个promise对象,并且在执行回调函数时获取返回值x,实现一个函数判断返回值类型,如果时下x === promise便抛出类型错误,如果x是promise的子类型,便返回x.then,如果是普通值,便直接resolve。

8.错误捕获.使用trycatch进行错误捕获

9.then参数可选,判断传入的是不是函数,如果是返回函数,如果不是,返回普通值

10.实现静态方法resolve和reject

// MyPromise.js

// 先定义三个常量表示状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

// 新建 MyPromise 类
class MyPromise {
  constructor(executor){
    // executor 是一个执行器,进入会立即执行
    // 并传入resolve和reject方法
    try {
      executor(this.resolve, this.reject)
    } catch (error) {
      this.reject(error)
    }
  }

  // 储存状态的变量,初始值是 pending
  status = PENDING;
  // 成功之后的值
  value = null;
  // 失败之后的原因
  reason = null;

  // 存储成功回调函数
  onFulfilledCallbacks = [];
  // 存储失败回调函数
  onRejectedCallbacks = [];

  // 更改成功后的状态
  resolve = (value) => {
    // 只有状态是等待,才执行状态修改
    if (this.status === PENDING) {
      // 状态修改为成功
      this.status = FULFILLED;
      // 保存成功之后的值
      this.value = value;
      // resolve里面将所有成功的回调拿出来执行
      while (this.onFulfilledCallbacks.length) {
        // Array.shift() 取出数组第一个元素,然后()调用,shift不是纯函数,取出后,数组将失去该元素,直到数组为空
        this.onFulfilledCallbacks.shift()(value)
      }
    }
  }

  // 更改失败后的状态
  reject = (reason) => {
    // 只有状态是等待,才执行状态修改
    if (this.status === PENDING) {
      // 状态成功为失败
      this.status = REJECTED;
      // 保存失败后的原因
      this.reason = reason;
      // resolve里面将所有失败的回调拿出来执行
      while (this.onRejectedCallbacks.length) {
        this.onRejectedCallbacks.shift()(reason)
      }
    }
  }

  then(onFulfilled, onRejected) {
    const realOnFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    const realOnRejected = typeof onRejected === 'function' ? onRejected : reason => {throw reason};

    // 为了链式调用这里直接创建一个 MyPromise,并在后面 return 出去
    const promise2 = new MyPromise((resolve, reject) => {
      const fulfilledMicrotask = () =>  {
        // 创建一个微任务等待 promise2 完成初始化
        queueMicrotask(() => {
          try {
            // 获取成功回调函数的执行结果
            const x = realOnFulfilled(this.value);
            // 传入 resolvePromise 集中处理
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error)
          } 
        })  
      }

      const rejectedMicrotask = () => { 
        // 创建一个微任务等待 promise2 完成初始化
        queueMicrotask(() => {
          try {
            // 调用失败回调,并且把原因返回
            const x = realOnRejected(this.reason);
            // 传入 resolvePromise 集中处理
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error)
          } 
        }) 
      }
      // 判断状态
      if (this.status === FULFILLED) {
        fulfilledMicrotask() 
      } else if (this.status === REJECTED) { 
        rejectedMicrotask()
      } else if (this.status === PENDING) {
        // 等待
        // 因为不知道后面状态的变化情况,所以将成功回调和失败回调存储起来
        // 等到执行成功失败函数的时候再传递
        this.onFulfilledCallbacks.push(fulfilledMicrotask);
        this.onRejectedCallbacks.push(rejectedMicrotask);
      }
    }) 
    
    return promise2;
  }

  // resolve 静态方法
  static resolve (parameter) {
    // 如果传入 MyPromise 就直接返回
    if (parameter instanceof MyPromise) {
      return parameter;
    }

    // 转成常规方式
    return new MyPromise(resolve =>  {
      resolve(parameter);
    });
  }

  // reject 静态方法
  static reject (reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason);
    });
  }
}

function resolvePromise(promise2, x, resolve, reject) {
  // 如果相等了,说明return的是自己,抛出类型错误并返回
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  // 判断x是不是 MyPromise 实例对象
  if(x instanceof MyPromise) {
    // 执行 x,调用 then 方法,目的是将其状态变为 fulfilled 或者 rejected
    // x.then(value => resolve(value), reason => reject(reason))
    // 简化之后
    x.then(resolve, reject)
  } else{
    // 普通值
    resolve(x)
  }
}

module.exports = MyPromise;

总结

  • Promise的状态一经改变就不能再改变。(见3.1)

  • .then.catch都会返回一个新的Promise。(上面的👆1.4证明了)

  • catch不管被连接到哪里,都能捕获上层未捕捉过的错误。(见3.2)

  • Promise中,返回任意一个非 promise 的值都会被包裹成 promise 对象,例如return 2会被包装为return Promise.resolve(2)

  • Promise.then 或者 .catch 可以被调用多次, 但如果Promise内部的状态一经改变,并且有了一个值,那么后续每次调用.then或者.catch的时候都会直接拿到该值。(见3.5)

  • .then 或者 .catchreturn 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获。(见3.6)

  • .then.catch 返回的值不能是 promise 本身,否则会造成死循环。(见3.7)

  • .then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传。(见3.8)

  • .then方法是能接收两个参数的,第一个是处理成功的函数,第二个是处理失败的函数,再某些时候你可以认为catch.then第二个参数的简便写法。(见3.9)

  • .finally方法也是返回一个Promise,他在Promise结束的时候,无论结果为resolved还是rejected,都会执行里面的回调函数。

  • Promise.all()的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调。

  • .race()的作用也是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。

  • Promise.all().then()结果中数组的顺序和Promise.all()接收到的数组顺序一致。

  • all和race传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被then的第二个参数或者后面的catch捕获;但并不会影响数组中其它的异步任务的执行。