【悄咪咪学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 抛出的异常。

相关推荐
你挚爱的强哥44 分钟前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
前端Hardy1 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189111 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
小镇程序员4 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
疯狂的沙粒5 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪5 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背5 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M5 小时前
node.js第三方Express 框架
前端·javascript·node.js·express
weiabc5 小时前
学习electron
javascript·学习·electron
盛夏绽放5 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js