了解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...'));