【译】JavaScript中的Callbacks

简介: 你是否遇到过"callbacks"一词,但是不知道这意味着什么?别着急。你不是一个人。许多JavaScript的新手发现回调也很难理解。

你是否遇到过"callbacks"一词,但是不知道这意味着什么?别着急。你不是一个人。许多JavaScript的新手发现回调也很难理解。


尽管callbacks可能令人疑惑,但是你仍然需要彻底了解它们,因为它们是JavaScript中的一个重要的概念。如果你不知道callbacks,你不可能走得很远🙁。


这就是今天的文章(要讲的)!你将了解callbacks是什么,为什么它们很重要,以及如何使用它们。😄


备注:你会在这篇文章中看到ES6箭头函数。如果你不是很熟悉它们,我建议你在往下读之前复习一下ES6这篇文章(只了解箭头函数部分就可以了)。


callbacks是什么?


callback是作为稍后要执行的参数传递给另一个函数的函数。(开发人员说你在执行函数时“调用”一个函数,这就是被命名为回调函数的原因)。


它们在JavaScript中很常见,你可能自己潜意识的使用了它们而不知道它们被称为回调函数。


接受函数回调的一个示例是addEventLisnter:


const button = document.querySelector('button')
button.addEventListener('click', function(e) {
  // Adds clicked class to button
  this.classList.add('clicked')
})


看不出是回调函数吗?那么,这种写法怎样?


const button = document.querySelector('button')
// Function that adds 'clicked' class to the element
function clicked (e) {
  this.classList.add('clicked')
}
// Adds click function as a callback to the event listener
button.addEventListener('click', clicked)


在这里,我们告诉JavaScript监听按钮上的click事件。如果检测到点击,则JavaScript应触发clicked函数。因此,在这种情况下,clicked是回调函数,而addEventListener是一个接受回调的函数。


现在,你明白什么是回调函数了嘛?:)


我们来看另外一个例子。这一次,假设你希望通过过滤一组数据来获取小于5的列表。在这里,你将回调函数传递给filter函数:


const numbers = [3, 4, 10, 20]
const lesserThanFive = numbers.filter(num => num < 5)


现在,如果你想通过命名函数执行上面的代码,则过滤函数将如下所示:


const numbers = [3, 4, 10, 20]
const getLessThanFive = num => num < 5
// Passing getLessThanFive function into filter
const lesserThanFive = numbers.filter(getLessThanFive)


在这种情况下,getLessThanFive是回调函数。Array.filter是一个接受回调的函数。

现在明白为什么了吧?一旦你知道回调函数是什么,它们就无处不在!


下面的示例向你展示如何编写回调函数和接受回调的函数:


// Create a function that accepts another function as an argument
const callbackAcceptingFunction = (fn) => {
  // Calls the function with any required arguments
  return fn(1, 2, 3)
}
// Callback gets arguments from the above call
const callback = (arg1, arg2, arg3) => {
  return arg1 + arg2 + arg3
}
// Passing a callback into a callback accepting function
const result = callbackAcceptingFunction(callback)
console.log(result) // 6


请注意,当你将回调函数传递给另一个函数时,你只传递该函数的引用(并没有执行它,因此没有括号()


const result = callbackAcceptingFunction(callback)


你只能在callbackAcceptingFunction中唤醒(调用)回调函数。执行此操作时,你可以传递回调函数可能需要的任意数量的参数:


const callbackAcceptingFunction = (fn) => {
  // Calls the callback with three args
  fn(1, 2, 3)
}


这些由callbackAcceptingFunction传递给回调函数的参数,然后再通过回调函数(执行):


// Callback gets arguments from callbackAcceptingFunction
const callback = (arg1, arg2, arg3) => {
  return arg1 + arg2 + arg3
}


这是回调的解剖。现在,你应该知道addEventListener包含一个event参数:)


// Now you know where this event object comes from! :)
button.addEventListener('click', (event) => {
  event.preventDefault()
})


唷!这是callbacks的基本思路!只需要记住其关键:将一个函数传递给另一个函数,然后,你会想起我上面提到的机制。


旁注:这种传递函数的能力是一件很重要的事情。它是如此重要,以至于说JavaScript中的函数是高阶函数。高阶函数在编程范例中称为函数编程,是一件很重大的事情。


但这是另一天的话题。现在,我确信你已经开始明白callbacks是什么,以及它们是如何被使用的。但是为什么?你为什么需要callbacks呢?


为什么使用callbacks


回调函数以两种不同的方式使用 -- 在同步函数和异步函数中。


同步函数中的回调


如果你的代码从上到下,从左到右的方式顺序执行,等待上一个代码执行之后,再执行下一行代码,则你的代码是同步的


让我们看一个示例,以便更容易理解:


const addOne = (n) => n + 1
addOne(1) // 2
addOne(2) // 3
addOne(3) // 4
addOne(4) // 5


在上面的例子中,addOne(1)首先执行。一旦它执行完,addOne(2)开始执行。一旦addOne(2)执行完,addOne(3)执行。这个过程一直持续到最后一行代码执行完毕。


当你希望将部分代码与其它代码轻松交换时,回调将用于同步函数。


所以,回到上面的Array.filter示例中,尽管我们将数组过滤为包含小于5的数组,但你可以轻松地重用Array.filter来获取大于10的数字数组:


const numbers = [3, 4, 10, 20]
const getLessThanFive = num => num < 5
const getMoreThanTen = num => num > 10
// Passing getLessThanFive function into filter
const lesserThanFive = numbers.filter(getLessThanFive)
// Passing getMoreThanTen function into filter
const moreThanTen = numbers.filter(getMoreThanTen)


这就是为什么你在同步函数中使用回调函数的原因。现在,让我们继续看看为什么我们在异步函数中使用回调。


异步函数中的回调


这里的异步意味着,如果JavaScript需要等待某些事情完成,它将在等待时执行给予它的其余任务。


异步函数的一个示例是setTimeout。它接受一个回调函数以便稍后执行:


// Calls the callback after 1 second
setTimeout(callback, 1000)


如果你给JavaScript另外一个任务需要完成,让我们看看setTimeout是如何工作的:


const tenSecondsLater = _ = > console.log('10 seconds passed!')
setTimeout(tenSecondsLater, 10000)
console.log('Start!')


在上面的代码中,JavaScript会执行setTimeout。然后,它会等待10秒,之后打印出"10 seconds passed!"的消息。


同时,在等待setTimeout10秒内完成时,JavaScript执行console.log("Start!")


所以,如果你(在控制台上)打印上面的代码,这就是你会看到的:


// What happens:
// > Start! (almost immediately)
// > 10 seconds passed! (after ten seconds)


啊~异步操作听起来很复杂,不是吗?但为什么我们在JavaScript中频繁使用它呢?


要了解为什么异步操作很重要呢?想象一下JavaScript是你家中的机器人助手。这个助手非常愚蠢。它一次只能做一件事。(此行为被称为单线程)。


假设你告诉你的机器人助手为你订购一些披萨。但机器人是如此的愚蠢,在打电话给披萨店之后,机器人坐在你家门前,等待披萨送达。在此期间它无法做任何其它事情。


你不能叫它去熨衣服,拖地或在等待(披萨到来)的时候做任何事情。(可能)你需要等20分钟,直到披萨到来,它才愿意做其他事情...


此行为称为阻塞。当你等待某些内容完成时,其他操作将被阻止。


const orderPizza = flavour => {
  callPizzaShop(`I want a ${flavour} pizza`)
  waits20minsForPizzaToCome() // Nothing else can happen here
  bringPizzaToYou()
}
orderPizza('Hawaiian')
// These two only starts after orderPizza is completed
mopFloor()
ironClothes()


而阻止操作是一个无赖。🙁


为什么?


让我们把愚蠢的机器人助手放到浏览器的上下文中。想象一下,当单击按钮时,你告诉它更改按钮的颜色。


这个愚蠢的机器人会做什么?


它专注于按钮,忽略所有命令,直到按钮被点击。同时,用户无法选择任何其他内容。看看它都在干嘛了?这就是异步编程在


JavaScript中如此重要的原因。


但是,要真正了解异步操作期间发生的事情,我们需要引入另外一个东西 -- 事件循环。


事件循环


为了设想事件循环,想象一下JavaScript是一个携带todo-list的管家。此列表包含你告诉它要做的所有事情。然后,JavaScript将按照你提供的顺序逐个遍历列表。


假设你给JavaScript下面五个命令:


const addOne = (n) => n + 1
addOne(1) // 2
addOne(2) // 3
addOne(3) // 4
addOne(4) // 5
addOne(5) // 6


这是JavaScript的待办事项列表中出现的内容。


image.png


相关命令在JavaScript待办事项列表中同步出现。


除了todo-list之外,JavaScript还保留一个waiting-list来跟踪它需要等待的事情。如果你告诉JavaScript订购披萨,它会打电话给披萨店并在等候列表名单中添加“等待披萨到达”(的指令)。与此同时,它还会做了其他已经在todo-list上的事情。


所以,想象下你有下面代码:


const orderPizza (flavor, callback) {
  callPizzaShop(`I want a ${flavor} pizza`)
  // Note: these three lines is pseudo code, not actual JavaScript
  whenPizzaComesBack {
    callback()
  }
}
const layTheTable = _ => console.log('laying the table')
orderPizza('Hawaiian', layTheTable)
mopFloor()
ironClothes()


JavaScript的初始化todo-list如下:


image.png


订披萨,拖地和熨衣服!😄


然后,在执行orderPizza时,JavaScript知道它需要等待披萨送达。因此,它会在执行其余任务时,将“等待披萨送达”(的指令)添加到waiting list上。


image.png


JavaScript等待披萨到达


当披萨到达时,门铃会通知JavaScript,当它完成其余杂务时。它会做个**心理记录(mental note)**去执行layTheTable


image.png


JavaScript知道它需要通过在其 mental note 中添加命令来执行layTheTable


然后,一旦完成其他杂务,JavaScript就会执行回调函数layTheTable


image.png


其他所有内容完成后,JavaScript就会去布置桌面(layTheTable)


我的朋友,这个就被称为事件循环。你可以使用事件循环中的实际关键字替换我们的管家,类比来理解所有的内容:


  • Todo-list -> Call stack
  • Waiting-list -> Web apis
  • Mental note -> Event queue


image.png


JavaScript的事件循环


如果你有20分钟的空余时间,我强烈建议你观看Philip Roberts 在JSconf中谈论的事件循环。它将帮助你理解事件循环的细节。


厄...那么,为什么callbacks那么重要呢?


哦~我们在事件循环绕了一大圈。我们回正题吧😂。


之前,我们提到如果JavaScript专注于按钮并忽略所有其他命令,那将是不好的。是吧?


通过异步回调,我们可以提前提供JavaScript指令而无需停止整个操作


现在,当你要求JavaScript查看点击按钮时,它会将“监听按钮”(指令)放入waiting list中并继续进行杂务。当按钮最终获得点击时,JavaScript会激活回调,然后继续执行。


以下是回调中的一些常见用法,用于告诉JavaScript要做什么...


  1. 当事件触发时(比如addEventListener
  2. 在AJAX调用后(比如jQuery.ajax
  3. 在读/写文件之后(比如fs.readFile


// Callbacks in event listeners
document.addEventListener(button, highlightTheButton)
document.removeEventListener(button, highlightTheButton)
// Callbacks in jQuery's ajax method
$.ajax('some-url', {
  success (data) { /* success callback */ },
  error (err) { /* error callback */}
});
// Callbacks in Node
fs.readFile('pathToDirectory', (err, data) => {
  if (err) throw err
  console.log(data)
})
// Callbacks in ExpressJS
app.get('/', (req, res) => res.sendFile(index.html))


这就是它(异步)的回调!😄

希望你清楚callbacks是什么以及现在如何使用它们。在开始的时候,你不会创建很多回调,所以要专注于学习如何使用可用的回调函数。


现在,在我们结束(本文)之前,让我们看一下开发人员(使用)回调的第一个问题 -- 回调地狱。


回调地狱


回调地狱是一种多次回调相互嵌套的现象。当你执行依赖于先前异步活动的异步活动时,可能会发生这种情况。这些嵌套的回调使代码更难阅读。


根据我的经验,你只会在Node中看到回调地狱。在使用前端JavaScript时,你几乎从不会遇到回调地狱。


下面是一个回调地狱的例子:


// Look at three layers of callback in this code!
app.get('/', function (req, res) {
  Users.findOne({ _id:req.body.id }, function (err, user) {
    if (user) {
      user.update({/* params to update */}, function (err, document) {
        res.json({user: document})
      })
    } else {
      user.create(req.body, function(err, document) {
        res.json({user: document})
      })
    }
  })
})


而现在,你有个挑战 -- 尝试一目了然地破译上面的代码。很难,不是吗?难怪开发者在看到嵌套回调时会不寒而栗。


克服回调地狱的一个解决方案是将回调函数分解为更小的部分以减少嵌套代码的数量:


const updateUser = (req, res) => {
  user.update({/* params to update */}, function () {
    if (err) throw err;
    return res.json(user)
  })
}
const createUser = (req, res, err, user) => {
  user.create(req.body, function(err, user) {
    res.json(user)
  })
}
app.get('/', function (req, res) {
  Users.findOne({ _id:req.body.id }, (err, user) => {
    if (err) throw err
    if (user) {
      updateUser(req, res)
    } else {
      createUser(req, res)
    }
  })
})


更容易阅读了,是吧?


还有其他解决方案来对抗新版JavaScript中的回调地狱 -- 比如promisesasync / await。但是,解释它们是我们另一天的话题。




相关文章
|
JavaScript 前端开发 数据安全/隐私保护
vue2+elementui上传照片(el-upload 超简单)
【6月更文挑战第4天】element上传附件(el-upload 超详细) 这个功能其实比较常见的功能,后台管理系统基本上都有,这就离不开element的el-upload 展示:
1649 0
|
6月前
|
数据库 Android开发
Android使用EditText+Listview实现搜索效果(使用room模糊查询)
本文介绍如何在Android中使用EditText与ListView实现搜索功能,并结合Room数据库完成模糊查询。主要内容包括:Room的模糊查询语句(使用`||`代替`+`号)、布局美化(如去除ListView分割线和EditText下划线)、EditText回车事件监听,以及查询逻辑代码示例。此外,还提供了相关扩展文章链接,帮助读者深入了解ListView优化、动态搜索及Room基础操作。
443 65
|
数据采集 算法 大数据
【专栏】大规模数据处理在数据化时代的重要性、应用领域以及面临的挑战
【4月更文挑战第27天】随着信息技术发展,数据成为驱动社会和经济的核心。大规模数据处理技术助力企业优化决策、推动科研创新、促进社会治理现代化,广泛应用于金融、电商、医疗等领域。然而,数据质量、安全、技术更新、法律伦理等问题也随之而来,需通过建立数据管理体系、加强技术研发、人才培养和法规建设等策略应对。大规模数据处理技术在变革生活的同时,其健康发展至关重要。
481 2
|
机器学习/深度学习 人工智能 算法
ChatGPT是如何训练得到的?通俗讲解
ChatGPT是如何训练得到的?通俗讲解
|
机器学习/深度学习 调度 Python
SOFTS: 时间序列预测的最新模型以及Python使用示例
这是2024年4月《SOFTS: Efficient Multivariate Time Series Forecasting with Series-Core Fusion》中提出的新模型,采用集中策略来学习不同序列之间的交互,从而在多变量预测任务中获得最先进的性能。
352 4
|
Java 编译器 数据安全/隐私保护
Java 重写(Override)与重载(Overload)详解
在 Java 中,重写(Override)和重载(Overload)是两个容易混淆但功能和实现方式明显不同的重要概念。重写是在子类中重新定义父类已有的方法,实现多态;重载是在同一类中定义多个同名但参数不同的方法,提供多种调用方式。重写要求方法签名相同且返回类型一致或为父类子类关系,而重载则关注方法参数的差异。理解两者的区别有助于更好地设计类和方法。
1056 3
|
Web App开发 缓存 JavaScript
Node.js安装及环境配置,详细简单易懂!一文get全部!
Node.js安装及环境配置,详细简单易懂!一文get全部!
|
前端开发 开发工具 git
[巨详细]使用HBuilder-X启动uniapp项目教程
【6月更文挑战第6天】使用HBuilder-X启动uniapp项目教程 先用HBuilder-X打开本地的uniapp项目
2502 0
|
Java Maven Android开发
Maven神坑之PKIX path building failed终极解决办法
Maven神坑之PKIX path building failed终极解决办法
2528 0
Maven神坑之PKIX path building failed终极解决办法
|
存储 编译器 程序员
【C语言】整形数据和浮点型数据在内存中的存储
【C语言】整形数据和浮点型数据在内存中的存储
177 0