码字,杂谈

真丶深入理解JavaScript异步编程(二):Promise 原理

有了前文的基础,我们深入剖析一下 Promise 的原理。

Promise

由于 JS 的单线程和任务队列,造成了很多函数嵌套,当这种嵌套激增,就会造成所谓的 回调地狱,这是我们深恶痛绝的。

创建一个 Promise

基于几方面原因,JS 催生了 Promise,它解决了很多问题。先看用法:

new Promise(
  (
    resolve, // 成功状态回调
    reject // 失败状态回调
  ) => {
    // 执行体
  }
);

这是一个最基本的创建一个 Promise 的方式。

Promise 的状态

Promise 官方定义了三种状态:

  • pending:准备阶段
  • fulfilled:完成,可以理解为成功
  • rejected:失败,理解为失败

当一个 Promise 被创建之后,它的状态是 pending,此时会执行 执行体 的内容。

当遇到回调时:

  • resolve 会将 Promise 的状态改为成功(fulfilled)
  • reject 会将 Promise 的状态改为失败(rejected)

resolve()reject() 在同一执行体时,只有第一个会被执行。

new Promise((resolve, reject) => {
  // ... 执行逻辑
  resolve("succeed");
});

可能有些人会有疑问了,好像还见过 resolved 状态。这里解释一下,它不是标准状态。总的来说,一个 Promise 被创建之后,一定是 pending,之后无论执行了 resolve() 还是 reject(),状态都属于 resolved,它表示已处理,不会再改变的状态。所以,fulfilledrejected 都属于 resolved 的一种状态。

总结如下:

pending -> resolve() -> fulfilled -> resolved
pending -> reject() -> rejected -> resolved

只有当改变 Promise 状态的时候,才会创建微任务,也就是只有当执行到 resolve()reject() 时,才会有微任务生成。

Promise 状态的中转

resolve()reject() 方法如果返回一个值,它会按照上面的逻辑改变状态。但是返回的是一个 Promise 呢?此时它将返回该 Promise 的状态,而与自身无关。

const p1 = new Promise((resolve, reject) => {
  reject("failed");
});

new Promise((resolve, reject) => {
  resolve(p1); // 虽然我们调用的是成功回调,但是它返回的是 p1 的失败状态
}).then(
  val => console.log("val: " + val),
  err => console.log("err: " + err)
);

此时,其打印的应该是:

err: failed;

使用 then 处理 Promise 的返回状态

上面已经创建了一个 Promise,回调时需要通过 then 方法。

它接收两个参数,一个成功,一个失败。

console.log("start");

let promise = new Promise((resolve, reject) => {
  console.log("promise");
  resolve("succeed");
}).then(
  val => console.log("val: " + val),
  err => console.log("err: " + err)
);

console.log("end");

现在应该很明显可以看出来,它的执行结果应该是:

start;
promise;
end;
val: succeed;

then 的链式操作

一个 Promise 可以有多个 then,这是链式的,这也就解决了我们之前的 回调地狱 的问题。

那么为什么 then 可以是链式操作呢? 因为一个 then 本身也会返回一个 Promise,同时默认执行的是成功状态。

简单理解,就是一个 then 仅仅是对前面的 Promise 的状态的处理。遇到多个 then 时,仅仅是把当前 then 前面的内容看成一个 Promise 即可。

then 状态的传递

如果前一个 then 没有处理对应的状态,那会该状态会自动坠入后面一个 then,这样的方式有点像 try...catch

new Promise((resolve, reject) => {
  reject("failed");
})
  .then(val => console.log("val: " + val))
  .then(null, err => console.log("err: " + err));

很显然,失败状态由第二个 then 来接收。

then 值的传递

如果我们需要在前一个 then 里面处理数据,然后传递给后面一个 then,我们可以通过 return 来进行传递:

new Promise((resolve, reject) => {
  resolve();
})
  .then(
    val => {
      return "jeremyjone";
    },
    err => {}
  )
  .then(val => console.log(val));

此时,我们得到的打印应该是前一个 then 返回的值。

这样的方式提供了链式操作中,分批分次处理数据的需求。

改变 then 的默认状态

前面提到 then 默认返回成功状态,这是它返回一个值的时候。但是当它本身 return 了一个 Promise,则会根据当前返回的 Promise 的状态来向下坠入。

new Promise((resolve, reject) => {
  resolve();
})
  .then(val => {
    return new Promise((resolve, reject) => {
      reject("failed");
    });
  })
  .then(
    val => console.log("val: " + val),
    err => console.log("err: " + err)
  );

此时打印的将会是:

err: failed;

这里有个细节,一定是要将 Promise 返回,如果不是返回的 Promise,那么下一个 then 将处理的就是前一个 then 抛出的 Promise。

then 作为方法单独使用

有时候,需要封装一个类、对象或者方法,来返回给下一个 then。此时可以通过 then 方法来对类、对象或者方法加以封装。

then 方法的用法与 Promise 本身无异,它也接收两个参数,并可以执行参数回调。系统会检查返回的类、对象或者方法中是否包含一个名为 then 的方法,如果有,将会封装为一个 Promise 返回。

new Promise((resolve, reject) => {
  resolve();
})
  .then(val => {
    // 以对象为例
    return {
      then(resolve, reject) {
        resolve("success");
      }
    };
  })
  .then(
    val => console.log("val: " + val),
    err => console.log("err: " + err)
  );

这样的写法大大简化了代码,看上去也更加简洁。

catch 捕获异常

前面 then 方法中都可以用第二个参数来捕获异常,如果有很多 then,同时并不想每一次单独捕获,那么可以在最后添加一个 catch 来一并捕获。

_当然,MARKDOWN_HASHa8982eb2a988054aa4d8d792dd074ccaMARKDOWNHASH 可以放在链式的任意位置,但是通常它放在最后面。

new Promise((resolve, reject) => {
  resolve();
})
  .then(val => console.log(val))
  .then(val => console.log("val: " + val))
  .catch(err => console.log("catch: " + err));

finally 执行

跟异常捕获一样,Promise 的链式也可以有一个 finally 方法,当所有链式执行完成之后,无论成功还是失败,这里的代码都会执行。

new Promise((resolve, reject) => {
  resolve();
})
  .then(val => console.log(val))
  .then(val => console.log("val: " + val))
  .catch(err => console.log("catch: " + err))
  .finally(() => console.log("finally"));

Promise 的方法

除了上面使用 new 关键字声明的 Promise,它本身还提供了一些简写形式。

Promise.resolve

看名字就知道,这是直接返回成功状态的 Promise。

Promise.resolve("jeremyjone").then(val => console.log(val));

很好理解,它相当于实现了一个方法:

Promise.resolve = function (val) {
  return new Promise(resolve => {
    resolve(val);
  });
};

Promise.reject

该方法可以抛出一个 Promise 的异常,用以改变 Promise 本身的状态。

new Promise((resolve, reject) => {
  resolve("jeremyjone");
})
  .then(val => {
    if (val !== "a") {
      return Promise.reject("参数不正确");
    }
    return val;
  })
  .catch(err => console.log(err));

Promise.all

该方法会批量处理多个 Promise,接收一个 Promise 的数组。如果都成功,则返回成功状态;只要有一个错误,就返回错误状态。

所以,参数中的 Promise 最好每一个都处理好失败状态,这样 all 中就可以成功获取数据了。失败的数据经过处理后就返回 undefined

返回的结果也是一个数组,其值对应传入的参数。

看下面的示例作为理解。

1、当都成功时:

const p1 = new Promise((resolve, reject) => {
  resolve("p1");
});

const p2 = new Promise((resolve, reject) => {
  resolve("p2");
});

Promise.all([p1, p2]).then(res => console.log(res)); // ["p1", "p2"]

2、某一个失败时:

const p1 = new Promise((resolve, reject) => {
  reject("p1");
});

const p2 = new Promise((resolve, reject) => {
  resolve("p2");
});

Promise.all([p1, p2]).then(res => console.log(res)); // 返回失败(Uncaught (in promise) p1),此时可以使用 catch 获取失败

3、每一个 Promise 都处理好异常

const p1 = new Promise((resolve, reject) => {
  reject("p1");
}).catch(err => console.log(err));

const p2 = new Promise((resolve, reject) => {
  resolve("p2");
});

Promise.all([p1, p2]).then(res => console.log(res)); // [undefined, "p2"]

这样就可以更好的使用批处理结果了。

Promise.allSettled

该方法比上面的 all 更宽泛一些,无论成功还是失败,它都可以接收。用法是一样的,这里就不再赘述了。

Promise.race

该方法比较有意思,它同样需要一个 Promise 的数组,只不过返回的值比较特殊,哪一个 Promise 返回的快,它就用哪一个。

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

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

Promise.race([p1, p2])
  .then(val => console.log("val: " + val))
  .catch(err => console.log("err: " + err));

上例会返回 p1 的结果,在 catch 语句中执行,打印:

err: p1;

小技巧: 可以配合 Promise.reject 对超时进行处理。

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注