我是靠谱客的博主 健壮世界,最近开发中收集的这篇文章主要介绍v-for 给每项绑定事件时,需要使用「事件代理」吗❓,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

Vue 开发者在日常码业务中,或多或少都需要对列表项绑定事件完成需求,那你有没有思考过,用 v-for 时,给每个列表项绑定事件时,是否需要使用「事件代理/(委托)」呢?还是说你觉得 Vue 内部帮我们实现了事件代理呢?接下来,让我们用事实说话,探讨一下这个问题吧❗️

纵观掘金里对「Vue事件代理」的讨论几乎没有~大家不如跟这笔者一起,探寻 Vue 中事件绑定的奥秘吧。(有些叫法是:事件委托,名词都不重要!大家知道就行,本文统一称之为「事件代理」)

阅读本文你可以知道:

  1. 我们在 v-for 列表每项中使用 @click 最终到 DOM 的结果;
  2. 编译后列表项目中的 @clickrender函数 长什么样;
  3. 整个 new Vue 到挂载,是怎么给真实 DOM 元素绑定事件
  4. 一起探究组件更新时,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 之前,直接给列表绑定事件时,是否都会用一个叫事件代理的写法呢。利用事件冒泡,把点击事件绑定在外层的元素,以解决这样的问题:

  1. 执行多个事件绑定引起性能的损耗

  2. 实现动态绑定。不必对每次插入的 DOM 重新做事件绑定


好了,现在我们不妨先从上面的问题回过来,一起看看我们案例代码 v-for 中绑定的事件最终渲染到浏览器时是怎么样的❓ Vue 是否有对其做一层事件代理呢 ❓ 下面我们对比以下这两张图有什么不同:

  1. 从第一张图能看到,ul 标签上并没有绑定到 click 事件。再往上 div #appbody 、 直至 html 标签,都没有绑定 click 事件。就不一一贴图了,都跟图一是一样的。得到结论就是:li 的父级元素都没有绑定 click 事件。 image.png

  2. 接着我们来看图2,可以看出 li 标签上绑定了 click 事件。每个 li 都都能在 Event Listeners 中找到 click 的绑定。这也不一一贴图了,都是一样的。 image.png

结果导向 来看,v-for 中绑定的 click 事件是直接绑定到对应的列表项中,Vue 内部并没有将其代理到上层元素。这是为什么呢?接下来,让我们带着疑问,从源码层面开始对 Vue 的事件实现一探究竟!????‍????


二、从 @click 到真实 DOM

1. 探究 Render 函数

直接对上述的 App组件 打包后的代码如下:

image.png

分析:

  • 生成的 lirender 函数中,第二个参数中有一个 on 的属性,里面有一个 click 属性指向我们代码中的 handleClick

  • n._l 是对 v-for 的实现,在源码中 core/instance/render-helpers/render-list.js

  • 运行时执行 render 函数,就会根据 on 的这个属性,对里面的事件进行处理

2. 回顾 Vue 组件化路程

组件化时候的流程图如下 :(如果想深入了解,可以看笔者的另外一篇:响应式原理)????

组件化简化.png

简单分析:

  • 如图可知,整个组件化的周期:new Vueinit$mountrender 得到 VNode

  • 最后对 VNode 进行 patch ,就到真实的 DOM 上了,完成整个组件化流程,而事件绑定就是在这个时候绑定到真实的 DOM 上的

3. 进行源码调试

  1. 直接从 createELm 开始看。(这是创建 真实DOM 的方法,大????不用深究,知道就行了)。如下图所示,可以看到此时的 liDOM 已经创建了,我们就从这里开始! image.png

  2. 创建所有完子元素之后会执行到 invokeCreateHooks 、再到 updateDOMListeners。(递归创建 li 子元素过程就不展开了,熟悉 Vue Patch的童鞋可以自己脑补一下哈)

  3. updateDOMListeners 实现。先判断没有 on 属性,直接返回;而后会调用到 updateListeners 这个关键的函数。(我们只需要关注核心点即可,像 normalizeEvents 是处理 v-model 的,还有其他的我们都可以不用关注) image.png

  4. updateListeners 可以看到 通过 createFnInvoker 赋值给 on.clickcreateFnInvoker 其实就是包装了我们的 handleClick 再返回去的一个新函数(高阶函数),目的是做一层函数包装。为什么这么做?这里先埋个伏笔

    image.png

  5. 看上图的 ⭐️ 处,这里执行 add 函数中对真实的 DOM 进行事件绑定。直接看下图,看看 add 函数做了什么。(当前的执行堆栈显示正在执行 add 函数)

    image.png

    • 列表项有多少,这里就会执行多少次 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!


四、组件更新 & 事件绑定

我们都知道,只要触发响应式数据改变,就会触发当前组件的更新。而组件更新,无非就是重新执行 renderpatch 的过程(中间还有个diff)。重新执行render,意味着我们 li 上绑定的 handleClick 会是一个全新的函数。

这里再次贴出 App 组件的 Render 函数图,加深大家的理解 image.png

接下来,通过两个案例的调试,一起验证一下,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 按钮,验证下是不是直接替换函数而已! image.png

直接看结果:

  • 如图所示,组件更新存在 old ,所以不会走到初始化的逻辑(截图打❌处)
  • 3个 li 包括 change按钮 在内的总共4个 click 事件全部都是走如下逻辑,将静态属性 fns 切换成新的点击函数 old.fns = cur,并不会走原生的 addEventListener 重新绑定事件!

image.png

2. 添加一个新 li 会怎么样?

大家可以根据前面的案例自己想一下!其实很简单哈~

我们在上述案例代码稍作修改,将 buttonclick 事件改一下:

<button @click="listArr.push('list-item')">pushItem</button> 

页面长这样????

image.png

我们点击 pushItem 按钮!如无意外,前三次应该跟上述一样!

image.png

没有意外,我们接着单步进行!直到进入了 addEventListener 环节,果然是我们新 pushli 元素!如下图,特地找了他的 innerHTML 给大????看

image.png

结论:Vue 组件更新时,不会重新对元素进行事件绑定! 展开说说,其实就是用 invoker 包装(高阶函数思想),将我们的事件回调放在 invoker.fns 静态属性。DOM事件绑定回调时,绑定了 invoker ;事件回调执行时,实际是找到 invoker.fns 中的函数去执行,也就是我们的 handleClick

So,其实 Vue 内部是有对事件这一块做一定的优化的,但是内部没有做 事件代理优化!!!


五、v-for是否需要事件代理?

首先抛出我的观点:

  1. 所有的优化处理,一般都是出现性能问题再考虑。
  2. 不需要在所有开发中都考虑优化,毕竟完整业务、早点下班才是首要。
  3. 不是说有优化思维和执行不好,而是要兼顾时间成本,和优化必要性。如果不需要优化也流畅得鸭批,那我感觉是没必要优化的。

从实际出发:

  1. 性能导向。对长列表进行事件代理处理,当然是有一定的性能提升,这点毋庸置疑,但是大家看场景决定是否需要写成事件代理。如果不会引起性能问题,直接 @click 在每一项也是可以的

  2. 注意绑定的函数的写法。不要给一些面试题的写法蒙蔽,有些面试题出题的写法确实性能开销更大,但其实是可以避免的。不妨看看下面的伪代码

    for (let i = 0; i < xxx; i++) {
      // 注意看写法的区别,每次赋值一个全新函数 
      li.onclick = function () { ... }
      // 每次指向同一个函数,不会造成函数n次生成
      li.onclick = myCick
    }
    
    function myClick () { ... } 
    

    所以,在 v-for 中绑定事件避免直接写成 @click="function () {}" 这种,加大函数创建的性能开销

  3. Vue 内部有一定的优化手段。由本文可知,Vue 内部其实是有做一定的优化的,特别是组件更新时减少了很多事件重新绑定、解绑的开销

结论:大家放心在列表项中直接绑定事件吧,一般都没问题!出现性能问题再解决就好了,不是吗?(毕竟长列表不都流行用虚拟列表优化了嘛 狗头.png


写在最后,其实写这个话题出于几点吧:

  • 其一:主要是因为掘金里确实很少对 Vue长列表绑定事件、Vue有没有内部帮我们实现事件代理 的讨论,但是这个对日常开发也算是有指引性作用的吧
  • 其二:笔者之前面试时就被问到这个问题~当时也是想当然,觉得这么基础的点,尤大不可能没考虑到吧?所以就被问垮了。其实对于源码的实现,如果没看到那一块,确实不知道,也没办法啦~
  • 其三:熟悉 React 的同学肯能就会很自然而然的想到合成事件,感觉 Vue 也是有这样处理事件的,做一层总代理

最后,想写一点「创作内容」的心里话。目前已经是坚持 写文章 的一个多月了吧,初衷只是想把自己的知识做一个沉淀,没想到发布的文章会有一定的阅读量、点赞收藏,也会有掘友想要转载,这也让我感受到了 知识分享、知识交流 带来的充实感。所以,笔者接下来会在选题、内容质量上更下功夫,做对大家日常工作、面试中有用的干货文章!❤️

最后

以上就是健壮世界为你收集整理的v-for 给每项绑定事件时,需要使用「事件代理」吗❓的全部内容,希望文章能够帮你解决v-for 给每项绑定事件时,需要使用「事件代理」吗❓所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(54)

评论列表共有 0 条评论

立即
投稿
返回
顶部