Promise详解

487 阅读15分钟

期约的意义

在传统的回调函数方式中,为了处理多个异步操作,通常需要在回调函数中嵌套另一个回调函数,导致代码可读性差、调试困难,而期约则可以通过链式调用的方式来避免这些问题。

例如,我们需要从一个 API 获取一组数据,然后对这些数据进行排序并显示在页面上。如果使用传统的回调函数方式,代码可能会类似于以下示例:

getData((data) => {
  processData(data, (processedData) => {
    saveData(processedData, (savedData) => {
      uploadData(savedData, (uploadedData) => {
        console.log('Data uploaded:', uploadedData);
      }, (uploadError) => {
        console.error('Error uploading data:', uploadError);
      });
    }, (saveError) => {
      console.error('Error saving data:', saveError);
    });
  }, (processError) => {
    console.error('Error processing data:', processError);
  });
}, (getError) => {
  console.error('Error getting data:', getError);
});

这种方式的问题在于代码的层次结构深度很大,可读性和可维护性都很差,而且容易出现错误。

使用期约可以将上面的代码改写成以下形式:

getData()
  .then((data) => processData(data))
  .then((processedData) => saveData(processedData))
  .then((savedData) => uploadData(savedData))
  .then((uploadedData) => console.log('Data uploaded:', uploadedData))
  .catch((error) => console.error(error));

通过这种方式,代码的层次结构更加清晰,易于理解和维护,同时错误处理也更加方便。

创建期约

可以使用 Promise 构造函数来创建期约对象。Promise 构造函数接受一个回调函数作为参数,这个回调函数包含两个参数 resolve 和 reject,分别表示期约成功和期约失败时的处理函数。

通常,我们会在这个回调函数中执行异步操作,并在异步操作完成后调用 resolve 或 reject 方法来改变期约的状态。

let promise = new Promise((resolve, reject) => {
  // 执行异步操作
  setTimeout(() => {
    let isSuccess = true; // 模拟异步操作的结果

    if (isSuccess) {
      resolve("操作成功"); // 将期约状态改变为已完成
    } else {
      reject("操作失败"); // 将期约状态改变为已拒绝
    }
  }, 1000);
});

期约的两个作用

期约是一种用于处理异步操作的工具,对于一个异步操作,我们最想知道的是这个操作是否完成,以及这个异步操作最后的结果

状态传递参数传递是期约的两个主要功能,它们分别用于表示异步操作的完成情况和异步操作的结果,可以完美解决我们的需求。

传递状态

期约有三种状态:

  • 未完成(pending):期约对象被创建后,即进入未完成状态。这时期约对象既不是已完成(fulfilled)也不是已拒绝(rejected),它等待异步操作的结果。
  • 已完成(fulfilled):当异步操作成功时,期约对象的状态就会从未完成变为已完成,同时会传递一个成功的结果值。
  • 已拒绝(rejected):当异步操作失败时,期约对象的状态就会从未完成变为已拒绝,同时会传递一个失败的原因。

注意,期约的状态只能从 pending 转变为 fulfilled或 rejected,并且一旦状态发生改变,就不能再次改变。

改变期约状态:

  1. resolve(value):将期约状态从 pending 改变为 fulfilled,并将 value 作为期约的终值(resolved value)。
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Hello, world!');
  }, 1000);
});

console.log(promise); // Promise <fulfilled>
  1. reject(reason):将期约状态从 pending 改变为 rejected,并将 reason 作为期约的拒因(rejected reason)。
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error('Something went wrong!'));
  }, 1000);
});

console.log(promise); // Promise <rejected>
  1. throw(error):与 reject 方法类似,将期约状态从 pending 改变为 rejected,并将 error 对象作为期约的拒因。
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    throw new Error('Something went wrong!');
  }, 1000);
});

console.log(promise); // Promise <rejected>
  1. Promise.resolve(value):返回一个以 value 为终值的已解决期约对象。
const promise = Promise.resolve('Hello, world!');

console.log(promise); // Promise <fulfilled> "Hello, world!"
  1. Promise.reject(reason):返回一个以 reason 为拒因的已拒绝期约对象。
const promise = Promise.reject(new Error('Something went wrong!'));

console.log(promise); // Promise <rejected> Error: Something went wrong!

期约传参

在创建期约时,可以将需要传递的参数作为函数resolve()、reject()的参数进行传递。这些参数可以是任何类型的值,例如字符串、数字、布尔值、对象等等。

例如,下面的代码演示了如何创建一个期约,并通过 resolve() 方法传递一个字符串作为参数:

const promise = new Promise((resolve, reject) => {
  resolve("Hello, World!");
});

promise.then((value) => {
  console.log(value); // 输出 "Hello, World!"
});

在上面的代码中,我们通过 Promise 构造函数创建了一个期约,并在 resolve() 方法中传递了一个字符串 "Hello, World!" 作为参数。然后,在 promise.then() 中使用了回调函数来获取传递的参数并进行处理。

类似地,我们也可以通过 reject() 方法传递一个拒绝原因作为参数:

const promise = new Promise((resolve, reject) => {
  reject(new Error("Something went wrong!"));
});

promise.catch((error) => {
  console.log(error.message); // 输出 "Something went wrong!"
});

在上面的代码中,我们通过 Promise 构造函数创建了一个期约,并在 reject() 方法中传递了一个错误对象作为参数。然后,在 promise.catch() 中使用了回调函数来获取传递的参数并进行处理。

期约的实例方法

1680356939480

then()

then(onFulfilled, onRejected) 方法是用于注册期约成功和失败时的回调函数。它接受两个回调函数参数,一个是 onFulfilled,在期约成功时被调用,另一个是 onRejected,在期约失败时被调用。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Success!");
  }, 1000);
});

promise.then((result) => {
  console.log(result); // Success!
}, (error) => {
  console.log(error);
});

promise.then() 被调用时,会生成一个新的期约,并返回该期约。该期约将根据原始期约的状态then() 方法所提供的处理函数的返回值来确定自己的状态。

以下是 promise.then() 方法生成新期约的详细过程:

  1. promise.then()不传入参数。此时新期约的状态和值与原始期约保持一致。
  2. 如果原始期约对象的状态为 pending,则新的期约对象也将处于 pending 状态,并等待原始期约对象的状态改变。
  3. 如果原始期约对象的状态为 fulfilled,则会执行 onFulfilled 回调函数,并根据 onFulfilled 的返回值来生成一个新的期约对象:
    • 如果 onFulfilled 函数返回一个同步值(即非期约值),则新期约对象将变为 fulfilled 状态,并使用 onFulfilled 函数的返回值作为其值。
    • 如果 onFulfilled 函数返回一个期约值,则新期约对象将变为与返回的期约相同的状态,并使用返回期约的值作为其值。
    • 如果 onFulfilled 函数抛出异常,则新期约对象将变为 rejected 状态,并使用抛出的异常作为其理由。
    • 如果 onFulfilled 函数未返回值或者返回 undefined,则新期约对象将变为 fulfilled 状态,并使用undefined 作为其返回值。
  4. 如果原始期约对象的状态为 rejected,则会执行 onRejected 回调函数,并根据 onRejected 的返回值来生成一个新的期约对象:
    • 如果 onRejected 函数返回一个同步值(即非期约值),则新期约对象将变为 fulfilled 状态,并使用 onRejected 函数的返回值作为其值。
    • 如果 onRejected 函数返回一个期约值,则新期约对象将变为与返回的期约相同的状态,并使用返回期约的值作为其值。
    • 如果 onRejected 函数抛出异常,则新期约对象将变为 rejected 状态,并使用抛出的异常作为其理由。
    • 如果 onRejected 函数未返回值或者返回 undefined,则新期约对象将变为 rejected 状态,并使用undefined 作为其理由。
  5. 如果 onFulfilled 或者 onRejected 函数中有一个或多个返回 undefined,则将忽略这些返回值,并使用原始期约对象的值或者理由作为新期约对象的值或者理由。
// 1. `promise.then()`不传入参数。
const promise = Promise.resolve('hello')
const newPromise = promise.then()
console.log(newPromise === promise) // true,新期约与原始期约相同

// 2. 如果原始期约对象的状态为 `pending`
const promise = new Promise((resolve) => {
  setTimeout(() => {
    resolve('world')
  }, 1000)
})
const newPromise = promise.then()
console.log(newPromise) // Promise <pending>,新期约处于 pending 状态,等待原始期约的状态改变

// 3. 如果原始期约对象的状态为 `fulfilled`
//    - 如果 `onFulfilled` 函数返回一个同步值(即非期约值)
const promise = Promise.resolve('hello')
const newPromise = promise.then((value) => {
  return value.toUpperCase()
})
console.log(newPromise) // Promise <fulfilled>: HELLO,新期约状态为 fulfilled,值为 'HELLO'

//    - 如果 `onFulfilled` 函数返回一个期约值
const promise = Promise.resolve('hello')
const newPromise = promise.then((value) => {
  return Promise.resolve(value.toUpperCase())
})
console.log(newPromise) // Promise <fulfilled>: HELLO,新期约状态为 fulfilled,值为 'HELLO'

//    - 如果 `onFulfilled` 函数抛出异常
const promise = Promise.resolve('hello')
const newPromise = promise.then((value) => {
  throw new Error('something went wrong')
})
console.log(newPromise) // Promise <rejected>: Error: something went wrong,新期约状态为 rejected,理由为抛出的异常

//    - 如果 `onFulfilled` 函数未返回值或者返回 `undefined`
const promise = Promise.resolve('hello')
const newPromise = promise.then((value) => {
  // 没有返回值或者返回 undefined
})
console.log(newPromise) // Promise <fulfilled>: hello,新期约状态为 fulfilled,值为原始期约的值

// 4. 如果原始期约对象的状态为 `rejected`
//    - 如果 `onRejected` 函数返回一个同步值(即非期约值)
const promise = Promise.reject('error');
const newPromise = promise.then(null, (reason) => {
  return `Resolved with ${reason}`;
});
console.log(newPromise); // Promise {<fulfilled>: "Resolved with error"}

//    - 如果 `onRejected` 函数返回一个期约值
const promise = Promise.reject('error');
const anotherPromise = Promise.resolve('hello');
const newPromise = promise.then(null, (reason) => {
  return anotherPromise;
});
console.log(newPromise); // Promise {<resolved>: "hello"}

//    - 如果 `onRejected` 函数抛出异常
const promise = Promise.reject('error');
const newPromise = promise.then(null, (reason) => {
  throw new Error(`Rejected with ${reason}`);
});
console.log(newPromise); // Promise {<rejected>: Error: Rejected with error}

//    - 如果 `onRejected` 函数未返回值或者返回 `undefined`
const promise = Promise.reject('error');
const newPromise = promise.then(null, (reason) => {
  return;
});
console.log(newPromise); // Promise {<rejected>: "error"}

// 5. 如果 `onFulfilled` 或者 `onRejected` 函数中有一个或多个返回 `undefined`
const promise = Promise.resolve('hello');
const newPromise = promise.then((value) => {
  console.log(value);
  return undefined;
});
console.log(newPromise); // Promise {<fulfilled>: "hello"}
// console output: hello

catch()

catch(onRejected) 方法是 then(null, onRejected) 的别名,用于捕获期约失败时的错误。它返回一个新的期约,可以被用于链式调用。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("Error!");
  }, 1000);
});

promise.catch((error) => {
  console.log(error); // Error!
});

事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype. then(null, onRejected)。

下面的代码展示了这两种同样的情况:

let p = Promise.reject();  
let onRejected = function(e) {  
 setTimeout(console.log, 0, 'rejected');  
};  

// 这两种添加拒绝处理程序的方式是一样的: 
p.then(null, onRejected); // rejected  
p.catch(onRejected); // rejected

finally()

finally(onFinally) 方法在期约无论成功或失败时都会执行,可以用于清理资源或做一些必要的操作。它接受一个回调函数参数,无论期约状态如何都会被调用,但它不会改变期约状态,也不会影响期约返回值。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Success!");
  }, 1000);
});

promise.finally(() => {
  console.log("Promise ended.");
});

Promise.all()

Promise.all(iterable) 方法接受一个可迭代对象参数,返回一个新的期约,只有在所有的期约都成功时才会成功,并返回一个由所有期约结果组成的数组。如果其中一个期约失败,则整个期约失败,并返回第一个被拒绝的期约的结果或原因。

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Result 1");
  }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Result 2");
  }, 2000);
});

Promise.all([promise1, promise2]).then((results) => {
  console.log(results); // ["Result 1", "Result 2"]
});

拓展,实现Promise.all() (大厂面试必备):

/*
注意点:
1.函数返回的是一个Promise对象
2.最好判断一下传入的参数是否为数组
3.并不是push进result数组的,而是通过下标的方式进行存储,这是因为我们为了保证输出的顺序,因为Promise对象执行的时间可能不同,push的话会破坏顺序。
4.通过计数标志来判断是否所有的promise对象都执行完毕了,因为在then中表示该promise对象已经执行完毕。
*/

function promiseAll(promises) {
  // 返回一个新的Promise对象
  return new Promise((resolve, reject) => {
    // 参数不是数组,则直接抛出错误
    if (!Array.isArray(promises)) {
      return reject(new Error('参数必须是数组'));
    }
    
    // 初始化变量
    const results = [];
    let counter = 0;

    // 遍历Promise数组
    promises.forEach((promise, index) => {
      // 使用Promise.resolve()保证每个元素都是Promise对象
      Promise.resolve(promise)
        // 所有Promise对象都已经resolve,将结果数组传递给下一个then方法
        .then((result) => {
          results[index] = result;
          counter++;
          if (counter === promises.length) {
            resolve(results);
          }
        })
        // 如果有一个Promise对象reject,整个Promise.all()返回的Promise对象就reject
        .catch((error) => {
          reject(error);
        });
    });
  });
}

Promise.race()

Promise.race(iterable) 方法接受一个可迭代对象参数,返回一个新的期约,只要其中一个期约成功或失败,整个期约就会立即返回,并返回第一个成功或失败的期约的结果或原因。

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Result 1");
  }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Result 2");
  }, 500);
});

Promise.race([promise1, promise2]).then((result) => {
  console.log(result); // "Result 2"
});

Promise.allSettled()

Promise.allSettled(iterable) 方法接受一个可迭代对象参数,返回一个新的期约,在所有的期约完成后返回一个由所有期约的状态信息组成的数组,数组的每个元素都包含以下属性:

  • status: 取值为 "fulfilled" 或 "rejected",表示相应期约的状态。
  • value (可选): 当期约状态为 "fulfilled" 时,为相应期约的结果值。
  • reason (可选): 当期约状态为 "rejected" 时,为相应期约的拒绝原因。
const p1 = Promise.resolve(1);
const p2 = Promise.reject("Error");
const p3 = Promise.resolve("Hello");

Promise.allSettled([p1, p2, p3]).then(results => {
  console.log(results);
});


//输出结果:
[
  { status: 'fulfilled', value: 1 },
  { status: 'rejected', reason: 'Error' },
  { status: 'fulfilled', value: 'Hello' }
]

期约连锁

Promise 链中,每个 then()catch() 方法返回一个新的 Promise 对象,因此,我们可以通过不断地调用 then() 方法来链接多个异步操作,实现一系列复杂的异步操作逻辑。

下面是一个简单的 Promise 连锁的示例:

asyncFunction()
  .then(result => {
    return anotherAsyncFunction(result);
  })
  .then(anotherResult => {
    return yetAnotherAsyncFunction(anotherResult);
  })
  .then(finalResult => {
    console.log(finalResult);
  })
  .catch(error => {
    console.error(error);
  });

多个 then 处理程序的执行顺序是按照它们注册的顺序依次执行的,以下是一个例子,展示了多个 then 处理程序的执行顺序:

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Hello')
  }, 1000)
})

myPromise
  .then((result) => {
    console.log(result)
    return result + ', world!'
  })
  .then((result) => {
    console.log(result)
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('Goodbye')
      }, 1000)
    })
  })
  .then((result) => {
    console.log(result)
  })

myPromise 状态变为 fulfilled 时,第一个处理程序将被执行,它将接收到字符串 'Hello' 作为参数并输出到控制台。

接下来,第二个处理程序将被执行,它将接收到字符串 'Hello, world!' 作为参数并输出到控制台。

最后,第三个处理程序将被执行,它将接收到一个新的期约对象作为参数,并在该期约的状态变为 fulfilled 时输出 'Goodbye' 到控制台。

非重入期约方法(面试必考)

什么是“非重入”(non-reentrancy)

非重入指当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处 理程序的代码之后的同步代码一定会在处理程序之前先执行。

下面的例子演示了这个特性:

// 创建解决的期约
let p = Promise.resolve(); 

// 添加解决处理程序
// 直觉上,这个处理程序会等期约一解决就执行
p.then(() => console.log('onResolved handler')); 

// 同步输出,证明 then()已经返回
console.log('then() returns'); 

// 实际的输出:
// then() returns 
// onResolved handler

非重入期约方法和事件循环机制密切相关,因为事件循环机制控制着 JavaScript 程序的执行顺序。

在 JavaScript 中,事件循环是一种机制,它可以帮助我们在处理异步任务时,避免阻塞主线程。在事件循环中,所有的任务都被分为两类:宏任务和微任务。

宏任务主要有:script 代码、setTimeout、setInterval、I/O 操作等

微任务主要有:promise 回调函数(then()、catch()、finally())、MutationObserver 回调函数、process.nextTick 等

事件循环的过程如下:

  1. 整个script脚本作为一个宏任务执行
  2. 先执行所有同步任务,碰到异步任务放到任务队列中
  3. 同步任务执行完毕,开始执行当前所有的异步任务
  4. 先执行任务队列里面所有的微任务,当前微任务产生的微任务push到微任务任务队列中
  5. 然后执行一个宏任务,当前宏任务产生的微任务push到微任务任务队列中,产生的宏任务push到宏任务任务队列中
  6. 然后再执行所有的微任务
  7. 再执行一个宏任务,再执行所有的微任务·······依次类推到执行结束。

1680361335450

非重入期约方法和事件循环机制的关系在于,非重入期约方法中的回调函数(即 then()catch()finally() 中注册的函数)会被添加到微任务队列中。

这个特性让我们可以使用非重入期约方法来创建异步任务,并且可以在异步任务执行完成后立即进行下一步操作,而不需要等待整个事件循环周期结束。同时,非重入期约方法也保证了异步操作的顺序,避免了在异步操作完成之前执行后续操作的情况。

面试考察方式,写出执行顺序:

//1
console.log('1');    
//2
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
//3
process.nextTick(function() {
    console.log('6');
})
//4
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})
//5
setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

// 先执行1 输出1
// 执行到2,把setTimeout放入异步的任务队列中(宏任务)
// 执行到3,把process.nextTick放入异步任务队列中(微任务)
// 执行到4,上面提到promise里面是同步任务,所以输出7,再将then放入异步任务队列中(微任务)
// 执行到5,同2
// 上面的同步任务全部完成,开始进行异步任务
// 先执行微任务,发现里面有两个微任务,分别是3,4压入的,所以输出6 8
// 再执行一个宏任务,也就是第一个setTimeout
// 先输出2,把process.nextTick放入微任务中,再如上promise先输出4,再将then放入微任务中
// 再执行所以微任务输出输出3 5
// 同样的,再执行一个宏任务setTImeout2,输出9 11 在执行微任务输出10 12
// 所以最好的顺序为:1 7 6 8 2 4 3 5 9 11 10 12