Promise详解

56 阅读16分钟

异步处理的困境

在Promise出现之前,在我们处理多个异步请求嵌套时,往往采用回调函数的方式。让我们考虑一个例子:从服务器获取用户的用户名,然后根据用户名获取用户的文章列表,最后根据文章列表获取每篇文章的内容。

这段代码中,我们需要在获取用户的用户名后,再获取用户的文章列表,然后对每篇文章进行操作。这样会导致嵌套的回调函数,降低代码的可读性和可维护性。

getUserUsername(function(username) {
  getArticlesByUsername(username, function(articles) {
    articles.forEach(function(articleId) {
      getArticleContent(articleId, function(content) {
        console.log(`Article ${articleId}: ${content}`);
      });
    });
  });
});

而使用 Promise 的方式可以使代码更清晰:

getUserUsername()
  .then(function(username) {
    return getArticlesByUsername(username);
  })
  .then(function(articles) {
    return Promise.all(articles.map(function(articleId) {
      return getArticleContent(articleId);
    }));
  })
  .then(function(contents) {
    contents.forEach(function(content, index) {
      console.log(`Article ${index}: ${content}`);
    });
  })
  .catch(function(error) {
    console.error("An error occurred:", error);
  });

什么是Promise

Promise是一个类,可以翻译成 承诺、期约,它是 JavaScript 中用于处理异步操作的一种对象。

通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor

  • 这个回调函数会被立即执行,并且给传入另外两个回调函数resolve、reject;
  • 调用resolve回调函数时,会执行Promise对象的then方法传入的回调函数;
  • 调用reject回调函数时,会执行Promise对象的catch方法传入的回调函数;

Promise对象存在三种状态:

  • 待定(pending): 初始状态,既没有被兑现,也没有被拒绝;
    • 当执行executor中的代码时,处于该状态;
  • 已兑现(fulfilled): 意味着操作成功完成;
    • 执行了resolve时,处于该状态;
  • 已拒绝(rejected): 意味着操作失败;
    • 执行了reject时,处于该状态;

对于下列代码,promise 对象表示了一个异步操作,在 1 秒后会执行成功,并返回 'Success!' 作为操作的结果。在 then() 方法中,我们定义了操作成功时的回调函数来处理操作的结果;在 catch() 方法中,我们定义了操作失败时的回调函数来处理操作失败的情况。

let promise = new Promise(function(resolve, reject) {
  // 异步操作
  setTimeout(function() {
    // 模拟异步操作成功
    resolve('Success!');
    // 模拟异步操作失败
    // reject(new Error('Something went wrong!'));
  }, 1000);
});

promise.then(function(result) {
  console.log('操作成功:', result);
}).catch(function(error) {
  console.error('操作失败:', error);
});

Executor

Executor是在创建Promise时需要传入的一个回调函数,这个回调函数会被立即执行,并且传入两个参数

通常会在Executor中确定我们的Promise状态

  • 通过resolve,可以兑现(fulfilled)Promise的状态,我们也可以称之为已决议(resolved);
  • 通过reject,可以拒绝(reject)Promise的状态;

一旦状态被确定下来,Promise的状态会被 锁死,该Promise的状态是不可更改的。

  • 在调用resolve的时候,如果resolve传入的值本身不是一个Promise,那么会将该Promise的状态变成兑现(fulfilled);
  • 如果之后再去调用reject时,已经不会有任何的响应了(并不是这行代码不会执行,而是无法改变Promise状态);

resolve传入不同值的区别

resolve(val),val传入不同值:

情况一:如果resolve传入一个普通的值或者对象,那么这个值会作为then回调的参数;

情况二:如果resolve中传入的是另外一个Promise,那么这个新Promise会决定原Promise的状态:

情况三:如果resolve中传入的是一个对象,并且这个对象有实现then方法(也称作thenable对象),那么会执行该then方法,并且根据then方法的结果来决定Promise的状态

// 1. resolver传入普通的值或对象
const promise = new Promise((resolve, reject) => {
    const obj = { name: 'weihy' }
    resolve(obj)
})

promise.then((res) => {
    console.log('then中拿到结果:', res);
})

// 2. resolve传入一个Promise
const promise = new Promise((resolve, reject) => {
  const promise2 = new Promise((resolve, reject) => {
    let randNum = Math.random() * 10;
    resolve('随机数' + randNum)
  })
  resolve(promise2)
})

promise.then((res) => {
  console.log('then中拿到结果:', res);
})\

// 3. resolve传入一个实现then方法的对象
const promise = new Promise((resolve, reject) => {
    // 创建一个带有 then 方法的对象 obj
    const obj = {
        name: 'weihy',
        then: function(resolve, reject) {
            resolve('resolve message')
        }
    }
    resolve(obj)
})

promise.then((res) => {
    console.log('then中拿到结果:', res);
})

Promise对象方法-then:

then方法是Promise对象上的一个方法:它其实是放在Promise的原型上的 Promise.prototype.then

then方法接受两个参数

then方法可以接受两个参数,分别是

  • fulfilled的回调函数:当状态变成fulfilled时会回调的函数;
  • reject的回调函数:当状态变成reject时会回调的函数

也就是说,then传入两个参数,等价于then().catch()的写法

// 0.Promise接受两个参数
const promise = new Promise((resolve, reject) => {
    resolve('123')
})
promise.then((res) => {
  console.log('res:', res);
}, (err) => {
  console.error(err)
})
// 等价于
promise.then((res) => {
  console.log('res:', res);
}).catch((err) => {
  console.error(err)
})

then方法可以被多次调用

一个Promise的then方法是可以被多次调用的,每次调用我们都可以传入对应的fulfilled回调;

当Promise的状态变成fulfilled的时候,这些回调函数都会被执行

const promise = new Promise((resolve, reject) => {
    resolve("abc")
})

// 1.同一个Promise可以被多次调用then方法
promise.then((res) => {
    console.log('res1:', res);
})
promise.then((res) => {
    console.log('res2:', res);
})
promise.then((res) => {
    console.log('res3:', res);
})

then方法的返回值

then方法本身是有返回值的,它的返回值是一个Promise,所以才可以进行链式调用。

那么then方法返回的Promise到底处于什么样的状态呢?

  • 当then方法传入的回调函数处于执行状态时,该Promise处于pending状态。
  • then方法中的回调函数返回一个值时,会将这个值作为resolve的参数,此时Promise处于fulfilled状态
  • then方法抛出异常时,处于reject状态

then方法中的回调函数返回一个值时,会将这个值作为resolve的参数这句话的意思:

下列代码中,A可以指一个字符串,一个Promise,也可以是一个对象。then方法中的回调函数返回一个A时,会将A作为resolve的参数。

// 下列下列两种写法等价
promise.then((res) => {
  return A
})

promise.then((res) => {
  return new Promise((resolve) => {
    resolve(A)
  })
})

then方法中回调函数的返回值

讨论一下then方法中传入的回调函数返回值的不同情况

当then方法中的回调函数本身在执行的时候,那么它处于pending状态;

当then方法中的回调函数返回一个结果时;

  • 情况一:返回一个普通的值,那么它处于fulfilled状态,并且会将结果作为resolve的参数:
  • 情况二:返回一个Promise,由新的Promise的状态来决定;
  • 情况三:返回一个thenable值,会调用then方法,决定状态;

当then方法抛出一个异常时,那么它处于reject状态;

返回一个普通的值

如果返回的是一个普通值(数值/字符串/普通对象/undefined),那么这个普通的值被作为一个新的Promise的resolve值。

注意:回调函数没有写return时,则返回undefined

// 1> 如果我们返回的是一个普通值(数值/字符串/普通对象/undefined), 
// 那么这个普通的值被作为一个新的Promise的resolve值
const promise = new Promise((resolve, reject) => {
    resolve("abc")
})
promise.then((res) => {
    return "123"
}).then((res) => {
    console.log('res:', res);
})

也就是说,上述代码等价于以下形式:

const promise = new Promise((resolve, reject) => {
    resolve("abc")
})
promise.then((res) => {
    return new Promise((resolve, reject) => {
        resolve("123")
    })
}).then((res) => {
    console.log('res:', res);
})

返回一个Promise

返回一个Promise,那么会将这个返回值(一个promise)作为新的Promise的resolve的参数值。

由于resolve中传入的是另外一个Promise,那么这个新Promise会决定原Promise的状态。所以最终res传入的是字符串“resolve”

const promise = new Promise((resolve, reject) => {
    resolve("abc")
})
promise.then((res) => {
    return new Promise((resolve, reject) => {
        resolve("resolve")
    })
}).then((res) => {
    console.log('res', res);
})

另外补充一下,由于会将返回值作为新Promise的resolve的参数值,所以可以等价成下列写法:

// 等价于
promise.then((res) => {
  return new Promise((resolve, reject) => {
    resolve(
      new Promise((resolve, reject) => {
        resolve("resolve2")
      })
    )
  })
}).then((res) => {
  console.log('res', res);
})

也可以看图理解

返回一个thenable值;

返回一个实现了then方法的对象,那么把这个对象作为新的Promise的resolve的值。如果resolve中传入的是一个对象,并且这个对象有实现then方法,那么会执行该then方法,并且根据then方法的结果来决定Promise的状态。

// 3> 如果返回的是一个对象, 并且该对象实现了thenable
const promise = new Promise((resolve, reject) => {
    resolve("abc")
})
promise.then((res) => {
    const obj = {
        then: function(resolve, reject) {
            resolve("123")
        }
    }
    return obj
}).then((res) => {
    console.log('res', res);
})

Promise对象方法-catch

当Promise链中的任何Promise被拒绝(rejected)时,catch方法会被调用,并且可以提供一个回调函数来处理该错误或拒绝的情况。

catch方法可以被多次调用

一个Promise的catch方法是可以被多次调用,每次调用我们都可以传入对应的reject回调,当Promise的状态变成reject的时候,这些回调函数都会被执行

// 0.catch方法是可以被多次调用
const promise = new Promise((resolve, reject) => {
  reject("rejected")
})

promise.catch((err) => {
  console.log('err1:', err);
})
promise.catch((err) => {
  console.log('err2:', err);
})
promise.catch((err) => {
  console.log('err3:', err);
})

catch方法的返回值

catch方法也会返回一个Promise对象的,所以catch方法后面可以继续调用then方法或者catch方法。

并且与then方法类似,对于catch方法返回的promise,这个promise内执行resolve的参数是catch方法回调函数的返回值,并且也会根据返回值的类型不同进行处理,与then方法类似。

// catch方法的返回值
const promise = new Promise((resolve, reject) => {
  reject("111111")
})
promise.then((res) => {
  console.log('res', res);
}).catch((err) => {
  console.log('err', err);
  return "22222"
}).then((res) => {
  console.log('catch返回promise的res', res);
})

上述代码中,catch返回的promise的resolve参数值是"22222",所以返回结果:

其他细节

executor抛出异常时, 也是会调用错误捕获的回调函数

// 1.当executor抛出异常时, 也是会调用错误捕获的回调函数的

const promise = new Promise((resolve, reject) => {
  // resolve('fulfilled status')
  throw new Error("rejected status")
})

promise.then((res) => {
  console.log('res', res);
}, (err) => {
  console.error('err', err)
  console.log('---');
})

但是在这需要注意的是,在异步函数内部抛出的错误会像未捕获的错误一样。

// 在异步函数内部抛出的错误会像未捕获的错误一样!!

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    throw new Error("未捕获的异常!");
  }, 1000);
});

p2.catch((e) => {
  console.error(e); // 永远不会被调用
});

运行结果:

catch方法捕获异常时从第一个promise开始捕获

// catch优先捕获第一个promise的错误

const promise2 = new Promise((resolve, reject) => {
    reject('rejected status')
})
promise2.then((res) => {
    return "111"
}).catch((err) => {
    console.error('err:', err)
})

上述代码,catch会对promise2内的reject进行捕获。

然而如果第一个promise没有进行reject时,才会对第2个promise进行捕获,如下列代码所示,对then中的reject进行捕获。

const promise3 = new Promise((resolve, reject) => {
    resolve('aaa')
})
promise3.then((res) => {
    return new Promise((resolve, reject) => {
        reject("then rejected status")
    })
}).catch((err) => {
    console.error('err:', err)
})

Promise对象方法-finally

finally是在ES9(ES2018)中新增的一个特性:表示无论Promise对象无论变成fulfilled还是reject状态,最终都会被执行的代码。

finally方法是不接收参数的,因为无论前面是fulfilled状态,还是reject状态,它都会执行。

const promise = new Promise((resolve, reject) => {
  resolve("success")
})
promise.then((res) => {
  console.log('res', res);
}).catch((err) => {
  console.log('err', err);
}).finally(() => {
  console.log('执行finally代码段');
})

finally方法返回的是一个新的Promise对象,因此可以在调用finally方法之后继续链式调用then和catch方法。

finally方法执行完毕后,会继续执行后续then或catch方法中注册的回调函数,具体取决于finally之前的Promise的状态。

finally方法传入的回调函数如果返回一个 promise 会等待这个 promise 也执行完毕。如果返回的是成功的 promise(比如return 普通值),会采用上一次的结果;如果返回的是失败的 promise,会用这个失败的结果,传到 catch 中。

const promise = new Promise((resolve, reject) => {
    resolve("success")
    // reject('fail')
})
promise.finally(() => {
    console.log('finally 成功或失败都会执行');
    return 111
}).then((res) => {
    console.log('res', res);
}).catch((err) => {
    console.log('err', err);
})

如果返回的是失败的 promise,会用这个失败的结果,传到 catch 中。所以finally方法传入的回调函数return一个Promise对象,并且执行reject时,则会捕捉错误

promise.finally(() => {
    console.log('finally 成功或失败都会执行');
    return new Promise((resolve, reject) => {
        reject(1) // 失败,会用这里的值作为错误原因
    })
}).then((res) => {
    console.log('res', res);
}).catch((err) => {
    console.log('err', err);
})

补充:如果第一个Promise为reject,并且finally方法中return的Promise也执行reject,那么catch捕捉finally传入的回调函数的return Promise的失败原因

const promise = new Promise((resolve, reject) => {
    reject('fail')
})
promise.finally(() => {
    console.log('finally 成功或失败都会执行');
    return new Promise((resolve, reject) => {
        reject('err')
        // resolve(2)
    })
}).then((res) => {
    console.log('res', res);
}).catch((err) => {
    console.log('err', err);
})

以下内容解释上述现象,可不看。

原因:内部实现finally时,执行callback()时如果状态变为rejected,不会执行图中红框后续的then方法,会直接被代码最后的catch捕获

Promise类方法-resolve

如果已经有一个值,希望将其转成Promise来使用,这个时候我们可以使用 Promise.resolve 方法来完成。

Promise.resolve的用法相当于new Promise,并且执行resolve操作:

const promise = Promise.resolve("abc")
// 等价于
const promise = new Promise((resolve, reject) => {
    resolve("abc")
})

resolve参数的形态

  • 情况一:参数是一个普通的值或者对象
  • 情况二:参数本身是Promise
  • 情况三:参数是一个thenable
const promise = Promise.resolve("success message")
promise.then((res) => {
    console.log('res:', res);
}).catch((err) => {
    console.log('err', err);
})

Promise类方法-reject

reject方法类似于resolve方法,只是会将Promise对象的状态设置为reject状态。

Promise.reject的用法相当于new Promise,并执行reject操作:

const promise = Promise.reject("err message")
// 等价于
const promise = new Promise((resolve ,reject) => {
  reject("err message")
})

需要注意的是,Promise.reject传入的值无论是什么类型,都会直接作为reject状态的参数传递到catch的。

// Promise.reject传入的参数无论是什么形态,都会直接作为reject状态的参数传递到catch的
const promise = Promise.reject("123")
promise.catch((err) => {
    console.log('err:', err);
})

如果reject传入一个promise,那么会直接把这个promise传递到catch

const promise2 = Promise.reject(new Promise( (resolve, reject) => {} ))
promise2.catch((err) => {
    console.log('err:', err);
})

Promise类方法-all

Promise.all的作用是将多个Promise包裹在一起形成一个新的Promise;新的Promise状态由包裹的所有Promise共同决定。

当所有的Promise状态变成fulfilled状态时,新的Promise状态为fulfilled,会将所有Promise的返回值组成一个数组

// 1. 当所有的Promise状态变成fulfilled状态时,新的Promise状态为fulfilled,会将所有Promise的返回值组成一个数组
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(11111)
  }, 1000)
})

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(22222)
  }, 2000)
})

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(33333)
  }, 3000)
})

Promise.all([p1, p2, p3]).then((res) => {
  console.log('res', res);
}).catch((err) => {
  console.log('err', err);
})

由于有定时器,所以会等所有Promise都为fulfilled时,新Promise的状态才会为fulfilled,并打印。

需要注意的是,当有一个Promise状态为reject时,新的Promise状态为reject,并且会将第一个reject的返回值作为参数

// 2.当有一个Promise状态为reject时,新的Promise状态为reject,并且会将第一个reject的返回值作为参数
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(11111)
  }, 1000)
})

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(222)
  }, 2000)
})

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(33333)
  }, 3000)
})

Promise.all([p1, p2, p3]).then((res) => {
  console.log('res', res);
}).catch((err) => {
  console.log('err', err);
})

如果all方法的数组内传入了字符串,那么会将其转化为Promise

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(11111)
    }, 1000)
})
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(22222)
    }, 2000)
})

Promise.all([p1, p2, "123"]).then((res) => {
    console.log('res', res);
}).catch((err) => {
    console.log('err', err);
})

所以上述代码的运行结果为:

Promise类方法-allSettled

all的缺点在于:当有其中一个Promise变成reject状态时,新Promise就会立即变成对应的reject状态。那么对于resolved的,以及依然处于pending状态的Promise,我们是获取不到对应的结果的

因此在ES11(ES2020)中,添加了新的API Promise.allSettled

该方法会在所有的Promise都有结果(settled),无论是fulfilled,还是reject时,才会有最终的状态,并且这个Promise的结果一定是fulfilled的;

最终allSettled的结果是一个数组,数组中存放着每一个Promise的结果,包含status状态,以及对应的value值;

// allSettle方法会在所有的Promise都有结果(settled),无论是fulfilled,还是reject时,才会有最终的状态
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(11111);
  }, 1000);
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(22222);
  }, 2000);
});

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(33333);
  }, 3000);
});

// allSettled的res会返回一个数组,标志每一个promise的状态已经对应的值
Promise.allSettled([p1, p2, p3])
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  });

Promise类方法-race

race是竞赛的意思,所以race方法表示多个Promise相互竞争,谁先有结果,就会决定最终新Promise的状态。

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(11111);
  }, 1000);
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(22222);
  }, 2000);
});

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(33333);
  }, 3000);
});

Promise.race([p1, p2, p3])
  .then((res) => {
    console.log("res", res);
  })
  .catch((err) => {
    console.log("err", err);
  });

运行结果为p1最终的状态:

如果其中一个promise比较快,并且执行reject。那么最终的Promise也是rejected状态

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(11111)
    }, 3000);
  })
  
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
    reject(22222)
}, 500);
})

const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
    resolve(33333)
}, 1000);
})

Promise.race([p1, p2, p3]).then((res) => {
console.log('res', res);
}).catch((err) => {
console.log('err', err);
})

Promise类方法-any

any方法是ES12中新增的方法,和race方法是类似的,但是any方法会等到一个fulfilled状态,才会决定新Promise的状态。(也就是说any希望至少等一个promise状态为fulfilled)

然而如果所有的Promise都是reject的,那么也会等到所有的Promise都变成rejected状态,并报一个AggregateError的错误

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(11111)
    }, 1000)
})

const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(22222)
    }, 2000)
})

const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(33333)
    }, 3000)
})

// any方法会等到一个fulfilled状态,才会决定新Promise的状态
Promise.any([p1, p2, p3]).then((res) => {
    console.log('res', res);
}).catch((err) => {
    console.log('err', err);
})

上述方法虽然p1最先reject,但是any方法会等到p2变为fulfilled状态,才决定新Promise状态,所以最终结果:

res 22222

试验一下全部Promise都为rejected的情况:

// 如果所有的Promise都是reject的,那么也会等到所有的Promise都变成rejected状态,并报一个AggregateError的错误
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(11111)
  }, 1000)
})

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(22222)
  }, 2000)
})

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(33333)
  }, 3000)
})

// any方法会等到一个fulfilled状态,才会决定新Promise的状态
Promise.any([p1, p2, p3]).then((res) => {
  console.log('res', res);
}).catch((err) => {
  console.log('err', err);
})

会等到所有Promise都rejected,再报错误,并且err中含有errors,会包含所有Promise的rejected值