• 39.JavaScript中Promise的基本概念、使用方法,回调地狱规避、链式编程


    在这里插入图片描述


    文章目录

    JavaScript中Promise的基本概念、使用方法,以及回调地狱规避

    本文是上篇《JavaScript异步与回调》的后继,建议先行阅读,以便理解本文的核心内容。

    一、前言

    异步是为了提高CPU的占用率,让其始终处于忙碌状态。

    有些操作(最典型的就是I/O)本身不需要CPU参与,而且非常耗时,如果不使用异步就会形成阻塞状态,CPU空转,页面卡死。

    在异步环境下发生I/O操作,CPU就把I/O工作扔一边(此时I/O由其他控制器接手,仍然在数据传输),然后处理下一个任务,等I/O操作完成后通知CPU(回调就是一种通知方式)回来干活。

    《JavaScript异步与回调》想要表达的核心内容是,异步工作的具体结束时间是不确定的,为了准确的在异步工作完成后进行后继的处理,就需要向异步函数中传入一个回调,从而在完成工作后继续下面的任务。

    虽然回调可以非常简单的实现异步,但是却会由于多重嵌套形成回调地狱。避免回调地狱就需要解嵌套,将嵌套编程改为线性编程。

    PromiseJavaScript中处理回调地狱最优解法。

    二、Promise基本概念

    Promise可以翻译为“承诺”,我们可以通过把异步工作封装称一个Promise,也就是做出一个承诺,承诺在异步工作结束后给出明确的信号!

    Promise语法:

    let promise = new Promise(function(resolve,reject){
        // 异步工作
    })
    
    • 1
    • 2
    • 3

    通过以上语法,我们就可以把异步工作封装成一个Promise。在创建Promise时传入的函数就是处理异步工作的方法,又被称为executor(执行者)。

    resolvereject是由JavaScript自身提供的回调函数,当executor执行完了任务就可以调用:

    • resolve(result)——如果成功完成,并返回结果result
    • reject(error)——如果执行是失败并产生error

    executor会在Promise创建完成后立即自动执行,其执行状态会改变Promise内部属性的状态:

    • state——最初是pending,然后在resolve被调用后转为fulfilled,或者在reject被调用时变为rejected
    • result——最初时undefined,然后在resolve(value)被调用后变为value,或者在reject被调用后变为error;

    2.1 异步工作的封装

    文件模块的fs.readFile就是一个异步函数,我们可以通过在executor中执行文件读取操作,从而实现对异步工作的封装。

    以下代码封装了fs.readFile函数,并使用resolve(data)处理成功结果,使用reject(err)处理失败的结果。

    代码如下:

    let promise = new Promise((resolve, reject) => {
        fs.readFile('1.txt', (err, data) => {
            console.log('读取1.txt')
            if (err) reject(err)
            resolve(data)
        })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果我们执行这段代码,就会输出“读取1.txt”字样,证明在创建Promise后立刻就执行了文件读取操作。

    Promise内部封装的通常都是异步代码,但是并不是只能封装异步代码。

    2.2 Promise执行结果获取

    以上Promise案例封装了读取文件操作,当完成创建后就会立即读取文件。如果想要获取Promise执行的结果,就需要使用thencatchfinally三个方法。

    then

    Promisethen方法可以用来处理Promise执行完成后的工作,它接收两个回调参数,语法如下:

    promise.then(function(result),function(error))
    
    • 1
    • 第一个回调函数用于处理成功执行后的结果,参数result就是resolve接收的值;
    • 第二个回调函数用于处理失败执行后的结果,参数error就是reject接收的参数;

    举例:

    let promise = new Promise((resolve, reject) => {
        fs.readFile('1.txt', (err, data) => {
            console.log('读取1.txt')
            if (err) reject(err)
            resolve(data)
        })
    })
    promise.then(
        (data) => {
            console.log('成功执行,结果是' + data.toString())
        },
        (err) => {
            console.log('执行失败,错误是' + err.message)
        })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    如果文件读取成功执行,会调用第一个函数:

    PS E:CodeNodedemos03-callback> node .index.js
    读取1.txt
    成功执行,结果是1
    
    • 1
    • 2
    • 3

    删掉1.txt,执行失败,就会调用第二个函数:

    PS E:CodeNodedemos03-callback> node .index.js
    读取1.txt
    执行失败,错误是ENOENT: no such file or directory, open 'E:CodeNodedemos-callback.txt'
    
    • 1
    • 2
    • 3

    如果我们只关注成功执行的结果,可以只传入一个回调函数:

    promise.then((data)=>{
        console.log('成功执行,结果是' + data.toString())
    })
    
    • 1
    • 2
    • 3

    到这里我们就是实现了一次文件的异步读取操作。

    catch

    如果我们只关注失败的结果,可以把第一个then的回调传nullpromise.then(null,(err)=>{...})

    亦或者采用更优雅的方式:promise.catch((err)=>{...})

    let promise = new Promise((resolve, reject) => {
        fs.readFile('1.txt', (err, data) => {
            console.log('读取1.txt')
            if (err) reject(err)
            resolve(data)
        })
    })
    promise.catch((err)=>{
        console.log(err.message)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    .catch((err)=>{...})then(null,(err)=>{...})作用完全相同。

    finally

    .finallypromise不论结果如何都会执行的函数,和try...catch...语法中的finally用途一样,都可以处理和结果无关的操作。

    例如:

    new Promise((resolve,reject)=>{
        //something...
    })
    .finally(()=>{console.log('不论结果都要执行')})
    .then(result=>{...}, err=>{...})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • finally回调没有参数,不论成功与否都会执行
    • finally会传递promise的结果,所以在finally后仍然可以.then

    三、使用Promise解决回调地狱

    3.1 回调地狱出现的场景

    现在,我们有一个需求:使用fs.readFile()方法顺序读取10个文件,并把十个文件的内容顺序输出。

    由于fs.readFile()本身是异步的,我们必须使用回调嵌套的方式,代码如下:

    fs.readFile('1.txt', (err, data) => {
        console.log(data.toString()) //1
        fs.readFile('2.txt', (err, data) => {
            console.log(data.toString())
            fs.readFile('3.txt', (err, data) => {
                console.log(data.toString())
                fs.readFile('4.txt', (err, data) => {
                    console.log(data.toString())
                    fs.readFile('5.txt', (err, data) => {
                        console.log(data.toString())
                        fs.readFile('6.txt', (err, data) => {
                            console.log(data.toString())
                            fs.readFile('7.txt', (err, data) => {
                                console.log(data.toString())
                                fs.readFile('8.txt', (err, data) => {
                                    console.log(data.toString())
                                    fs.readFile('9.txt', (err, data) => {
                                        console.log(data.toString())
                                        fs.readFile('10.txt', (err, data) => {
                                            console.log(data.toString())
                                            // ==> 地狱之门
                                        })
                                    })
                                })
                            })
                        })
                    })
                })
            })
        })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    虽然以上代码能够完成任务,但是随着调用嵌套的增加,代码层次变得更深,维护难度也随之增加,尤其是我们使用的是可能包含了很多循环和条件语句的真实代码,而不是例子中简单的 console.log(...)

    3.2 不使用回调产生的后果

    如果我们不使用回调,直接把fs.readFile()顺序的按照如下代码调用一遍,会发生什么呢?

    //注意:这是错误的写法
    fs.readFile('1.txt', (err, data) => {
        console.log(data.toString())
    })
    fs.readFile('2.txt', (err, data) => {
        console.log(data.toString())
    })
    fs.readFile('3.txt', (err, data) => {
        console.log(data.toString())
    })
    fs.readFile('4.txt', (err, data) => {
        console.log(data.toString())
    })
    fs.readFile('5.txt', (err, data) => {
        console.log(data.toString())
    })
    fs.readFile('6.txt', (err, data) => {
        console.log(data.toString())
    })
    fs.readFile('7.txt', (err, data) => {
        console.log(data.toString())
    })
    fs.readFile('8.txt', (err, data) => {
        console.log(data.toString())
    })
    fs.readFile('9.txt', (err, data) => {
        console.log(data.toString())
    })
    fs.readFile('10.txt', (err, data) => {
        console.log(data.toString())
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    以下是我测试的结果(每次执行的结果都是不一样的):

    PS E:CodeNodedemos03-callback> node .index.js
    1
    2
    3
    4
    6
    9
    5
    7
    10
    8
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    产生这种非顺序结果的原因是异步,并非多线程并行,异步在单线程里就可以实现。

    之所以在这里使用这个错误的案例,是为了强调异步的概念,如果不理解为什么会产生这种结果,一定要回头补课了!

    3.3 Promise解决方案

    使用Promise解决异步顺序文件读取的思路:

    1. 封装一个文件读取promise1,并使用resolve返回结果
    2. 使用promise1.then接收并输出文件读取结果
    3. promise1.then中创建一个新的promise2对象,并返回
    4. 调用新的promise2.then接收并输出读取结果
    5. promise2.then中创建一个新的promise3对象,并返回
    6. 调用新的promise3.then接收并输出读取结果

    代码如下:

    let promise1 = new Promise((resolve, reject) => {
        fs.readFile('1.txt', (err, data) => {
            if (err) reject(err)
            resolve(data)
        })
    })
    let promise2 = promise1.then(
        data => {
            console.log(data.toString())
            return new Promise((resolve, reject) => {
                fs.readFile('2.txt', (err, data) => {
                    if (err) reject(err)
                    resolve(data)
                })
            })
        }
    )
    let promise3 = promise2.then(
        data => {
            console.log(data.toString())
            return new Promise((resolve, reject) => {
                fs.readFile('3.txt', (err, data) => {
                    if (err) reject(err)
                    resolve(data)
                })
            })
        }
    )
    let promise4 = promise3.then(
        data => {
            console.log(data.toString())
            //.....
        }
    )
    ... ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    这样我们就把原本嵌套的回调地狱写成了线性模式。

    但是代码还存在一个问题,虽然代码从管理上变的美丽了,但是大大增加了代码的长度。

    3.4 链式编程

    以上代码过于冗长,我们可以通过两个步骤,降低代码量:

    • 封装功能重复的代码,完成文件读取和输出工作
    • 省略中间promise的变量创建,将.then链接起来

    代码如下:

    function myReadFile(path) {
        return new Promise((resolve, reject) => {
            fs.readFile(path, (err, data) => {
                if (err) reject(err)
                console.log(data.toString())
                resolve()
            })
        })
    }
    
    myReadFile('1.txt')
        .then(data => { return myReadFile('2.txt') })
        .then(data => { return myReadFile('3.txt') })
        .then(data => { return myReadFile('4.txt') })
        .then(data => { return myReadFile('5.txt') })
        .then(data => { return myReadFile('6.txt') })
        .then(data => { return myReadFile('7.txt') })
        .then(data => { return myReadFile('8.txt') })
        .then(data => { return myReadFile('9.txt') })
        .then(data => { return myReadFile('10.txt') })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    由于myReadFile方法会返回一个新的Promise,我们可以直接执行.then方法,这种编程方式被称为链式编程

    代码执行结果如下:

    PS E:CodeNodedemos03-callback> node .index.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这样就完成了异步且顺序的文件读取操作。

    注意:在每一步的.then方法中都必须返回一个新的Promise对象,否则接收到的将是上一个旧的Promise

    这是因为每个then方法都会把它的Promise继续向下传递。

    总结

    1. Promise基本概念
    2. Promise的结果获取
    3. 回调地狱的解决
    4. 链式编程
  • 相关阅读:
    竞赛选题 基于设深度学习的人脸性别年龄识别系统
    RISC-V峰会一周年,全志做了这些事
    【软件测试】超细HttpRunner接口自动化框架使用案例,一篇策底打通...
    四旋翼飞行器控制和路径规划附Matlab代码
    3d环形图开发(vue3+vite+ts)
    「开源系统」mybatis-plus代码生成工具(自己基于官方的封装的,打包成了maven插件的方式)
    小车联网—通过ESP8266将速度发送到客户端
    计算机网络——传输层
    C++继承和派生
    【PostgreSQL内核学习(十一)—— (CreatePortal)】
  • 原文地址:https://blog.csdn.net/egegerhn/article/details/126029977