概述
Vue
开发者在日常码业务中,或多或少都需要对列表项绑定事件完成需求,那你有没有思考过,用v-for
时,给每个列表项绑定事件时,是否需要使用「事件代理/(委托)」呢?还是说你觉得 Vue 内部帮我们实现了事件代理呢?接下来,让我们用事实说话,探讨一下这个问题吧❗️
纵观掘金里对「Vue事件代理」的讨论几乎没有~大家不如跟这笔者一起,探寻 Vue
中事件绑定的奥秘吧。(有些叫法是:事件委托,名词都不重要!大家知道就行,本文统一称之为「事件代理」)
阅读本文你可以知道:
- 我们在
v-for
列表每项中使用@click
最终到DOM
的结果; - 编译后列表项目中的
@click
的 render函数 长什么样; - 整个
new Vue
到挂载,是怎么给真实DOM
元素绑定事件的 - 一起探究组件更新时,
Vue
是否会重新 绑定事件
注:本文调试的 Vue
源码 版本为 v2.6.14
一、事件绑定时的思考
先看几个问题????:
- 日常开发中,在
Vue
长列表绑定事件时,都是怎写的? - 如果在
v-for
每项中直接绑定事件,会不会存在性能问题呢? - 如果这样会有性能问题,那
Vue
内部有没有处理?
先看一段代码:在每个 li
中绑定了点击事件 handleClick
// 模版代码
<ul>
<li
v-for="(item, i) in listArr"
:key="item + i"
@click="handleClick(item + (i + 1))"
>
{{item + (i + 1)}}
</li>
</ul>
// js代码
methods: {
handleClick (params) {
console.log('触发绑定事件' + params)
}
}
相信这是绝大部分 Vueer 完成业务功能的写法(包括我????)。此时此景,不知道在座的各位会不会想到经常碰到的一道面试题:「事件代理」❓ 以前我们不用 Vue
之前,直接给列表绑定事件时,是否都会用一个叫事件代理的写法呢。利用事件冒泡,把点击事件绑定在外层的元素,以解决这样的问题:
-
执行多个事件绑定引起性能的损耗
-
实现动态绑定。不必对每次插入的
DOM
重新做事件绑定
好了,现在我们不妨先从上面的问题回过来,一起看看我们案例代码 v-for
中绑定的事件最终渲染到浏览器时是怎么样的❓ Vue
是否有对其做一层事件代理呢 ❓ 下面我们对比以下这两张图有什么不同:
-
从第一张图能看到,
ul
标签上并没有绑定到click
事件。再往上div #app
、body
、 直至html
标签,都没有绑定click
事件。就不一一贴图了,都跟图一是一样的。得到结论就是:li
的父级元素都没有绑定click
事件。 -
接着我们来看图2,可以看出
li
标签上绑定了click
事件。每个li
都都能在Event Listeners
中找到click
的绑定。这也不一一贴图了,都是一样的。
从 结果导向 来看,v-for
中绑定的 click
事件是直接绑定到对应的列表项中,Vue
内部并没有将其代理到上层元素。这是为什么呢?接下来,让我们带着疑问,从源码层面开始对 Vue
的事件实现一探究竟!????????
二、从 @click
到真实 DOM
1. 探究 Render
函数
直接对上述的 App组件 打包后的代码如下:
分析:
-
生成的
li
的render
函数中,第二个参数中有一个on
的属性,里面有一个click
属性指向我们代码中的handleClick
-
n._l
是对v-for
的实现,在源码中core/instance/render-helpers/render-list.js
-
运行时执行
render
函数,就会根据on
的这个属性,对里面的事件进行处理
2. 回顾 Vue
组件化路程
组件化时候的流程图如下 :(如果想深入了解,可以看笔者的另外一篇:响应式原理)????
简单分析:
-
如图可知,整个组件化的周期:
new Vue
、init
、$mount
、render
得到VNode
-
最后对
VNode
进行patch
,就到真实的DOM
上了,完成整个组件化流程,而事件绑定就是在这个时候绑定到真实的DOM
上的
3. 进行源码调试
-
直接从
createELm
开始看。(这是创建 真实DOM 的方法,大????不用深究,知道就行了)。如下图所示,可以看到此时的li
的DOM
已经创建了,我们就从这里开始! -
创建所有完子元素之后会执行到
invokeCreateHooks
、再到updateDOMListeners
。(递归创建li
子元素过程就不展开了,熟悉 Vue Patch的童鞋可以自己脑补一下哈) -
updateDOMListeners
实现。先判断没有on
属性,直接返回;而后会调用到updateListeners
这个关键的函数。(我们只需要关注核心点即可,像normalizeEvents
是处理v-model
的,还有其他的我们都可以不用关注) -
updateListeners
可以看到 通过createFnInvoker
赋值给on.click
。createFnInvoker
其实就是包装了我们的handleClick
再返回去的一个新函数(高阶函数),目的是做一层函数包装。为什么这么做?这里先埋个伏笔 -
看上图的 ⭐️ 处,这里执行
add
函数中对真实的DOM
进行事件绑定。直接看下图,看看add
函数做了什么。(当前的执行堆栈显示正在执行add
函数)- 列表项有多少,这里就会执行多少次
add
函数进行事件绑定。我们案例中有8个li
,这里会执行 8 次,给 8 个li
绑定click
事件
- 列表项有多少,这里就会执行多少次
OK,这就是组件初始化阶段时,DOM
的绑定事件的过程。根据上述调试结果,可以肯定的是: Vue
没有对 v-for
列表项的绑定事件进行 事件代理 。
这里我把列表长度调整为3,去掉其他 debugger
,且只保留 addEventListener
的,再录个屏给大家看。注意看,会执行3次 addEventListener
????
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rWKrk3bj-1655437479768)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/59a8bd5ad6284142a9b18f42537e20e2~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]
结论:Vue
内部没有 对列表项绑定的 click
事件进行 事件代理 !
三、Vue
真的没做一点处理吗?
上文埋下伏笔的 createFnInvoker
函数,在这里就有用处了!????看过 Vue错误处理 实现的童鞋知道这个方法会用 try-catch
包装我们的 handleClick
以实现错误捕获,那还有其他的用途吗?(想详细了解错误捕获的可以 戳这里,看Vue的错误处理机制)
1. invoker
函数
这里关注 createFnInvoker
的核心处理:
-
核心点一: 接收一个
fns
函数 或 函数数组, 返回一个Function
-
核心点二: 返回的
invoker
方法的 静态属性fns
,然后每次invoker
执行的时候,会重新拿这个invoker.fns
-
核心点三: 我们开发者写的
handleClick
会被包裹在invokeWithErrorHandling
中,点击事件的回调会在其内部执行。作用开头有讲过,就是 Vue 做了一层 错误捕获 的处理
export function createFnInvoker (fns: Function | Array<Function>, vm: ?Component): Function {
function invoker () {
// 获取 invoker 的静态属性 fns
const fns = invoker.fns
if (Array.isArray(fns)) {
const cloned = fns.slice()
for (let i = 0; i < cloned.length; i++) {
// 1. cloned[i] 其实就是对一个的执行函数,如 handleClick,传入函数中
// 2. 最后会在 invokeWithErrorHandling 内部执行(被 try-catch 包裹)
invokeWithErrorHandling(cloned[i], null, arguments, vm, `v-on handler`)
}
} else {
// 只有一个函数走这里,逻辑跟上述一模一样
return invokeWithErrorHandling(fns, null, arguments, vm, `v-on handler`)
}
}
invoker.fns = fns
return invoker
}
2. 为什么要用invoker
包装?
- 先着看
updateListeners
中的else if
中的逻辑:old.fns = cur
- 这就是 Vue 对我们列表绑定事件的处理了!组件更新时:仅仅是替换绑定函数!
- 这里注意一点:组件初始化、组件更新都是使用
updateListeners
这个方法的
function updateListeners () {
...
for (name in on) {
...
if (isUndef(cur)) {
...
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
// 初始化的时候执行到这里,cur = invoker
cur = on[name] = createFnInvoker(cur, vm);
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture);
}
// 回顾步骤 4. 画五角星⭐️的部分,首次会在这里添加函数
add(event.name, cur, event.capture, event.passive, event.params);
} else if (cur !== old) {
// 组件更新时,走到这里,将新的 invoker 赋值到之前的 invoker 静态属性 fns 中
old.fns = cur;
on[name] = old;
}
}
...
}
- 再回到绑定事件的源码看看。这里的
handler
,其实就是invoker
函数。现在,为什么不直接绑定我们的handleClick
函数,大????get到了吗?笔者在这先大胆下个结论:就是为了避免组件更新的时候,每一项li
需要重新绑定事件
target$1.addEventListener(
name, // click
handler, // 其实绑定到 DOM 的是 invoker 函数,并不直接是我们的 handleClick
supportsPassive
? { capture: capture, passive: passive }
: capture
);
接下来,我们就验证一下到底是不是这样,看看 old.fns = cur;
逻辑的执行,Let’s go!
四、组件更新 & 事件绑定
我们都知道,只要触发响应式数据改变,就会触发当前组件的更新。而组件更新,无非就是重新执行 render
、 patch
的过程(中间还有个diff)。重新执行render,意味着我们 li
上绑定的 handleClick
会是一个全新的函数。
这里再次贴出 App 组件的 Render 函数图,加深大家的理解
接下来,通过两个案例的调试,一起验证一下,Vue
组件更新,到底有没有重新绑定事件
案例代码如下:
/* template */
<div id="app">
// 多了个flag
<p>{{flag}}</p>
<ul>
<li
v-for="(item, i) in listArr"
:key="item + i"
@click="handleClick(item + (i + 1))"
>
{{item + (i + 1)}}
</li>
</ul>
// 按钮点击改变 flag
<button @click="flag = !flag">change</button>
</div>
/* script */
data () {
return {
flag: true,
listArr: new Array(3).fill('list-item')
}
},
methods: {
handleClick (params) {
console.log('触发绑定事件' + params)
}
}
1. 普通组件更新会怎么样?
上述代码的页面长这样。现在我们就点击 change
按钮,验证下是不是直接替换函数而已!
直接看结果:
- 如图所示,组件更新存在
old
,所以不会走到初始化的逻辑(截图打❌处) - 3个
li
包括change按钮
在内的总共4个click
事件全部都是走如下逻辑,将静态属性fns
切换成新的点击函数old.fns = cur
,并不会走原生的addEventListener
重新绑定事件!
2. 添加一个新 li
会怎么样?
大家可以根据前面的案例自己想一下!其实很简单哈~
我们在上述案例代码稍作修改,将 button
的 click
事件改一下:
<button @click="listArr.push('list-item')">pushItem</button>
页面长这样????
我们点击 pushItem
按钮!如无意外,前三次应该跟上述一样!
没有意外,我们接着单步进行!直到进入了 addEventListener
环节,果然是我们新 push
的 li
元素!如下图,特地找了他的 innerHTML
给大????看
结论:Vue 组件更新时,不会重新对元素进行事件绑定! 展开说说,其实就是用 invoker
包装(高阶函数思想),将我们的事件回调放在 invoker.fns
静态属性。DOM事件绑定回调时,绑定了 invoker
;事件回调执行时,实际是找到 invoker.fns
中的函数去执行,也就是我们的 handleClick
So,其实 Vue
内部是有对事件这一块做一定的优化的,但是内部没有做 事件代理优化!!!
五、v-for
是否需要事件代理?
首先抛出我的观点:
- 所有的优化处理,一般都是出现性能问题再考虑。
- 不需要在所有开发中都考虑优化,毕竟完整业务、早点下班才是首要。
- 不是说有优化思维和执行不好,而是要兼顾时间成本,和优化必要性。如果不需要优化也流畅得鸭批,那我感觉是没必要优化的。
从实际出发:
-
性能导向。对长列表进行事件代理处理,当然是有一定的性能提升,这点毋庸置疑,但是大家看场景决定是否需要写成事件代理。如果不会引起性能问题,直接
@click
在每一项也是可以的 -
注意绑定的函数的写法。不要给一些面试题的写法蒙蔽,有些面试题出题的写法确实性能开销更大,但其实是可以避免的。不妨看看下面的伪代码
for (let i = 0; i < xxx; i++) { // 注意看写法的区别,每次赋值一个全新函数 li.onclick = function () { ... } // 每次指向同一个函数,不会造成函数n次生成 li.onclick = myCick } function myClick () { ... }
所以,在 v-for 中绑定事件避免直接写成
@click="function () {}"
这种,加大函数创建的性能开销 -
Vue
内部有一定的优化手段。由本文可知,Vue
内部其实是有做一定的优化的,特别是组件更新时减少了很多事件重新绑定、解绑的开销
结论:大家放心在列表项中直接绑定事件吧,一般都没问题!出现性能问题再解决就好了,不是吗?(毕竟长列表不都流行用虚拟列表优化了嘛 狗头.png
)
写在最后,其实写这个话题出于几点吧:
- 其一:主要是因为掘金里确实很少对 Vue长列表绑定事件、Vue有没有内部帮我们实现事件代理 的讨论,但是这个对日常开发也算是有指引性作用的吧
- 其二:笔者之前面试时就被问到这个问题~当时也是想当然,觉得这么基础的点,尤大不可能没考虑到吧?所以就被问垮了。其实对于源码的实现,如果没看到那一块,确实不知道,也没办法啦~
- 其三:熟悉
React
的同学肯能就会很自然而然的想到合成事件,感觉Vue
也是有这样处理事件的,做一层总代理
最后,想写一点「创作内容」的心里话。目前已经是坚持 写文章 的一个多月了吧,初衷只是想把自己的知识做一个沉淀,没想到发布的文章会有一定的阅读量、点赞收藏,也会有掘友想要转载,这也让我感受到了 知识分享、知识交流 带来的充实感。所以,笔者接下来会在选题、内容质量上更下功夫,做对大家日常工作、面试中有用的干货文章!❤️
最后
以上就是健壮世界为你收集整理的v-for 给每项绑定事件时,需要使用「事件代理」吗❓的全部内容,希望文章能够帮你解决v-for 给每项绑定事件时,需要使用「事件代理」吗❓所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复