JavaScript异步编程——05-回调函数

我们在前面的文章《JavaScript 基础:异步编程/单线程和异步》中讲过,Javascript 是⼀⻔单线程语⾔。早期我们解决异步场景时,⼤部分情况都是通过回调函数来进⾏。

(如果你还不了解单线程和异步的概念,可以先去回顾上一篇文章。)

回调函数的定义

把函数 A 传给另一个函数 B 调用,那么函数 A 就是回调函数。

例如在浏览器中发送 ajax 网络请求,或者在定时器中执行异步任务,就是最常⻅的异步场景。发送请求后,需要等待一段时间,等服务端响应之后我们才能拿到结果。如果我们希望等待异步任务结束之后再执⾏想要的操作,就只能通过回调函数这样的⽅式进⾏处理。

  
const dynamicFunc = function (callback) {
   setTimeout(function () {
     console.log("一开始在这里执行异步任务 task1,延迟3秒执行");
     // task1: total 计数
     let total = 0;
     for (let i = 0; i < 10; i++) {
       total += i;
     }
 ​
     // 等待异步任务 task1 执行完成后,通过回调传入的 callback() 函数,通知外面的调用者,可以开始做后续任务 task2 了
     // 如果有需要的话,可以把 task1 的执行结果 total 传给外面。
     callback && callback(total);
   }, 3000);
 };
 ​
 // 执行同步任务 task2。需要先等 异步任务 task1做完。
 dynamicFunc(function (value) {
   console.log("外面监听到,异步任务 task1已经完成了,并且还能拿到 task1的执行结果 value");
   console.log("task1的返回值value:" + value);
 ​
   // task2:将task1的执行结果乘以2
   const result = value * 2;
   console.log("result:" + result);
 });

上⾯的例⼦中,dynamicFunc() 函数里面的 setTimeout()就是⼀个异步函数,在里面执行了一些异步任务,延迟3秒执行。dynamicFunc() 的参数 callback() 就是一个回调函数。这段代码的诉求是:先等待 异步任务 task1 做完,再做 同步任务task2。我们来分析一下。

已知异步任务 task1 需要3秒才能做完。3秒结束后,通知 dynamicFunc 函数的调用者,里面的异步任务 task1 已经做完了,外面可以开始做后续的任务 task2 了。那要怎么通知呢?在ES5中,最常见的做法就是需要回调传入的 callback 函数(也就是回调函数), 通知外面的调用者。并且,如果有需要的话,外面还可以拿到异步任务task1的执行结果 total(详见代码注释)。

(注:callback这个单词并不是关键字,可以自由命名,我们通常习惯性地用“回调”这个词的英文名 callback 代表回调函数。)

回调函数的异常处理

实际开发中,为什么会经常存在异步任务呢?这是因为,有很多函数在执行时无法立即完成,我们也不知道它什么时候能完成。但是,我们需要等待它完成后,才能做接下来的事情。换句话说,我们接下来要做的事情,需要依赖此前的异步任务。

比如, ajax 网络请求就是典型的异步任务。在渲染一个页面时,我们需要请求接口,获取页面所需要的数据。等接口请求完成、数据准备好之后,前端就可以对数据进行处理,并将数据渲染到页面了。前端做的这部分事情,就是在回调函数里面做。

当然,异步任务在执行时可能出现异常、错误信息、执行失败等等。当出现异常时,往往导致后续的回调函数无法执行。这就需要在异步任务中将异常信息通知给外部。

代码举例如下:

 
// 封装异步任务
 const dynamicFunc = function (number, successCallback, failureCallback) {
   setTimeout(function () {
     console.log('一开始在这里执行异步任务 task1,延迟3秒执行');
     let total = 0;
     for (let i = 0; i < 10; i++) {
       total += i;
     }
     if (number > 0) {
       // 异步任务执行成功
       successCallback && successCallback(total);
     } else {
       // 异步任务执行鼠标
       failureCallback && failureCallback('异步任务执行失败');
     }
   }, 3000);
 };
 ​
 // 执行异步任务:等待 异步任务 执行完成后,再执行回调函数。
 dynamicFunc(
   100,
   (value) => {
     console.log('异步函数调用成功:' + value);
     // task2:将task1的执行结果乘以2
     const result = value * 2;
     console.log('result:' + result);
   },
   (err) => {
     console.log('异步函数调用失败:', err);
   }
 );

处理异步任务的基本模型

我们以“发送网络请求”为例,通过回调函数处理异步任务时,既有请求成功的情况,也有请求失败的情况。其基本处理模型如下:

(1)调用一个异步函数,在这个函数中发送网络请求(也可以用定时器来模拟异步任务)。

(2)如果网络请求成功,则告知调用者请求成功,并将接口返回的数据传递出去。

(3) 如果网络请求失败,则告知调用者发送失败,并将错误信息传递出去。

ES5中,回调函数处理异步任务的基本代码结构如下:

 // ES5中,使用传统的回调函数,处理异步任务的基本模型
 ​
 // 封装异步任务
 function requestData(url, successCallback, failureCallback) {
   const res = {
     retCode: 0,
     data: 'qiangu yihao`s data',
     errMsg: 'network is error',
   };
   setTimeout(() => {
     if (res.retCode == 0) {
       // 网络请求成功
       successCallback(res.data);
     } else {
       // 网络请求失败
       failureCallback(res.errMsg);
     }
   }, 1000);
 }
 ​
 // 调用(请求)异步任务
 requestData(
   'www.qianguyihao.com/xxx',
   // 成功监听
   res => {
     console.log('异步任务执行成功:', res);
   },
   // 失败监听
   err => {
     console.log('异步任务执行失败:', err);
   }
 );

我们一定要记住这个处理模型,它我们学习异步编程的范式之一。如果前端接下来要做的事情需要依赖这个异步任务、需要等待这个异步任务做完之后才能继续,那就符合上面的处理模型。

ES5中,回调的缺点(异步代码的困境)

上面的回调函数的写法,都是ES5的写法。ES5中回调的写法比较直观,不需要 return,层层嵌套即可。但也存在两个问题:

    1. 如果嵌套过深,则会出现回调地狱的问题。

    1. 不同的异步函数,回调的参数,在写法上可能不一致,导致不规范、且需要单独记忆

我们来具体看看这两个问题。

1、回调地狱的问题

如果多个异步任务存在依赖关系(比如,需要等第一个异步任务执行完成后,才能执行第二个异步函数;等第二个异步任务执行完毕后,才能执行第三个异步任务),就需要多个异步任务进⾏层层嵌套,⾮常不利于后续的维护,而且会导致回调地狱(callback hell)的问题。

简而言之,当一个回调函数嵌套另一个回调函数时,就会出现一个嵌套结构。如果嵌套次数过多,就会出现回调地狱的情况。像下面这样:

img

关于回调地狱,我们来举一个形象的例子:

假设买菜、做饭、洗碗、倒厨余垃圾都是异步的。

但真实的场景中,实际的操作流程是:买菜成功之后,才能开始做饭。做饭成功后,才能开始洗碗。洗碗完成后, 再倒厨余垃圾。这里的一系列动作就涉及到了多层嵌套调用,也就是回调地狱。

关于回调地狱,我们来看看几段代码举例。

1.1、定时器的代码举例:(回调地狱)

 setTimeout(function () {
     console.log('qiangu1');
     setTimeout(function () {
         console.log('qiangu2');
         setTimeout(function () {
             console.log('qiangu3');
         }, 3000);
     }, 2000);
 }, 1000);

1.2、Node.js 读取文件的代码举例:(回调地狱)

 fs.readFile(A, 'utf-8', function (err, data) {
     fs.readFile(B, 'utf-8', function (err, data) {
         fs.readFile(C, 'utf-8', function (err, data) {
             fs.readFile(D, 'utf-8', function (err, data) {
                 console.log('qianguyihao:' + data);
             });
         });
     });
 });

上面代码的逻辑为:先读取 A 文本内容,再根据 A 文本内容读取 B,然后再根据 B 的内容读取 C。为了实现这个业务逻辑,上面的代码就很容易形成回调地狱。

1.3、ajax 请求的代码举例:(回调地狱)

 // 伪代码
 ajax('a.json', (res1) => {
     console.log(res1);
     ajax('b.json', (res2) => {
         console.log(res2);
         ajax('c.json', (res3) => {
             console.log(res3);
         });
     });
 });

2、回调写法不一致的问题

我们需要自己去设计回调函数,包括回调函数的参数格式 、调用方式等等。

 // Node.js 读取文件时,成功回调和失败回调,是通过 error参数来区分
 readFile('d:\\readme.text', function (err, data) {
     if (error) {
         console.log('文件读取失败');
     } else {
         console.log('文件读取成功');
     }
 });
 ​
 // jQuery的 ajax 写法中,成功回调和失败回调,是通过两个回调函数来区分
 $.ajax({
     url: '/ajax.json',
     success: function (response) {
         console.log('文件读取成功');
     },
     error: function (err) {
         console.log('文件读取失败');
     },
 });

我们可以看到,上面的回调函数的代码中,成功回调和失败回调,参数的写法不一致。在实战开发中,封装异步函数的人和调用异步函数的人,往往不是同一个人。甚至可能出现的极端的情况是,回调函数里需要传很多参数,参数的顺序也不一致,各有各的风格,每个人写得都不一样。因为这种回调参数的写法不一致、不规范的问题,所以需要单独记忆,导致在调用时需要小心翼翼,很容易出错。

小结

按照上面的分析,在 ES5 中处理异步任务时,产生的这两个问题,ES6 中的 Promise 就可以解决。当然, Promise 的强大功能,不止于此。我们去下一篇文章一探究竟。

赞赏作者

创作不易,你的赞赏和认可,是我更新的最大动力:

希望各位可以点个赞点个关注,这对up真的很重要,谢谢大家啦!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SlowFast报错:ValueError: too many values to unpack (expected 4)

SlowFast报错&#xff1a;ValueError: too many values to unpack (expected 4) 报错细节 File "/home/user/yuanjinmin/SlowFast/tools/visualization.py", line 81, in run_visualizationfor inputs, labels, _, meta in tqdm.tqdm(vis_loader): ValueError: too …

流星烛台如何交易?Anzo Capital昂首资本3步盈利收场

各位投资者通过之前的文章可以准确的辨认出什么是流星烛台了&#xff0c;但是各位投资者一旦遇到流星图案知道怎么交易吗?其实一点都不困难&#xff0c;只要掌握住流星图案的交易真棒&#xff0c;Anzo Capital昂首资本3步就可以盈利收场。 首先&#xff0c;投资者需要确定图…

文件下载出现 IOExpcetion: closed

异常原因截图 : 异常代码位置 : 出现的原因是 使用 try-with-resources, downloadFile 方法执行完毕, 文件流被关闭了...导致前面读取文件字节, 异常提示已经关闭... try (Response response OkHttpUtils.getInstance().client.newCall(new Request.Builder().url(fileUrl)…

Yolov8实现loopy视频识别

1、前言 loopy是一个非常可爱的动漫角色&#xff08;可爱粉色淀粉肠&#xff09;&#xff0c;闲来无事&#xff0c;打算用yolov8训练一个模型对loopy进行识别。 2、准备工作 先在网络上搜寻很多loopy的图片&#xff0c;然后将图片导入Lablel Studio软件进行标注&#xff0c;并…

第1章.STM32单片机入门知识介绍

目录 0. 《STM32单片机自学教程》专栏 1.1 嵌入式系统简介 1.1.1 什么是嵌入式系统 1.1.2 嵌入式系统的特点 1.1.3 嵌入式系统的应用领域 1.2 单片机基本概念 1.3 ARM简介 1.3.1 ARM公司简介 1.3.2 ARM处理器简介 1.4 STM32简介 1.4.1 基于Cortex内核的MCU 1.4.…

springMVC入门学习

目录 1、 什么是springmvc 2、springmvc工作流程 3、 springmvc快速入门&#xff08;XML版本&#xff09; 4、加载自定义目录下的springmvc.xml配置文件 5、 解析器InternalResourceViewResolver 6、 映射器BeanNameUrlHandlerMapping 7、 适配器SimpleControllerHandle…

【算法-程序的灵魂#谭浩强配套】(适合专升本、考研)

无偿分享学习资料&#xff0c;需要的小伙伴评论区或私信dd。。。 无偿分享学习资料&#xff0c;需要的小伙伴评论区或私信dd。。。 无偿分享学习资料&#xff0c;需要的小伙伴评论区或私信dd。。。 完整资料如下&#xff1a; 1.一个程序主要包括以下两方面信息&#xff1a;程…

React:Router-1.BrowserRouter组件式

使用步骤 安装 react-router-dom 依赖 $ npm install react-router-dom6导入 BrowserRouter, Link, Routes, Route 对象 import {BrowserRouter, Link, Routes, Route} from react-router-dom;3.BrowserRouter&#xff1a;history模式路由&#xff1b; HashRouter&#xff1…

Flask gevent启动报错UnicodeDecodeError

文章目录 环境代码报错Track解决思路 环境 acondana 24.1.2python 3.7.13 32bitflask 2.2.3gevent 21.8.0 代码 port 7236 logging.basicConfig(levellogging.INFO, # 控制台打印的日志级别filename./logs/app.log, # 将日志写入log_new.log文件中filemodea, # 模式&…

python笔记:dataclass

1 引子&#xff1a;其他类似实现方法的局限性 假设我们现在需要实现这样的内容&#xff1a; nameChinaarea960population140967 1.1 tuple/list country1_tuple(China,960,140967) country1_tuple[0] #China 缺点&#xff1a;需要记住各个属性是list/tuple第几位的属性&am…

DEV--C++小游戏(吃星星(0.2))

目录 吃星星&#xff08;0.2&#xff09; 简介 分部代码 头文件&#xff08;增&#xff09; 命名空间变量&#xff08;增&#xff09; 副函数&#xff08;新&#xff0c;增&#xff09; 清屏函数 打印地图函数&#xff08;增&#xff09; 移动函数 选择颜色&#xff…

LuaJIT源码分析(三)字符串

LuaJIT源码分析&#xff08;三&#xff09;字符串 要表示一个字符串&#xff0c;核心就是需要知道字符串的长度&#xff0c;以及存放字符串具体数据的地址。lua的字符串是内化不可变的&#xff0c;也就是lua字符串变量存放的不是字符串的拷贝&#xff0c;而是字符串的引用。那么…

OpenNJet产品体验-手把手在Ubuntu20.04系统从零部署到应用OpenNJet

目录 一、引言 二、OpenNJet产品安装 2.1下载OpenNJet安装包 2.2安装OpenNJet V2.0.1 ​2.3快速启动并测试OpenNJet 三、OpenNJet产品应用体验 3.1配置OpenNJet 3.2 部署 Web 应用程序 3.3启动 NJet 3.4访问 Web 应用程序 四、总结 一、引言 OpenNJet应用引擎是高性…

代码随想录算法训练营DAY47|C++动态规划Part8|198.打家劫舍、213.打家劫舍II、198.打家劫舍III

文章目录 198.打家劫舍思路CPP代码 ⭐️213.打家劫舍II解决环的问题思路总结CPP代码 ⭐️198.打家劫舍III思路递归三部曲——确定参数和返回值递归三部曲——确定终止条件递归三部曲——确定单层遍历的逻辑 打印dp数组CPP代码暴力递归记忆化递归 198.打家劫舍 力扣题目链接 文章…

小米手机怎么截屏?一篇文章教会你!

我们经常需要截取手机屏幕上的内容&#xff0c;无论是为了分享、教学还是记录。对于小米手机用户来说&#xff0c;掌握截屏技巧不仅能够方便地记录精彩瞬间&#xff0c;还能在需要时轻松分享或保存屏幕内容。小米手机怎么截屏&#xff0c;有哪些截屏方式呢&#xff1f;本文将为…

Remix中使用 Tailwind

如何Remix 中使用 Tailwindcss Remix是通过 Vite 进行构建的&#xff0c;Vite 原生就支持 PostCSS&#xff0c;通过以下几步就可以设置完成 安装 PostCSS 插件 npm install -D tailwindcss autoprefixer添加PostCSS配置文件 ## postcss.config.js export default {plugins:…

深度学习网络:设计、开发和部署

​书籍&#xff1a;Deep Learning Networks: Design, Development and Deployment 作者&#xff1a;Jayakumar Singaram&#xff0c;S. S. Iyengar&#xff0c;Azad M. Madni 出版&#xff1a;Springer书籍下载-《​深度学习网络&#xff1a;设计、开发和部署》该教材为学生和工…

cesium雷达遮罩(轨迹球效果)

cesium 雷达遮罩(轨迹球效果) 以下为源码直接复制可用 1、实现思路 通过修改“material”材质来实现轨迹球效果 2、代码示例 2.1 index.html <!DOCTYPE html> <html lang="en"><head><!

【刷题篇】双指针(二)

文章目录 1、有效三角形的个数2、查找总价格为目标值的两个商品3、三数之和4、四数之和 1、有效三角形的个数 给定一个包含非负整数的数组 nums &#xff0c;返回其中可以组成三角形三条边的三元组个数。 class Solution { public:int triangleNumber(vector<int>& n…

u盘数据突然不见了怎么回事 u盘数据为什么无故不见如何恢复

当我们把文档资料保存好并复制到U盘后&#xff0c;发现刚才保存的资料没了&#xff0c;并且连文档都不见了&#xff0c;不过文件夹还在U盘。那么U盘数据突然不见了怎么回事呢?U盘数据突然不见了怎么办? 一、U盘数据突然不见了怎么回事 有许多原因可能导致u盘数据无故消失&a…
最新文章