了解 Promise - 上

Promise 对象的构造器

Promise 对象的构造器语法如下:

let promise = new Promise(function (resolve, reject) {
    console.log("执行 1");  // 生产者代码,想象成一个“歌手”
})

传递给 new Promise 的函数被称为 executor
new Promise 被创建,executor 会自动运行。它包含最终应产出结果的生产者代码。按照上面的类比:executor 就是“歌手”。
executor 的参数 resolvereject 是由 JavaScript 自身提供的回调函数。我们的代码仅在 executor 的内部。

当 executor 获得了结果,无论是早还是晚都没关系,它应该调用以下两个回调之一:

  • resolve(value) 任务成功,value 是执行任务得到的结果
  • reject(error) 任务出错,error 是代表错误的对象

总结一下,executor 会自动执行工作任务。任务结束后,成功则调用 resolve,失败调用 reject。

承诺的状态变化

构造器 new Promise 返回的 promise对象实例 有以下两个重要的 内部属性

  • state: 最初是“pending”,当 resolve 被调用时,是“fulfilled”。当 reject 被调用时,是“rejected”
  • result:最初是“undefined”,当 resolve(value) 被调用时其值是 value。当 reject(error) 被调用时,值是“error”。其他方法无法修改 result 值

总结,executor 最终将 promise 变为以下状态之一:

代码举例

一个 promise 构造器和一个简单的 executor 函数:

console.log("主线程 1");
let promise = new Promise(function (resolve, reject) {
    // 当 promise 被构造完成时,自动执行此函数
    console.log("executor 被立即调用");

    // 1000ms 后发出任务完成信号, 成功结果 value 为 "成功完成"
    setTimeout(() => {
        resolve("成功完成");
    }, 1000)
})

console.log("主线程 2");

// 主线程 1
// executor 被立即调用
// 主线程 2

我们可以看出两件事儿:

  • executor 被立即调用(同步执行)
  • resolve 和 reject 两个函数由 JavaScipt 引擎预先定义。我们只需要在合适的时机去调用其中之一即可。

经过 1 秒的 “处理” 后,executor 调用 resolve(“done”) 来产生结果。这将改变 promise 对象的状态:

这是一个成功完成任务的例子,一个“成功实现了的诺言”。

再来一个 executor 以 error 拒绝承诺的例子:

console.log("主线程 1");
let promise = new Promise(function (resolve, reject) {
    console.log("执行了");

    setTimeout(() => {
        reject(new Error("出错了!"));
    }, 1000);
})
console.log("主线程 1");

输出信息如下:

promise 对象的状态:

executor 执行的一般是异步任务,然后调用 resolve 或者 reject 来改变对应的 promise 对象的状态。
一个调用了 resolved 或 rejected 的 promise 都会被称为 “settled”的 promise。

细节

一些细节问题。

状态可以反复横跳吗?

executor 只能调用一个 resolve 或一个 reject 。任何状态的更改都是最终的。
调用之后所有其他的再对 resolve 和 reject 的调用都会被忽略!
例如:

console.log("主线程 1");
let promise = new Promise(function (resolve, reject) {
    console.log("执行");
    setTimeout(() => {
        resolve("任务成功");
        console.log("继续执行 1");
        resolve("再次成功");// 忽略
        console.log("继续执行 2");
    }, 1000);
    setTimeout(() => {
        reject(new Error("任务失败!"));// 忽略
        console.log("继续执行 3");
        reject(new Error("再次失败!"));// 忽略
        console.log("继续执行 4");
    }, 1000);
});
console.log("主线程 2");

// 主线程 1
// 执行
// 主线程 2
// 继续执行 1
// 继续执行 2
// 继续执行 3
// 继续执行 4

宗旨是,一个被 executor 完成的工作只能有一个 resolve 或一个 error。
并且, resolve/reject 只需要一个参数(或不包含任何参数),并且将忽略额外的参数.

reject 参数必须是 Error 对象?

可以使用任何类型的参数来完成(就像 resolve 一样)。
但是建议使用 Error 对象(或继承自 Error 的对象)。这样做的理由很快就会显而易见。

resolve 和 reject 可以立即执行

executor 通常是异步执行某些操作,并在一段时间后调用 resolve/reject ,但这不是必须的。
我们还可以立即调用 resolve 或 reject ,就像这样:

let promise = new Promise(function(resolve, reject) {
    // 不花时间去做这项工作
    resolve(123); // 立即给出结果:123
});

state 和 result 都是内部的

我们无法直接访问它们。但我们可以对它们使用 .then / .catch / .finally 方法

resolve、reject 会导致 executor 函数返回吗

不会。问这个问题看来你还是没真正懂。resolve 与 reject 都是 JavaScript 负责调用的,不会直接导致 executor 函数返回。就算那种没有异步任务,立即执行的 resolve,其后面的代码也会继续执行。

console.log(1);
let promise = new Promise(function (resolve, reject) {
    resolve('DDOONNEE');
    console.log("3");
});
console.log(2);

打印
1
3
2

消费者:then,catch,finally

Promise 对象充当的是 executor(“生产者代码”或“歌手”)和消费函数(“粉丝”)之间的桥梁,消费函数将接收结果或 error。
消费函数一般是:then,catch,finally

then

最重要最基础的一个消费函数

语法:

promise 对象. then(
function(result) { /* 处理成功后的结果 */ },
function(error) { /* 处理错误 */ }
);
  • 第一个参数是一个函数,该函数将在 promise resolved 后运行并接收 result
  • 第二个参数也是一个函数,该函数将在 promise rejected 后运行并接收 error。

示例:

console.log("主线程 1");
let promise = new Promise(function (resolve, reject) {
    setTimeout(() => {
        resolve("任务成功");
    }, 1000);
})
console.log("主线程 2");

promise.then(
    function (result) {
        // 处理成功的结果
        console.log(result);
    },
    function (error) {
        // 处理错误
        console.log(error);
    }
)
console.log("主线程 3");

// 主线程 1
// 主线程 2
// 主线程 3
// 任务成功

可以看出,then 在任务执行结束后被执行,所以也是异步的。
第一个参数函数被执行,在 reject 的情况下,运行第二个:

主线程 1
主线程 2
主线程 3
Error: 出错了!
    at Timeout._onTimeout (E:\My-FrontEND-Way \ 现 
代 JSinfo\JS 篇 \ Promise\1 - 消费者 1-then\0 - 成功时消费
.js:5:16)
    at listOnTimeout (internal/timers.js:549:17) 
    at processTimers (internal/timers.js:492:7) 

如果我们只对成功的情况感兴趣,那么我们可以只为 .then 提供一个函数参数:

let promise = new Promise(resolve => {
    setTimeout(() => resolve("done!"), 1000);
});
promise.then(alert); // 1 秒后显示 "done!"

如果我们只对 error 感兴趣,那么我们可以使用 null 作为第一个参数:

 .then(null,errorHandlingFunction) 。

then 可以写多个,并且状态改变时,都会被调用

let p = Promise.resolve("OKKKK")
p.then(value => {
    console.log("then 1", value);
})
p.then(value => {
    console.log("then 2", value);
})

输出:

then 1 OKKKK
then 2 OKKKK

catch

如果我们只对 error 感兴趣, 也可以使用 .catch(errorHandlingFunction)

.catch(f) 调用是 .then(null, f) 的完全的模拟,它只是一个简写形式。

finally

.finally(f) 调用与 .then(f, f) 类似,在某种意义上, f 总是在 promise 被 settled 时运行:即 promise 被 resolve 或 reject 之后。
finally 是执行清理(cleanup)的很好的处理程序(handler)

console.log("主线程 1");

let promise = new Promise(function (resolve, reject) {
    console.log("任务 1 要执行了");
    setTimeout(() => {
        resolve("我是任务 1 成功执行后的结果");
        // reject("出错了");
    }, 1000);
})

console.log("主线程 2");

promise.finally(function () {
    console.log("清理任务 1 占用的系统资源");
}).then(
    function (result) {
        // 继续处理上一个 promise 的结果
        console.log("pormise 1 的 then 的任务成功处理函数:" + result);
        console.log("pormise 1 的 then 的任务成功处理函数处理完毕");

    },
    function (error) {

    })

console.log("主线程 3");

执行结果:

主线程 1
任务 1 要执行了
主线程 2
主线程 3
清理任务 1 的资源
pormise 1 的 then 的任务成功处理函数: 我是任务 1 成功 成功执行后的结果
pormise 1 的 then 的任务成功处理函数处理完毕     

finally(f) 其实并不是 then(f,f) 的别名。它们之间有一些细微的区别 /

  • finally 的 f 函数没有参数。在 finally 内,我们不知道 promise 是成功还是失败,只知道 promise 被 settled 了
  • finally 将 resolve 接收到的 value 或者 reject 接收到的 error 传递给下一个消费者。

finally 目的并不是处理 promise 的结果。所以将 promise 结果传递给了后面的消费者。

实际代码

接下来,让我们看一下关于 promise 如何帮助我们编写异步代码的。
用于加载脚本的 loadScript 函数,基于回调:

function loadScript(src, callback) {
    let script = document.createElement('script');
    script.src = src;
    script.onload = () => callback(null, script);
    script.onerror = () => callback(new Error(`Script load error for ${src}`));
    document.head.append(script);
}

让我们用 promise 重写它。
新函数 loadScript 将不需要 callback 函数。取而代之的是,它将创建并返回一个在加载完成时解析(resolve)的 promise 对象。外部代码可以使用 .then 向其添加处理程序(订阅函数):

function loadScript(src) {
    return new Promise(function(resolve, reject) {
        let script = document.createElement('script');
        script.src = src;
        script.onload = () => resolve(script);
        script.onerror = () => reject(new Error(`Script load error for ${src}`));
        document.head.append(script);
});
}

之后,可以调用 then:

let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
    script => alert(`${script.src} is loaded!`),
    error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('Another handler...'));

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论。
我的空间