# Promise

Promise的一些常识及注意点。

# 1、基础

主要用于解决异步问题,比传统的回调函数和事件, 更加合理。

Promise的两个特点

1、Promise对象的状态不受外界影响

1)pending 初始状态

2)fulfilled 成功状态

3)rejected 失败状态

Promise 有以上三种状态,只有异步操作的结果可以决定当前是哪一种状态,其他任何操作都无法改变这个状态

2、Promise的状态一旦改变,就不会再变,任何时候都可以得到这个结果,状态不可逆,只能由 pending变成fulfilled或者由pending变成rejected。也就是说, 如果改变已经发生了, 你再对Promise添加回调函数, 也会立即得到结果(当然这个回调函数的触发是在微任务中)。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

Promise的三个缺点

1)无法取消Promise,一旦新建它就会立即执行,无法中途取消 2)如果不设置回调函数,Promise内部抛出的错误,不会反映到外部, 吞掉错误

try {
  new Promise(resolve => {
    console.log(1);
    throw new Error('error');
    console.log(2);
  });
} catch (error) {
  console.log('catch');
  console.error(error);
}
// 如果不设置回调函数,这个错误就不会被捕获到,上面的try catch 是捕获不到的,必须添加catch回调

3)当处于pending状态时,无法得知目前进展到哪一个阶段,是刚刚开始还是即将完成

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

# 2、基础使用

没啥好说的, 以一个最简单的XHR实例举例即可

function fetch(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onreadystatechange = () => {
      if (xhr.readyState === XMLHttpRequest.DONE) {
        const status = xhr.status;
        if (status === 0 || (status >= 200 && status < 400)) {
          resolve(xhr.responseText);
        } else {
          reject(status);
        }
      }
    };
    xhr.send();
  });
}

fetch('http://www.xxx.com/xxx')
  .then(res => {})
  .catch(error => {});

TIP

注意点:Promise新建后就会立即执行,也就是说Promise的构造函数是同步执行的

resolve或者reject之后的代码如果没有return, 还是会继续执行

一般来说,调用resolvereject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolvereject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。

# 3、resolve的参数是另外一个Promise

const p1 = new Promise(resolve => {
  resolve('p1');
});

const p2 = new Promise(resolve => {
  resolve(p1);
});
p2.then(res => {
  console.log(res);
});	

上述代码,p2的状态由p1来决定

# 4、then、catch

关于这两个的用处就不说, then的链式调用也没什么好说的

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

调用reject()或者抛出一个错误都可以被catch捕获

const p = new Promise(() => {
  throw new Error('test');
});

p.catch(error => {
  console.log(error);
});

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

const p = new Promise(resolve => {
  resolve(1);
});

p.then(
  res => {
    console.log(res);
    throw new Error('error');
  },
  error => {
    console.error(1, error);
  }
);

p.then(res => {
  console.log(res);
  throw new Error('error');
}).catch(error => {
  console.error(2, error);
});

上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch()方法,而不使用then()方法的第二个参数。

# 5、内部错误会被吞掉

WARNING

跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

const p = new Promise(resolve => {
  resolve(x + 2); // ReferenceError: x is not defined
});
p.then(res => {
  console.log('1');
});

console.log('2')

内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined,但是不会退出进程、终止脚本执行。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”, 如果想捕获这种错误,可以使用 unhandledrejection

// 浏览器
window.addEventListener('unhandledrejection', (event) => {
  const { reason } = event;
  console.log(event);
});
// Node.js
process.on('unhandledRejection', function (err, p) {
  throw err;
});

一个测试题, 下面哪个catch可以捕获到抛出的错误

try {
  const p = new Promise(resolve => {
    resolve(2);
    setTimeout(function () {
      throw new Error('1');
    }, 0);
  });
  p.then(res => {
    console.log(res);
  }).catch(error => {
    console.log('error1');
    console.error(error);
  });
} catch (error) {
  console.log('error2');
  console.error(error);
}

确实一个都抓不到, 无论是Promise的catch 还是 try...catch 都抓不到下一轮宏任务抛出的错误

# 6、Promise.prototype.finally

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。

finally的实现

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

# 7、Promise.all()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例, 只有所有的实例都是fullfilled状态, 才会返回。

使用没什么问题,我们自己实现一下

const all = function (promises) {
  if (!promises || promises.length === 0) {
    return Promise.resolve([]);
  }
  return new Promise((resolve, reject) => {
    let count = 0;
    let result = new Array(promises.length);
    promises.forEach((promise, index) => {
      if (!(promise instanceof Promise)) {
        promise = Promise.resolve(promise);
      }
      promise
        .then(res => {
          result[index] = res;
          count++;
          if (count === promises.length) {
            resolve(result);
          }
        })
        .catch(error => {
          reject(error);
        });
    });
  });
};

# 8、Promise.race()

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

实现一个简单版的rance

const race = function (promises) {
  return new Promise((resolve, reject) => {
    promises.forEach(promise => {
      if (!(promise instanceof Promise)) {
        promise = Promise.resolve(promise);
      }
      promise
        .then(res => {
          resolve(res);
        })
        .catch(error => {
          reject(error);
        });
    });
  });
};

使用race封装接口请求超时机制

function fetchApi(url, timeout) {
  return Promise.race([
    fetch(url),
    new Promise((resolve, reject) => {
      setTimeout(() => {
        reject(new Error('request timeout'));
      }, timeout);
    })
  ]);
}

# 9、Promise.allSettled()

用来确定一组异步操作是否都结束了(不管成功或失败)。

简单自己实现一个, 其实不使用finally也可以的, 思路简单的。

const allSettled = function (promises) {
  return new Promise((resolve, reject) => {
    let count = 0;
    let result = [];
    promises.forEach(promise => {
      if (!(promise instanceof Promise)) {
        promise = Promise.resolve(promise);
      }
      promise
        .then(res => {
          result.push(res);
        })
        .catch(error => {
          result.push(error);
        })
        .finally(() => {
          count++;
          if (count === promises.length - 1) {
            resolve(result);
          }
        });
    });
  });
};

# 10、Promise.any()

只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

# 11、打破Promise链式调用

打破Promise链式调用:抛出一个错误或者返回 一个Promise.reject 都行

const p = new Promise((resolve) => {
  resolve(1);
});
p.then((res) => {
  console.log(res);
  return 2;
})
  .then((res) => {
    console.log(res);
    // throw new Error('need break');
    // return Promise.reject('need break');
    return 4;
  })
  .then((res) => {
    console.log(res);
    return 5;
  })
  .catch((err) => {
    console.log(err);
  })
  .finally(() => {
    console.log('finally');
  });

# 12、resolve后面的代码还会执行吗

如果没有return, resolve后面的代码仍然会执行, 只不过此时Promise的状态已经变成fullfilled而已。

# 13、微任务

Promise本身是同步执行函数,当执行到resolve或者reject时, 会把then回调或者catch的任务作为微任务添加到该次宏任务末尾。

所以Promise的回调是在微任务队列处理的,看一个常见的面试题, 说出下面的打印顺序

console.log(1);

Promise.resolve(2).then(res => {
  console.log(res);
});

setTimeout(console.log.bind(null, 3)); // setTimeout 的任务会在下一次宏任务循环处理

console.log(4);

// 1 4 2 3 应该没有疑问

# 14、Async/Await

async 函数返回一个Promise实例,return的值会作为then回调的参数。 如果没有return 会默认返回undefined。 如果在函数内部抛出错误throw new Error(), 这个返回的Promise会变成rejected状态。

搭配await使用,能让代码结构看起来更像是同步的代码,不过需要注意多个promise的串并行问题, 看是使用串行还是并行。 比如下面的伪代码, 总是会在url1的请求完成之后才会请求url2的资源, 这就是串行了。

function getData(url) {
  return new Promise(resolve => {
    fetch(url, res => {
      resolve(res);
    });
  });
}

async function getAllData() {
  const res1 = await getData('url1');
  const res2 = await getData('url2');
}

// 如果想改成并行发出请求, 可以改成下面的样子, 当然也可以基于业务需求使用Promise.all或者Promise.race等工具
async function getAllData() {
  const p1 = getData('url1');
  const p2 = getData('url2');
  const res1 = await p1;
  const res2 = await p2;
}
// 这样子两个getData的函数都是同步并行执行的。


# 15、在TS中使用Promise

主要是如何定义Promise回调函数的参数类型, 也就是then回调的参数类型, 2个办法

  1. 通过构造函数的泛型

    function foo(): Promise<number> {
      return new Promise(resolve => {
        resolve(1);
      });
    }
    // 可以推断res的类型是number
    foo().then(res => {
      console.log(res.toFixed(2));
    });
    
    
  2. 通过定义resolve函数的类型

    function foo() {
      return new Promise((resolve: (val: number) => void) => {
        resolve(1);
      });
    }
    // 可以明确知道res的类型是number
    foo().then(res => {
      console.log(res.toFixed(2));
    });
    

# 16、Promise.resolve()

Promise.resolve() 静态方法以给定值“解决(resolve)”一个 [Promise]。如果该值本身就是一个 Promise,那么该 Promise 将被返回;如果该值是一个 [thenable]对象,Promise.resolve() 将调用其 then() 方法及其两个回调函数;否则,返回的 Promise 将会以该值兑现。

const promise1 = Promise.resolve(123);

promise1.then((value) => {
  console.log(value);
  // Expected output: 123
});

简而言之,Promise.resolve() 返回一个 Promise 对象,其最终状态取决于另一个 Promise 对象、thenable 对象或其他值。

实际上,大部分解决逻辑是由 Promise() 构造函数传递的 resolve 函数 (opens new window)实现的,简单概括如下:

  • 如果传入的是一个非 thenable (opens new window) 对象的值,则返回的 Promise 对象将以该值兑现。
  • 如果传入的是一个 thenable 对象,则通过传入一对解决函数作为参数调用该 thenable 对象的 then 方法后得到的状态将作为返回的 Promise 对象的状态。(但是因为原生的 Promise 直接通过 Promise.resolve() 调用,而不创建封装对象,所以不会在原生 Promise 上调用 then 方法。)如果 resolve 函数接收到另一个 thenable 对象,则会再次进行解决,以确保 Promise 对象的最终兑现值永远不会是 thenable 对象。

# 16.1 resolve 另一个 promise

Promise.resolve() 方法会重用已存在的 Promise 实例。如果它正在解决一个原生的 Promise,它将返回同一 Promise 实例,而不会创建一个封装对象。

const original = Promise.resolve(33);
const cast = Promise.resolve(original);
cast.then((value) => {
  console.log(`值:${value}`);
});
console.log(`original === cast ? ${original === cast}`);

// 按顺序打印:
// original === cast ? true
// 值:33

# 16.2 resolve thenable 对象并抛出错误

resolve thenable 对象

const p1 = Promise.resolve({
  then: (resolve, reject) => {
    resolve('已兑现!');
  }
});
console.log(p1 instanceof Promise); // true,thenable 对象被转换为一个 Promise 对象

p1.then(
  v => {
    console.log(v); // "已兑现!"
  },
  e => {
    // 不会被调用
  }
);

Thenable 在回调之前抛出异常

// Promise 被拒绝
const thenable = {
  then(resolve) {
    throw new TypeError('抛出异常');
    resolve('Resolving');
  }
};
const p2 = Promise.resolve(thenable);
p2.then(
  v => {
    // 不会被调用
  },
  e => {
    console.error(e); // TypeError: 抛出异常
  }
);

Thenable 在回调 Promise 被解决之后抛出异常

const thenable2 = {
  then(onFulfilled) {
    onFulfilled('解决');
    throw new TypeError('Throwing');
  }
};

const p3 = Promise.resolve(thenable2);
p3.then(
  v => {
    console.log(v); // "解决"
  },
  e => {
    // 不会被调用
  }
);

上次更新: 1/22/2025, 9:39:13 AM