开发过程中,你可能会经常用到控制台命令console.log(),但是,其实除了这个命令外,还有一些其他的命令和技巧可供我们使用,让我们看看它们究竟是什么,会不会为你的调试能力带来一点新的启发。
同样作为开发者,有一点不可否认的是程序员这个群体总是会不断优化工作流程,使其变得更高效。不过这很容易使我们陷入误区,让我们很难突破已经惯有的工作流程,继而闭耳塞听得认为没有比现在更好的工作方法和流程。
一般Web开发者的工作流程是在IDE中写好代码并保存,然后到浏览器中刷新测试。同时使用浏览器的DevTools调整CSS,还可以测试产品在不同分辨率和移动设备上的表现。在需要深入研究的地方可以通过添加 console.log()语句来调试我们的脚本。
如果console.log()在最终产品中被滥用,那么你在网上冲浪时如果一直打开DevTools,你就会在控制台中看到很多本不该出现在最终产品中的调试信息。
下面让我为大家介绍一下除了console.log()之外的其他命令,看看它们会不会为各位的工作带来什么新的启发吧!
我们可能已经习惯了通过 console.log("参数") 来了解程序中正在发生的事情,一般来说对于字符或数字这种类型的输出,这种用法就足够了,不过在输出些类似像对象、数组类型的数据时却没有那么顺手。
第一个技巧是在变量上加上大括号,这样不仅可以打印出它们的值,还可以打印出变量的名称,这使我们在日志中更方便的定位到什么值来自哪里。
let x = 2;
console.log(x) // 2
console.log({x}) // {x: 2}
格式化日志
你可以在console.log中使用以百分号操作符指代不同格式的记录值来格式化字符串,以下是操作符类型的定义:
你可以把它们分别放在控制台试试效果,首先是字符串和整数的示例:
console.log('%ix %s developer', 10, 'console');
// 10x console developer
如果计划将数字格式化为整型,可以使用如下示例:
console.log('%i', 12.34455241234324234);
// 12
%c操作符可以令你使用CSS样式定制输出日志的样式
console.log('%cPay attention to me','color:firebrick;font-size:40px')
分组日志
你可以使用console.group()来对日志进行分组,以将其显示为可扩展和可折叠的组。
const label = 'The Millenium Falcon Crew';
console.group(label);
console.log('Leia');
console.log('Han');
console.log('Chewie');
console.log('Ben');
console.groupEnd(label);
你可以嵌套分组,并可以使用 console.groupCollapsed() 在默认情况下不展开它们:
const extendedlabel = 'The Millenium Falcon Crew extended';
const meat = 'Humanoids';
const metal = 'Droids';
console.group(extendedlabel);
console.groupCollapsed(meat);
console.log('Leia');
console.log('Han');
console.log('Chewie');
console.log('Ben');
console.groupEnd(meat);
console.group(metal);
console.log('R2D2');
console.log('C3PO');
console.groupEnd(metal);
console.groupEnd(extendedlabel);
日志控制台过滤
除了console.log外,你也可以使用 console.info()、console.error()和 console.warning()来代替它。通过这些语句,你可以在控制台侧边栏或下拉列表中来过滤你在控制台中看到的消息。这样一来,你可以更容易地在来自第三方脚本和项目中的其他脚本中找到自己的日志消息。
其他控制台命令
你可能在debug时曾创建过统计某个方法被调用或被执行次数的变量。这里推荐另一种方法, console.count()和 console.countReset(),通过它们你可以创建任意数量的变量,并通过标签来区分。
console.count('Chocula'); // Chocula: 1
console.count(); // default: 1
console.count('Chocula'); // Chocula: 2
console.countReset('Chocula');
console.count(); // default: 2
console.count(); // default: 3
console.count('Chocula'); // Chocula: 1
您还可以使用console.time()方法去统计代码执行的总耗时:
console.time('go');
for(let i = 0; i < 200000; i+=1) {
let x = Math.random()*2000;
}
console.timeEnd('go'); // go: 11.7861328125 ms
使用 console.dir()不仅可以显示内容,还可以显示你发送的数据类型。例如,如果你想要一个节点的XML表示,你可以使用console.dirxml()。而console.table()对于显示JSON数据作为一个可排序的表格显示效果也很好。
使用 console.log() 来监测那些变化范围很大的数值时,不仅低效且困难。你可在在开发人员工具中通过点击“眼睛“图标来激活Live Expression功能。它可以将你想要关注的数值pin在工具顶端。
例如,你可以先输入document.activeElement 来试试。该表达式表示当前获得焦点的元素。
在这有一点需要说明,因为Live Expression并不和某一个站点及域名所关联,所以它会一直保留在你的DevTools中。因此建议在完成一项调试后及时删除它们,以免为调试其他站点时带来不必要的麻烦。
使用控制台处理当前文档
开发人员工具中的控制台不仅仅是用于显示日志的一种方式。它是一个REPL,可让您编写和执行JavaScript并使用自动完成功能了解当前文档的可用方法和属性。
你可以试试,在开发人员工具的控制台,输入doc并按下tab,它会自动将其转为document。如果输入’.’ 你会看到所有document可用的方法和属性。这是一种学习可用方法和属性的有趣且简单的方法之一,这样可以使你在短时间内写出大量代码。
除此之外,控制台和当前文档进行交互还有很多快捷方式可供你使用 “控制台实用程序”。其中一些是:
其中有些方法的功能很强大,但可能我们在并不清楚的前提下自己实现了一系列 console.log() 语句。
例如如下使用场景:
monitorEvents(window, ['resize', 'scroll']);
monitorEvents($0, 'key');
每次窗口滚动或调整大小时都会记录一条日志。第二个示例比较有意思,因为它记录了当前选定元素上的任何按键行为。
以下代码列出页面中的所有a标签(因为$$('a')是document.querySelectorAll('a')的简称),并以可排序的表格形式显示。作为table方法的第二个参数的数组定义了表格的列。否则,链接的每个属性都会变成一列,那就很难浏览了。这个表不仅是可排序的,而且你还可以复制和粘贴它--例如,复制到Excel中。
console.table($$('a'),['href','text'])
与其使用复杂的JavaScript来过滤这些结果,你不如试试CSS选择器。再比如,你想获得一个文档中所有非内嵌图片的src和alt信息的表格,你可以使用以下方法:
console.table($$('img:not([src^=data])'), ['src','alt'])
另外,当您使用Markdown生成HTML时,大多数页面生成器都会在标题上创建自动ID,例如 # New Stuff标题会变成<h1 id="new-stuff">New stuff</h1>。如果我需要批量创建许多指向这些锚点的URL,但不想手动去做这些事时,可能需要通过控制台编写脚本来为我做这件事:
let out = '';
$$('#main [id]').filter(
elm => {return elm.nodeName.startsWith('H')}
).forEach(elm => {
out += `${elm.innerText}
${document.location.href}#${elm.id}
`
});
copy(out);
结果是一个文本块,每个标题的文本内容后跟指向该标题的完整URL。
这里展示了$$快捷方式的一个有趣的额外功能。document.querySelectorAll('#main [id]').filter() 会导致一个错误,因为返回的值不是一个数组而是一个NodeList。你需要用[...document.querySelectoAll('#main [id]').filter()]或Array.from(document.querySelectoAll('#main [id]').filter())方法把它强制转换成一个Array,这在相当长的一段时间中困扰着从jQuery转到JavaScript的开发者,而使用$$,可以直接使用所有的Array方法。
一般来说,你可以通过控制台来改变浏览器页面中的所有元素。而且你还有一个额外的好处,就是可以使用DevTools的元素选项卡来获得元素所有的页面路径。点击每个元素旁的...菜单,并通过弹出的上下文菜单中选择你要复制的路径。
虽然控制台本身很好用,但很快你就会发现Console在编写代码存在着诸多困难,例如,Console是单行环境,不小心点击Enter后就会立即执行。不过在这最后为大家介绍一个小技巧,你可以使用Shift + Enter来代替编写多行脚本。
Sources
总的来说,Console是一个很好的测试环境,但对于编辑体验来说却很差。不过还好在Sources面板中也有一个完整的编辑器。在那里,你可以检查当前页面的代码,并编写更复杂的脚本与之交互。
除了点按上面tab菜单之外,DevTools还有一套快捷键Command Menu供你使用,你可以通过按control + shift + P(Windows, Linux)或Command+Shift+P(macOS)来访问它。或选择(...或⋮)菜单,选择“Run command”。
Snippets 代码片段
Snippets是保存你曾写过的能明显提高开发效率的代码小片段。在DevTools中点击Command Menu键盘快捷键,输入snip并按下Enter键,选择创建一个新的snippet,这样就会进入Snippets编辑器,具体如下图所示:
右边的窗体包括一个完整的源码编辑器,具有关键词着色、自动补全、多光标等功能。下面我们开始试试上面的示例:
console.clear();
let out = '';
let problems = [];
$$('a').forEach(a => {
let text = a.innerText.trim();
let prefix = '';
if (!text) {
if (a.querySelector('img')){
text = a.querySelector('img').alt;
prefix = 'Image: ';
}
if (a.getAttribute('aria-label')) {
text = a.getAttribute('aria-label');
prefix = 'Aria Label: ';
}
if (a.getAttribute('aria-labelledby')) {
text = $('#' + a.getAttribute('aria-labelledby')).innerText;
prefix = 'Aria Labelled By: ';
}
}
if (text) {
text = prefix + text
} else {
a.style.border = '1px solid firebrick';
problems.push(a);
}
out += `
${text||'No Link text'}
${a.href}`;
});
if (out === '') {
console.warn('Sorry, no links found');
} else {
copy(out);
console.info('done harvesting links, ready to paste');
if (problems.length > 0) {
console.warn('There were %d issues:', problems.length);
console.groupCollapsed('Links without text');
problems.forEach(a => {console.dirxml(a)});
console.groupEnd('Links without text');
}
}
以下是执行演示:
Overrides
Override是通过修改远程文件的本地副本,实现本地测试替换服务器文件。例如,你可以在本地编辑完整的复杂样式表,但无需等待冗长的重新build和部署过程即可看到效果,这也是能在开发阶段即可发现问题的一种快捷的方式。
你可以通过安装Microsoft Edge Tools for VS Code扩展 ,即可在编辑中获得开发人员工具,通过下图可以看到基础用法。
希望通过这篇文章,可以使你能更全面的对DevTools有一些了解,而不仅仅只会使用console.log。另外作为一个开发人员除了上述的这些技巧外,还应多多习惯使用断点及条件断点进行调试,希望能够通过更多的使用和练习来达到熟练的状态。
文同步本人掘金平台原创翻译的文章:https://juejin.cn/post/6844903834246971400
你是否遇到过"callbacks"一词,但是不知道这意味着什么?别着急。你不是一个人。许多JavaScript的新手发现回调也很难理解。
尽管callbacks可能令人疑惑,但是你仍然需要彻底了解它们,因为它们是JavaScript中的一个重要的概念。如果你不知道callbacks,你不可能走得很远。
这就是今天的文章(要讲的)!你将了解callbacks是什么,为什么它们很重要,以及如何使用它们。
备注:你会在这篇文章中看到ES6箭头函数。如果你不是很熟悉它们,我建议你在往下读之前复习一下ES6这篇文章(只了解箭头函数部分就可以了)。
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呢?
回调函数以两种不同的方式使用 -- 在同步函数和异步函数中。
如果你的代码从上到下,从左到右的方式顺序执行,等待上一个代码执行之后,再执行下一行代码,则你的代码是同步的。
让我们看一个示例,以便更容易理解:
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的待办事项列表中出现的内容。
相关命令在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如下:
订披萨,拖地和熨衣服!
然后,在执行orderPizza时,JavaScript知道它需要等待披萨送达。因此,它会在执行其余任务时,将“等待披萨送达”(的指令)添加到waiting list上。
JavaScript等待披萨到达
当披萨到达时,门铃会通知JavaScript,当它完成其余杂务时。它会做个**心理记录(mental note)**去执行layTheTable。
JavaScript知道它需要通过在其 mental note 中添加命令来执行layTheTable
然后,一旦完成其他杂务,JavaScript就会执行回调函数layTheTable。
其他所有内容完成后,JavaScript就会去布置桌面(layTheTable)
我的朋友,这个就被称为事件循环。你可以使用事件循环中的实际关键字替换我们的管家,类比来理解所有的内容:
JavaScript的事件循环
如果你有20分钟的空余时间,我强烈建议你观看Philip Roberts 在JSconf中谈论的事件循环。它将帮助你理解事件循环的细节。
哦~我们在事件循环绕了一大圈。我们回正题吧。
之前,我们提到如果JavaScript专注于按钮并忽略所有其他命令,那将是不好的。是吧?
通过异步回调,我们可以提前提供JavaScript指令而无需停止整个操作。
现在,当你要求JavaScript查看点击按钮时,它会将“监听按钮”(指令)放入waiting list中并继续进行杂务。当按钮最终获得点击时,JavaScript会激活回调,然后继续执行。
以下是回调中的一些常见用法,用于告诉JavaScript要做什么...
// 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中的回调地狱 -- 比如promises和async / await。但是,解释它们是我们另一天的话题。
今天,你了解到了回调是什么,为什么它们在JavaScript中如此重要以及如何使用它们。你还学会了回调地狱和对抗它的方法。现在,希望callbakcs不再吓到你了。
你对回调还有任何疑问吗?如果你有,请随时在下面发表评论,我会尽快回复你的。【PS:本文译文,若需作者解答疑问,请移步原作者文章下评论】
感谢阅读。这篇文章是否帮助到你?如果有,我希望你考虑分享它。你可能会帮助到其他人。非常感谢!
事件委托是前端开发中常用的一种优化性能和代码可维护性的方法,它基于DOM的事件冒泡机制。当一个元素触发事件时,这个事件会按照从顶层到底层的顺序传播,直到最底层的元素(通常是文档的根节点)。事件委托利用了这个特性,通过在父元素上设置事件处理程序来监听子元素的事件,从而减少不必要的事件处理程序的数量。
1. 事件委托的定义与作用:
事件委托允许我们将事件监听器添加到其父元素上,这样只有当这些子元素触发事件时,才会执行相应的处理程序。这样做的好处是减少了不必要的事件监听器的创建,因为不需要为每个可能触发事件的子元素都添加一个监听器。此外,它还可以减少内存占用和提高页面渲染性能,因为减少了绑定到子元素上的事件处理函数的数量。
2. 使用事件委托的编程场景以及它如何提升性能:
事件委托通常用于以下场景:
通过事件委托,我们可以避免为每个独立的子元素重复编写相同的事件处理逻辑,从而提高了代码的复用性和可维护性。同时,由于减少了绑定到每个子元素的事件处理函数的数量,因此可以减轻浏览器的负担,提高页面的性能。
私信【学习】即可获取前端资料 都整理好啦!!!
*请认真填写需求信息,我们会在24小时内与您取得联系。