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

news2024/11/19 5:42:22

在这里插入图片描述

  • 《JavaScript再出发》系列文章阅读
  • 《仙宗》发布招仙贴,广招天下道友

文章目录

  • JavaScript中Promise的基本概念、使用方法,以及回调地狱规避
    • 一、前言
    • 二、Promise基本概念
      • 2.1 异步工作的封装
      • 2.2 Promise执行结果获取
        • then
        • catch
        • finally
    • 三、使用Promise解决回调地狱
      • 3.1 回调地狱出现的场景
      • 3.2 不使用回调产生的后果
      • 3.3 Promise解决方案
      • 3.4 链式编程
    • 总结

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){
    // 异步工作
})

通过以上语法,我们就可以把异步工作封装成一个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.txt”字样,证明在创建Promise后立刻就执行了文件读取操作。

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

2.2 Promise执行结果获取

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

then

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

promise.then(function(result),function(error))
  • 第一个回调函数用于处理成功执行后的结果,参数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)
    })

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

PS E:\Code\Node\demos\03-callback> node .\index.js
读取1.txt
成功执行,结果是1

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

PS E:\Code\Node\demos\03-callback> node .\index.js
读取1.txt
执行失败,错误是ENOENT: no such file or directory, open 'E:\Code\Node\demos\03-callback\1.txt'

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

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

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

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)
})

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

finally

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

例如:

new Promise((resolve,reject)=>{
    //something...
})
.finally(()=>{console.log('不论结果都要执行')})
.then(result=>{...}, err=>{...})
  • 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())
                                        // ==> 地狱之门
                                    })
                                })
                            })
                        })
                    })
                })
            })
        })
    })
})

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

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

PS E:\Code\Node\demos\03-callback> node .\index.js
1
2
3
4
6
9
5
7
10
8

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

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

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())
        //.....
    }
)
... ...

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

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

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') })

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

代码执行结果如下:

PS E:\Code\Node\demos\03-callback> node .\index.js
1
2
3
4
5
6
7
8
9
10

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

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

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

总结

  1. Promise基本概念
  2. Promise的结果获取
  3. 回调地狱的解决
  4. 链式编程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/409292.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

qiankun微应用之间、主微应用之间相互跳转方式总结与实践

一、子应用互相访问 1、背景 (1)未来可能需要做不同子应用菜单的合并,如在bi应用下的侧边栏或者别的地方,需要跳转到数据治理的数仓主题里,或者涉及到子应用值改变,其他应用也需要使用; &…

【JWT鉴权】如何来写一个token令牌认证登录?

目录一. 🦁 话题引入1.2 什么是JWT?二. 🦁 技术体现2.1 引入依赖2.2 编写JWT工具类2.3 编写登录方法2.4 编写JWT拦截器验证令牌2.5 编写要配置拦截的接口三. 🦁 话题终结一. 🦁 话题引入 在做项目过程中,我…

vue-element-ui前后端交互实现分页查询

大体思路: ①添加element-ui分页组件 ②在data里定义几个参数用来存放当前页,每页条数,条目总数以及存放后端分页查询的结果 ③后端使用分页查询,controller层接收当前页以及每页条数的参数 ④前端编写方法发送请求到controll…

用jsp实现简单登入注册界面功能(css美化)(软件idea)

思路:创建登入界面(login),再创建登入成功与登入失败界面(loginsuccess与loginfail),再创建注册成功界面(registersuccess)与注册界面(register)以…

用HTML实现简易版计算器

计算器功能:实现了加减乘除、清零、回退、四则运算、幂运算、根式运算等等。 运行结果如下: 引入的图片:back.png HTML部分:用table表格添加计算器的按键,给每个按键设置一个相应的单击事件,点击一个按键后…

RuoYi-Vue——图标使用

使用若依的小伙伴们,是否有遇到找图标的烦恼。若依框架里图标分两种, 1.用在表格中的图标: 2.用在菜单及个人信息的图标: 下面我就针对这两种图标给大家总结一下。 1.表格图标(ElementUI图标Icon) 若依的表…

【JavaScript】五个常用功能/案例:判断特定结尾字符串 | 获取指定字符串 | 颜色字符串转换 | 字符串转驼峰格式 | 简易购物车

CSDN话题挑战赛第2期 参赛话题:学习笔记 🖥️ NodeJS专栏:Node.js从入门到精通 🖥️ 博主的前端之路:前端之行,任重道远(来自大三学长的万字自述) 🖥️ TypeScript知识总…

layui数据表格的使用(前端和后端)

数据表格&#xff08;纯前端&#xff09; 快速使用 首先需要引入layui的css和js <link rel"stylesheet" href"../static/layui/css/layui.css"> <script src"../static/layui/layui.js" charset"utf-8"></script>…

如何搭建vue框架-1

提示&#xff1a;前端新人&#xff0c;初来乍到&#xff0c;若文章写的不好大家多包涵。 文章目录前言一、vue是什么&#xff1f;二、ElementUI是什么&#xff1f;三、搭建前的准备四、安装步骤1.安装webpack2.安装vue-cli3.通过vue-cli构建项目4.启动项目5.其他总结前言 基于…

element-ui表格编辑

前言&#xff1a; 准备&#xff1a; element-uivue3vscode 实现步骤&#xff1a; 数据标定打开激活编辑保存 1. 设定需要编辑的表格单元格是否编辑标识 在请求后台表格数据时进行一个二次包装&#xff0c;如果后端已经处理则不需要在进行包装&#xff08;此处包装是否编辑…

手撕前端面试题【javascript~模板字符串、类继承、参数解析器、生成页码等】

前端的那些基本标签&#x1f353;&#x1f353;模板字符串&#x1f353;&#x1f353;类继承&#x1f353;&#x1f353;参数解析器&#x1f353;&#x1f353;生成页码&#x1f353;&#x1f353;js中哪些操作会造成内存泄漏?html页面的骨架&#xff0c;相当于人的骨头&#…

react router6.x官方DEMO

Tutorial v6.4.2 | React Router 初始化项目 import React from "react"; import ReactDOM from "react-dom/client"; import {createBrowserRouter,RouterProvider,Route, } from "react-router-dom"; import "./index.css";const …

用CSS设置颜色、背景和图像效果

&#x1f4dc;个人简介 ⭐️个人主页&#xff1a;微风洋洋&#x1f64b;‍♂️ &#x1f351;博客领域&#xff1a;编程基础&#x1f4a1;,后端&#x1f4a1;,大数据,信息安全 &#x1f345;写作风格&#xff1a;干货,干货,还是tmd的干货 &#x1f338;精选专栏&#xff1a;【J…

js数组常用方法(19种)|你会的到底有多少呢?

一、改变原数组的方法 1.push&#xff08;&#xff09; 末尾添加数据 语法: 数组名.push(数据) 作用: 就是往数组末尾添加数据 返回值: 就是这个数组的长度 //push var arr [10, 20, 30, 40] res arr.push(20) console.log(arr);//[10,20,30,40,20] console.log(res);//52. …

【Cesium】使用TLE轨道两行数计算轨道信息,并生成CZML格式文件

TLE为轨道两行数&#xff0c;简单的说是用两行数字表示轨道的相关信息&#xff0c;本文即用轨道两行数来计算任一时刻卫星的位置信息和速度信息&#xff0c;并生成CZML文件能够读取的格式 1、satellite.js库简介 简而言之&#xff0c;satellite.js库可以根据TLE轨道两行数&…

vue 项目适配笔记本1920*1080 125%缩放

前言 在台式机上开发pc端项目时&#xff0c;由于是1920*1080的分辨路和100缩放&#xff0c;看起来是没有问题。在笔记本上有问题 因为现在很多14寸的笔记本&#xff0c;出厂默认就是125%或150%的显示。导致很多时候我们的项目&#xff0c;自己开发的时候都是按照100%比例来开发…

Vue使用Serial连接串口

本来只是随手记录一下&#xff0c;发现看的人多了&#xff0c;想着还是修复一下bug吧&#xff0c;供各位看官指正 2022-10-24本次更新: 1、修复在不支持Serial的情况下&#xff0c;控制台报错 2022-09-19本次更新: 1、修复了传输数据接收分隔的情况(增加数据缓存) 2、修复串口连…

【中兴】web训练营~一文带你走进前端 | 百图制作

&#x1f4e2;作者简介&#xff1a;物联网领域创作者&#xff0c;&#x1f3c5;阿里云专家博主&#x1f3c5; &#x1f3c5;华为云享专家&#x1f3c5; ✒️个人主页&#xff1a;Choice~ &#x1f310;格言&#xff1a;可正因为难&#xff0c;才有价值&#xff01;&#x1f536…

Linux 使用Nginx部署web(vue、react)项目

前言 本文基于&#xff1a;操作系统 CentOS 7.6 使用的工具&#xff1a;Xshell7、Xftp7 1.安装所需依赖 安装gcc yum -y install gcc安装pcre、pcre-devel yum -y install pcre pcre-devel安装zlib、zlib-devel yum install -y zlib zlib-devel安装openssl、openssl-dev…

【uni-app】点击左上角返回按钮,弹出弹窗或者是携带参数返回上一页

目录 1、弹出弹窗 2、把这一页的数据带回到上一页&#xff08;获取下一页的数据 &#xff09; 3、跳转页面并携带参数&#xff0c;接受页获取参数 1、弹出弹窗 当我返回上一页的时候需要做一个判断是否需要保存 onBackPress 只支持APP和H5 但不支持小程序 &#xff0c;可以…