【悄咪咪学Node.js】4. Promise 承诺

Promise 承诺

1. 前言

本节课将会引导大家学习了解:

  • JavaScript 为什么需要做流程控制
  • Promise 是什么
  • Promise 的执行过程
  • Promise回调函数 的写法对比
  • Promise 类promise 对象 的 API 解析
  • Promise 怎么解决 回调函数 的缺点

学习完本节课程后,应该具有:

  • 使用 Promise 改造 回调函数 控制执行流程的能力
  • 从零开始编写使用 Promise 的异步函数的能力
  • 巧妙使用 Promise.all()Promise.race() 的能力

2. 常见的流程控制手段

2.1 回调函数

最初,遇到异步问题,常被选用的解决方式叫 回调函数

回调函数就是把异步操作完成后所要执行的代码分装成方法,在异步操作完成之后调用。

回调函数 改写后的代码:

javaScript 复制代码
/**
* 声明 cb 回调函数。
* 在这里,在异步任务完成后,需要打印 end 到控制台
*/
function cb() {
    console.log('end');
}

/* 主函数 */
function main(callback) {
    console.log('start');
    
    setTimeout(function() {
        console.log('running');
        callback();
    }, 1000);
}

main(cb);

结果:

shell 复制代码
start
running
end
  • 先声明一个回调函数
  • 并在调用别的方法时将该回调函数传入
  • 主函数在流程恰当的时候调用传入的回调函数

2.2 Promise

ES6 时,JavaScript 加入了目前常被选用的 Promise

Promise 的出现比较好的解决了前文提到的 回调函数 的两个劣势 回调地狱异常捕获

Promise 改写后的代码:

javaScript 复制代码
/**
* 用 promise 将异步流程包裹起来,并返回 promise 对象
* 在适当的时候,更改 promise 状态
*/
function task() {
    // 返回 promise 对象
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('running');
            // 异步操作完成,更改 promise 对象状态为"已完成"
            resolve();
        }, 1000);
    });
}

这里先做 Promise 控制流程的方案展示,下面用别的例子再作解释。

javaScript 复制代码
console.log('start');
task()
    .then(function() {
        console.log('end');
    });

开始任务流程

  • 在第一行,打印start
  • 在第二行,执行任务,任务将在1秒后打印running
  • 在第三至五行,定义等待任务完成后 ,打印end

结果:

shell 复制代码
start
running
end
javascript 复制代码
那么问题来了:
    Promise 究竟是什么?
    它是怎么做到控制流程的?
    为什么 ES6 将其加入 JavaScript 语法,它有什么优势呢?

3. Promise 是什么

3.1 定义

上面的例子告诉我们,异步执行是 JavaScript 特性,所以在程序流程中,需要控制好异步。

Promise 是 CommonJS 工作组提出的一种规范,成为 ES6 新特性,在 Node.js 4.x 时加入。

其目的是为异步操作提供统一接口,将可读性、可维护性低的代码,统一风格成类同步操作写法。

一个 promise 有:

dart 复制代码
三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
两种转变:等待 => 已完成、等待 => 已拒绝

3.2 思路

Promise(承诺),他的思路是,程序在执行异步操作时,先向上一层返回一个promise(承诺)对象。

  • 一旦异步操作到某一步或已完成,我们可以切换这个promise对象的状态到 已完成 ,通过该promisethen方法定义的 回调函数 将会被触发。
  • 如果异步操作在执行过程中出现异常,我们可以切换这个promise对象的状态到 已拒绝 ,通过该promisecatch方法定义的 异常捕获函数 将会被触发。

我们拿一个贴近生活的例子,很多奶茶店点单后给顾客派送的 圆盘形提醒器,顾客不需要继续排队等待。

奶茶制作 / 派送流程变成:

  1. 客户下单奶茶后,向客户派送提醒器,随后客户即可自由行动。

  2. 奶茶完成后,店员更改某顾客提醒器模式为 亮绿灯并震动

  3. 客户看见提醒器 亮绿灯并震动,则来到取茶窗口取茶。

  4. 如果由于一些原因,导致不能继续完成奶茶制作(用料沽空等),店员更改某顾客提醒器模式为 亮红灯并震动

  5. 客户看见提醒器 亮红灯并震动,则去客户服务窗了解情况并退款。

    在这个例子中: 派送的提醒器就是向上一层返回的promise对象 亮绿灯并震动就是"已完成"状态 亮红灯并震动就是"已拒绝"状态 去取茶窗口就是触发通过promise的then方法定义的回调函数 去客户服务窗就是触发通过promise的catch方法定义的异常处理函数

Promise 配合上 ES7 的 asnyc/await 语法糖,能减少多层嵌套回调产生的"缩进三角",进一步优化代码结构、提升可读性。

3.3 方法详情

promise 对象提供如下API

API 含义 参数 备注
then promise.resolve(n)被调用后触发【状态变成"已完成"后】 function(n)
catch promise.reject(err)被调用后触发【状态变成"已拒绝"后】 function(err)
finally promise.resolve(n) / promise.reject(err)被调用后触发【状态改变后】 function() ES2018新特性,node8.1.4+ 可用

Promise 类提供如下API

| API | 含义 | 参数 |
|---------|-----------------------------------------------------------------------------------------------------|----|---|
| all | 返回一个 promise 对象,并于Array内的所有 promise 完成后状态更改为"已完成",若Array中一个 promise 被拒绝,则所返回的 promise 对象状态更改为"已拒绝"。 | | |
| race | 返回一个 promise 对象,并于Array内的任一 promise 完成后状态更改为"已完成",若Array中任一 promise 被拒绝,则所返回的 promise 对象状态更改为"已拒绝"。 | | |
| resolve | 返回一个以给定值解析后的 promise 对象 | | |
| reject | 返回一个带有拒绝原因的 promise对象 | | |

3.4 常规语法格式

javaScript 复制代码
new Promise(function(resolve, reject) {
    // todo
    ...
})
    // 回调函数
    .then(function(result) {
        // todo
    })
    // 异常捕获函数
    .catch(function(error) {
        // todo
    })

3.5 对比优势

Promise 对比 普通回调函数,有更良好的编写体验和可阅读性。

普通回调函数:

javaScript 复制代码
task1(function() {
    // todo
    ...
    
    task2(function() {
        // todo
        ...
        
        task3(function() {
            // todo
            ...
            
            task4(function() {
                // todo
                ...
            })
        })
    })
})

Promise改写:

javaScript 复制代码
task1()
    .then(function() {
        // todo
        ...
        
        return task2();
    })
    .then(function() {
        // todo
        ...
        
        return task3();
    })
    .then(function() {
        // todo
        ...
        
        return task4();
    })

我们能明显地看出,使用Promise改写后,对多层嵌套的回调函数的编写体验和可阅读性明显地提高。

4. 代码例子

javaScript 复制代码
/**
* generatePromise 方法返回一个promise对象
* 该对象将于2秒后,打印 'promise is running!'并且将状态切换成"已完成"
* 故generatePromise方法调用2秒后,其.then内回调函数将触发
*/
function generatePromise() {
    console.log('promise start!');
    
    let promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('promise is running!');
            resolve('promise resolved');
        }, 2000)
    });
    
    console.log('promise was defined');
    
    return promise;
}

先将异步代码嵌套到Promise中,在异步操作执行完成后,更改该promise对象的状态。并且为了方便理解代码流程,在promise定义前后都进行了打印。

javaScript 复制代码
console.log('demo run!');
generatePromise()
    .then(function(result) {
        console.log(result)
    })

结果打印:

shell 复制代码
demo run!
promise start!
promise was defined
promise is running!
promise resolved

不难看出,promise所包裹的setTimeout是异步方法,并不会阻塞'promise was defined'的打印。而2秒后'promise is running!'打印完成后,其.then()方法才被激活,故'promise resolved'在最后打印。

5. 执行过程

  • 在第一行,打印demo run
  • 在第二行,调用generatePromise函数。
    • 先打印promise start!
    • 由于setTimeout()是异步方法,在promise完成定义后,不会立马执行和阻塞,将直接打印promise was defined
    • promise返回后generatePromise函数执行完毕,由于promise状态未被改变,所以.then()内方法不会被触发。且由于Node.js的event loop仍有任务待执行(setTimeout),此时程序将继续等待。
    • 2秒后,setTimeout内代码被执行:先打印promise is running!,然后将该promise的状态改成 已完成 ,并将promise resolved作为promise的返回值。
  • 在第三至五行,由于promise的状态被改变成 已完成 ,其.then()内方法被触发,打印出返回值promise resolved
rust 复制代码
sequenceDiagram
外部逻辑->>外部逻辑: 打印 "demo run"
外部逻辑->>generatePromise: 调用
generatePromise->>generatePromise: 打印 "promise start!"
generatePromise->>timmer: generatePromise 定义 promise 对象,内部方法生成定时器,造成异步
generatePromise->>generatePromise: 打印 "promise was defined"
generatePromise->>外部逻辑: 返回 promise 对象
外部逻辑->>.then回调函数: 为 promise 对象指定 回调函数
timmer->>timmer: 2秒定时结束,触发定时器代码
timmer->>timmer: 打印 "promise is running!"
timmer->>外部逻辑: 修改 promise 状态为"已完成",同时将 "promise resolved" 作为返回值
外部逻辑->>.then回调函数: 由于 promise 对象状态改变,被触发
.then回调函数->>.then回调函数: 打印返回值 "promise resolved"

6. 应用场景

通常可用于控制异步流程,如等待数据库查询,文件读取,甚至等待接口转发的返回值等。

配合Promise类的race方法,可用于给一些操作配置超时时间。

配合Primise类的all方法,可管理多个异步操作。

7. 示例

示例描述一:写一个被延迟执行的方法,且后续方法能被继续延迟。

不使用promise实现:

javaScript 复制代码
setTimeout(function() {
    console.log('wait 1 second');
    setTimeout(function() {
        console.log('wait 2 more second');
        setTimeout(function() {
            console.log('wait 3 more second');
            setTimeout(function() {
                console.log('wait 4 more second');
            }, 4000);
        }, 3000);
    }, 2000);
}, 1000);

使用 promise 实现:

javaScript 复制代码
function wait(ms) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, ms);
    })
}
javaScript 复制代码
wait(1000)
    .then(function() {
        console.log('wait 1 second');
        return wait(2000);  // 在本轮.then时先完成等待,再触发下轮.then
    })
    .then(function() {
        console.log('wait 2 more seconds');
        return wait(3000);
    })
    .then(function() {
        console.log('wait 3 more seconds');
        return wait(4000);
    })
    .then(function() {
        console.log('wait 4 more seconds');
    })

流程描述:

  • 在第一行,调用wait函数(下文称wait1),等待1秒
  • 在第二行,指定在wait1等待完毕后,执行方法:
    • 打印wait 1 second
    • 再次调用wait函数(下文称wait2),等待2秒,并将该promise返回
  • 在第六行, 由于上一节.then链返回了promise,所以指定在wait2等待完毕后,执行方法:
    • 打印wait 2 more seconds
    • 再次调用wait函数(下文称wait3),等待3秒,并将该promise返回
  • 在第十行, 指定在wait3等待完毕后,执行方法:
    • 打印wait 3 more seconds
    • 再次调用wait函数(下文称wait4),等待4秒,并将该promise返回
  • 在第十四行, 指定在wait4等待完毕后,执行方法:
    • 打印wait 4 more seconds

运行效果展示:

shell 复制代码
wait 1 second
wait 2 more seconds
wait 3 more seconds
wait 4 more seconds

如果不封装使用,将会出现多层嵌套,并且如果多次出现这种需求,setTimeout将会出现多次,甚至多个文件出现。
进行封装后,使用.then链,代码结构获得优化。


示例描述二:使用 Promise.all 管理多个异步操作

代码实现如下:

javaScript 复制代码
function wait(ms) {
    return new Promise(function(resolve, reject) {
    })
}
javaScript 复制代码
let wait1 = wait(1000)
                .then(function() {
                    console.log('wait1 complete');
                }),
    wait2 = wait(2000)
                .then(function() {
                    console.log('wait2 complete');
                });
    
Promise.all([wait1, wait2])
    .then(function() {
        console.log('complete');
    });

流程描述:

  • 在第一行,调用异步wait函数,等待1秒后打印wait1 complete,并将该promise赋值给wait1
  • 在第五行,重复上一步操作,等待2秒后打印wait2 complete,并将该promise赋值给wait2
  • 在第十行,将wait1wait2两个promise组装成数组,推入Promise.all()方法
    • 等待wait1wait2都执行完成后,Promise.all()返回的promise状态更改为 已完成
    • 触发Promise.all().then(),打印complete

运行效果展示:

shell 复制代码
wait1 complete
wait2 complete
complete

效果描述:

  • 1秒时,打印出wait1 complete

  • 2秒时,打印出wait2 complete

  • 2秒时(完成打印wait2 complete后),马上打印出complete

    Promise.all(Array) 在Array内所有promise完成后再触发.then()。


示例描述三:使用 Promise.race 时限超时时间控制

代码实现如下:

javaScript 复制代码
// 定时更改状态的 promise
const E_TIMEOUT = new Error('timeout'); // 定义执行超时异常

/**
* 定义 timeoutLimit 超时监控方法,返回一个 promise 对象
* 到定义的超时时间时,将 promise 的状态改变为 "已拒绝"
*/
function timeoutLimit(ms) {
    return new Promise(function(resolve, reject) {
        setTimeout(reject, ms, E_TIMEOUT);
    })
}

/* 需要被控制执行时间的方法 */
function someFunction() {
    return new Promise(function(resolve, reject) {
        // todo
        ...
        
        resolve();
    })
}
JavaScript 复制代码
Promise.race([someFunction(), timeoutLimit(2000)])
    // 异步操作在超时时间前完成,走.then
    .then(function(res) {
        console.log('task complete');
    })
    // 异步操作没能在超时时间前完成,Promise.race返回的promise对象被timeoutLimit拒绝,走.catch
    .catch(function(err) {
        console.warn(err);
    })

流程描述:

  • 在第一行,将 被控制时间的方法所返回的 promise超时监控方法返回的 promise 组装成数组,推入Promise.race()
  • 在第三行,如果任务能在超时时间内完成,将状态修改为 已完成 ,那么Promise.race()返回的promise的状态也会被修改为 已完成 ,则触发Promise.race().then()
    • 打印task complete
  • 在第七行,如果任务没能在超时时间内完成,则超时监控方法返回的promise的状态修改为 已拒绝 ,那么Promise.race()返回的promise的状态也会被修改为 已拒绝 ,则触发Promise.race().catch()
    • 打印timeout 异常

8. 提示、注意

  1. resolve()、reject() 只用于修改该 promise 状态,后续代码还会继续执行,如需终止,还需要 return。
  2. resolve()、reject() 不需要都使用上,但考虑到代码的健壮性,在可能出现异常的地方捕获异常还是必须的。
  3. 多层嵌套的promise(.then),仅需要在最外层添加一层 .catch 即可捕获所有异常:
javaScript 复制代码
function wait(ms) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, ms);
    })
}

wait(1000)
    .then(function() {
        console.log('wait 1 second');
        return wait(2000);  // 在本轮.then时先完成等待,再触发下轮.then
    })
    .then(function() {
        console.log('wait 2 more seconds');
        throw new Error('test catch')
        return wait(3000);
    })
    .then(function() {
        console.log('wait 3 more seconds');
        return wait(4000);
    })
    .then(function() {
        console.log('wait 4 more seconds');
    })
    .catch(function(error) {
        console.log('catch error:');
        console.log(error);
    })

9. 经验分享

  1. Promise.all(Array) 内 promise 如果出现 / 抛出异常,Promise.all(Array).then()不会触发,而会触发 Promise.all(Array).catch()
  2. Promise.race(Array) 内 promise 就算不是第一个完成,也会执行完所有代码,所以不适用于相互冲突的操作。
  3. promise 可被逐层返回,即:
javaScript 复制代码
function wait(ms) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, ms);
    })
}

wait(1000)
    .then(function() {
        console.log('wait 1 second');
        return wait(2000)   
            .then(function() {
                console.log('wait 2 more seconds');
                return wait(3000)
                    .then(function() {
                        console.log('wait 3 more seconds');
                        // 允许这么返回,且该段等待完成后才触发最外层的.then。但不推荐,因为这种缩进不利于阅读
                        return wait(4000);
                    })
            })
    })
    .then(function() {
        console.log('wait 4 more seconds');    // 最后打印
    })

10. 知识对比

ES6 Promise 和 ES7 async/await 的关系:

  1. async/await 更像在 ES7 设计给 Promise 的语法糖,其主要针对的就是 promise 对象,用于继续优化 promise 的代码结构,更加方便阅读、理解。
  2. 换句话说 没有 promise 的话 async/await 没有意义。

Promise 和 普通回调函数 的区别:

  1. 回调函数 的可阅读性还是稍逊于链式promise
  2. 在实际生产上,如果过多使用 多层嵌套的回调函数 ,很可能坠入"回调地狱",使得开发变慢,并且给阅读代码的人很大的负担。
  3. Promise类 还有如上文提到的.all()、.race()等方法,还可以相对方便做异常捕获,这是 普通回调函数 不能比的。

11. 小结

本节课程我们主要学习了 什么是 PromisePromise 和回调函数的对比Promise 提供的 API

重点如下:

  1. 重点1

    Promise 是 Node.js 做流程控制的工具之一,是开发者面对复杂异步场景理顺编程思路的利器,用好 Promise 是 Node.js 开发者的基础能力体现的地方。

  2. 重点2

    Promise.then() 在该 promise 对象 状态修改为 已完成 时触发。

    Promise.catch() 在该 promise 对象 状态修改为 已拒绝 时触发。

    Promise.all() 返回一个新的 promise 对象 ,在数组中所有 子 promise 对象 状态修改为 已完成 时,新产生的 promise 对象 的状态也会被修改为 已完成 ;如果 子 promise 对象 中出现一个 已拒绝 状态,新产生的 promise 对象 的状态也会被修改为 已拒绝

    Promise.race() 返回一个新的 promise 对象 ,在数组中其中一个 子 promise 对象 状态修改为 已完成 时,新产生的 promise 对象 的状态也会被修改为 已完成 ;如果 子 promise 对象 中最先出现 已拒绝 状态,新产生的 promise 对象 的状态也会被修改为 已拒绝

  3. 重点3

    解决 回调地狱 的方法是使用 .then() 链,代码会由上至下执行。

  4. 重点4

    解决 回调函数异常捕获 失败的方法是使用 .catch(),且 .catch() 能在最外层捕获内层 Prmise 抛出的异常。

相关推荐
ZL不懂前端17 分钟前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x20 分钟前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落1 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
理想不理想v1 小时前
vue经典前端面试题
前端·javascript·vue.js
小阮的学习笔记2 小时前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
YBN娜2 小时前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=2 小时前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
小政爱学习!2 小时前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript