Vue源码系列讲解——实例方法篇【二】(事件相关方法)

目录

0.前言

1. vm.$on

1.1 用法回顾

1.2 内部原理

2. vm.$emit

2.1 用法回顾

2.2 内部原理

3. vm.$off

3.1 用法回顾

3.2 内部原理

4. vm.$once

4.1 用法回顾

4.2 内部原理


0.前言

与事件相关的实例方法有4个,分别是vm.$onvm.$emitvm.$offvm.$once。它们是在eventsMixin函数中挂载到Vue原型上的,代码如下:

export function eventsMixin (Vue) {Vue.prototype.$on = function (event, fn) {}Vue.prototype.$once = function (event, fn) {}Vue.prototype.$off = function (event, fn) {}Vue.prototype.$emit = function (event) {}
}

当执行eventsMixin函数后,会向Vue原型上挂载上述4个实例方法。

接下来,我们就来分析这4个与事件相关的实例方法其内部的原理都是怎样的。

1. vm.$on

1.1 用法回顾

在介绍方法的内部原理之前,我们先根据官方文档示例回顾一下它的用法。

vm.$on( event, callback )

  • 参数

    • {string | Array<string>} event (数组只在 2.2.0+ 中支持)
    • {Function} callback
  • 作用

    监听当前实例上的自定义事件。事件可以由vm.$emit触发。回调函数会接收所有传入事件触发函数的额外参数。

  • 示例

    vm.$on('test', function (msg) {console.log(msg)
    })
    vm.$emit('test', 'hi')
    // => "hi"
    

1.2 内部原理

在介绍内部原理之前,我们先有一个这样的概念:$on$emit这两个方法的内部原理是设计模式中最典型的发布订阅模式,首先定义一个事件中心,通过$on订阅事件,将事件存储在事件中心里面,然后通过$emit触发事件中心里面存储的订阅事件。

OK,有了这个概念之后,接下来,我们就先来看看$on方法的内部原理。该方法的定义位于源码的src/core/instance/event.js中,如下:

Vue.prototype.$on = function (event, fn) {const vm: Component = thisif (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$on(event[i], fn)}} else {(vm._events[event] || (vm._events[event] = [])).push(fn)}return vm
}

$on方法接收两个参数,第一个参数是订阅的事件名,可以是数组,表示订阅多个事件。第二个参数是回调函数,当触发所订阅的事件时会执行该回调函数。

首先,判断传入的事件名是否是一个数组,如果是数组,就表示需要一次性订阅多个事件,就遍历该数组,将数组中的每一个事件都递归调用$on方法将其作为单个事件订阅。如下:

if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$on(event[i], fn)}
}

如果不是数组,那就当做单个事件名来处理,以该事件名作为key,先尝试在当前实例的_events属性中获取其对应的事件列表,如果获取不到就给其赋空数组为默认值,并将第二个参数回调函数添加进去。如下:

else {(vm._events[event] || (vm._events[event] = [])).push(fn)
}

那么问题来了,当前实例的_events属性是干嘛的呢?

还记得我们在介绍生命周期初始化阶段的初始化事件initEvents函数中,在该函数中,首先在当前实例上绑定了_events属性并给其赋值为空对象,如下:

export function initEvents (vm: Component) {vm._events = Object.create(null)// ...}

这个_events属性就是用来作为当前实例的事件中心,所有绑定在这个实例上的事件都会存储在事件中心_events属性中。

以上,就是$on方法的内部原理。

2. vm.$emit

2.1 用法回顾

在介绍方法的内部原理之前,我们先根据官方文档示例回顾一下它的用法。

vm.$emit( eventName, […args] )

  • 参数
    • {string} eventName
    • [...args]
  • 作用: 触发当前实例上的事件。附加参数都会传给监听器回调。

2.2 内部原理

该方法接收的第一个参数是要触发的事件名,之后的附加参数都会传给被触发事件的回调函数。该方法的定义位于源码的src/core/instance/event.js中,如下:

Vue.prototype.$emit = function (event: string): Component {const vm: Component = thislet cbs = vm._events[event]if (cbs) {cbs = cbs.length > 1 ? toArray(cbs) : cbsconst args = toArray(arguments, 1)for (let i = 0, l = cbs.length; i < l; i++) {try {cbs[i].apply(vm, args)} catch (e) {handleError(e, vm, `event handler for "${event}"`)}}}return vm}
}

该方法的逻辑很简单,就是根据传入的事件名从当前实例的_events属性(即事件中心)中获取到该事件名所对应的回调函数cbs,如下:

let cbs = vm._events[event]

然后再获取传入的附加参数args,如下:

const args = toArray(arguments, 1)

由于cbs是一个数组,所以遍历该数组,拿到每一个回调函数,执行回调函数并将附加参数args传给该回调。如下:

for (let i = 0, l = cbs.length; i < l; i++) {try {cbs[i].apply(vm, args)} catch (e) {handleError(e, vm, `event handler for "${event}"`)}
}

以上,就是$emit方法的内部原理。

3. vm.$off

3.1 用法回顾

在介绍方法的内部原理之前,我们先根据官方文档示例回顾一下它的用法。

vm.$off( [event, callback] )

  • 参数

    • {string | Array<string>} event (只在 2.2.2+ 支持数组)
    • {Function} [callback]
  • 作用

    移除自定义事件监听器。

    • 如果没有提供参数,则移除所有的事件监听器;
    • 如果只提供了事件,则移除该事件所有的监听器;
    • 如果同时提供了事件与回调,则只移除这个回调的监听器。

3.2 内部原理

通过用法回顾我们知道,该方法用来移除事件中心里面某个事件的回调函数,根据所传入参数的不同,作出不同的处理。该方法的定义位于源码的src/core/instance/event.js中,如下:

Vue.prototype.$off = function (event, fn) {const vm: Component = this// allif (!arguments.length) {vm._events = Object.create(null)return vm}// array of eventsif (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$off(event[i], fn)}return vm}// specific eventconst cbs = vm._events[event]if (!cbs) {return vm}if (!fn) {vm._events[event] = nullreturn vm}if (fn) {// specific handlerlet cblet i = cbs.lengthwhile (i--) {cb = cbs[i]if (cb === fn || cb.fn === fn) {cbs.splice(i, 1)break}}}return vm
}

可以看到,在该方法内部就是通过不断判断所传参数的情况进而进行不同的逻辑处理,接下来我们逐行分析。

首先,判断如果没有传入任何参数(即arguments.length为0),这就是第一种情况:如果没有提供参数,则移除所有的事件监听器。我们知道,当前实例上的所有事件都存储在事件中心_events属性中,要想移除所有的事件,那么只需把_events属性重新置为空对象即可。如下:

if (!arguments.length) {vm._events = Object.create(null)return vm
}

接着,判断如果传入的需要移除的事件名是一个数组,就表示需要一次性移除多个事件,那么我们只需同订阅多个事件一样,遍历该数组,然后将数组中的每一个事件都递归调用$off方法进行移除即可。如下:

if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {this.$off(event[i], fn)}return vm
}

接着,获取到需要移除的事件名在事件中心中对应的回调函数cbs。如下:

const cbs = vm._events[event]

接着,判断如果cbs不存在,那表明在事件中心从来没有订阅过该事件,那就谈不上移除该事件,直接返回,退出程序即可。如下:

if (!cbs) {return vm
}

接着,如果cbs存在,但是没有传入回调函数fn,这就是第二种情况:如果只提供了事件,则移除该事件所有的监听器。这个也不难,我们知道,在事件中心里面,一个事件名对应的回调函数是一个数组,要想移除所有的回调函数我们只需把它对应的数组设置为null即可。如下:

if (!fn) {vm._events[event] = nullreturn vm
}

接着,如果既传入了事件名,又传入了回调函数,cbs也存在,那这就是第三种情况:如果同时提供了事件与回调,则只移除这个回调的监听器。那么我们只需遍历所有回调函数数组cbs,如果cbs中某一项与fn相同,或者某一项的fn属性与fn相同,那么就将其从数组中删除即可。如下:

if (fn) {// specific handlerlet cblet i = cbs.lengthwhile (i--) {cb = cbs[i]if (cb === fn || cb.fn === fn) {cbs.splice(i, 1)break}}
}

以上,就是$off方法的内部原理。

4. vm.$once

4.1 用法回顾

在介绍方法的内部原理之前,我们先根据官方文档示例回顾一下它的用法。

vm.$once( event, callback )

  • 参数

    • {string} event
    • {Function} callback
  • 作用

    监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。

4.2 内部原理

该方法的作用是先订阅事件,但是该事件只能触发一次,也就是说当该事件被触发后会立即移除。要实现这个功能也不难,我们可以定义一个子函数,用这个子函数来替换原本订阅事件所对应的回调,也就是说当触发订阅事件时,其实执行的是这个子函数,然后再子函数内部先把该订阅移除,再执行原本的回调,以此来达到只触发一次的目的。

下面我们就来看下源码的实现。该方法的定义位于源码的src/core/instance/event.js中,如下:

Vue.prototype.$once = function (event, fn) {const vm: Component = thisfunction on () {vm.$off(event, on)fn.apply(vm, arguments)}on.fn = fnvm.$on(event, on)return vm
}

可以看到,在上述代码中,被监听的事件是event,其原本对应的回调是fn,然后定义了一个子函数on

在该函数内部,先通过$on方法订阅事件,同时所使用的回调函数并不是原本的fn而是子函数on,如下:

vm.$on(event, on)

也就是说,当事件event被触发时,会执行子函数on

然后在子函数内部先通过$off方法移除订阅的事件,这样确保该事件不会被再次触发,接着执行原本的回调fn,如下:

function on () {vm.$off(event, on)fn.apply(vm, arguments)
}

另外,还有一行代码on.fn = fn是干什么的呢?

上文我们说了,我们用子函数on替换了原本的订阅事件所对应的回调fn,那么在事件中心_events属性中存储的该事件名就会变成如下这个样子:

vm._events = {'xxx':[on]
}

但是用户自己却不知道传入的fn被替换了,当用户在触发该事件之前想调用$off方法移除该事件时:

vm.$off('xxx',fn)

此时就会出现问题,因为在_events属性中的事件名xxx对应的回调函数列表中没有fn,那么就会移除失败。这就让用户费解了,用户明明给xxx事件传入的回调函数是fn,现在反而找不到fn导致事件移除不了了。

所以,为了解决这一问题,我们需要给on上绑定一个fn属性,属性值为用户传入的回调fn,这样在使用$off移除事件的时候,$off内部会判断如果回调函数列表中某一项的fn属性与fn相同时,就可以成功移除事件了。

以上,就是$once方法的内部原理。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端面试知识点合集

原型和原型链 任何函数都可以作为构造函数。当该函数通过 new 关键字调用的时候&#xff0c;就称之为构造函数。 var Parent function(){}//定义一个函数&#xff0c;那它只是一个普通的函数&#xff0c;不能称它为构造函数var instance new Parent(); //这时这个Parent就不…

C#理论 —— WPF 应用程序Console 控制台应用

文章目录 1. WPF 应用程序1.1 工程创建1.2 控件1.2.1 控件的公共属性1.2.1 TextBox 文本框1.2.1 Button 按钮 *. Console 控制台应用1.1 工程创建 1. WPF 应用程序 1.1 工程创建 Visual Studio 中新建项目 - 选择WPF 应用程序&#xff1b; 1.2 控件 1.2.1 控件的公共属性 …

如何备份和恢复MySQL数据库?有哪些常见的备份工具和策略?

如何备份和恢复MySQL数据库&#xff1f;有哪些常见的备份工具和策略&#xff1f; 在数据库管理中&#xff0c;备份和恢复是非常重要的环节&#xff0c;它们保障了数据的安全性和可恢复性。对于MySQL这样的关系型数据库管理系统&#xff0c;了解并实施有效的备份策略至关重要。…

Linux网络编程——网络基础

Linux网络编程——网络基础 1. 网络结构模式1.1 C/S 结构1.2 B/S 结构 2. MAC 地址3. IP地址3.1 简介3.2 IP 地址编址方式 4. 端口4.1 简介4.2 端口类型 5. 网络模型5.1 OSI 七层参考模型5.2 TCP/IP 四层模型 6. 协议6.1 简介6.2 常见协议6.3 UDP 协议6.4 TCP 协议6.5 IP 协议6…

【兔子机器人】根据自身机器人参数修改simulink模型

关节电机 机体初始高度 &#xff01;&#xff01;&#xff01;接下来尝试修改各腿的坐标朝向

LeetCode54题:螺旋矩阵(python3)

路径的长度即为矩阵中的元素数量&#xff0c;当路径的长度达到矩阵中的元素数量时即为完整路径&#xff0c;将该路径返回。 循环打印&#xff1a; “从左向右、从上向下、从右向左、从下向上” 四个方向循环打印。 class Solution:def spiralOrder(self, matrix: List[List[i…

怎么对App进行功能测试

测试人员常被看作是bug的寻找者&#xff0c;但你曾想过他们实际是如何开展测试的吗&#xff1f;你是否好奇他们究竟都做些什么&#xff0c;以及他们如何在一个典型的技术项目中体现价值&#xff1f;本文将带你经历测试人员的思维过程&#xff0c;探讨他们测试app时的各种考虑. …

Android和Linux的嵌入式开发差异

最近开始投入Android的怀抱。说来惭愧&#xff0c;08年就听说这东西&#xff0c;当时也有同事投入去看&#xff0c;因为恶心Java&#xff0c;始终对这玩意无感&#xff0c;没想到现在不会这个嵌入式都快要没法搞了。为了不中年失业&#xff0c;所以只能回过头又来学。 首先还是…

虚拟内存与mmap,brk

虚拟内存与mmap,brk 基本概念及相关术语 1.1 基本概念 虚拟内存使得应用程序认为它拥有连续的可用的内存&#xff08;一个连续完整的地址空间&#xff09;&#xff0c;而实际上&#xff0c;它通常是被分隔成多个物理内存碎片&#xff0c;还有部分暂时存储在外部磁盘存储器上&…

【C语言】linux内核generic_xdp_tx

一、中文注释 /* 在执行通用XDP时&#xff0c;我们必须绕过qdisc层和网络挖掘点&#xff0c;* 以匹配驱动内XDP的行为。*/ void generic_xdp_tx(struct sk_buff *skb, struct bpf_prog *xdp_prog) {struct net_device *dev skb->dev; // 获取skb对应的网络设备struct netd…

面试高频率问答题目

索引&#xff1a; 主键索引&#xff1a;表的id &#xff08;唯一 且 不能为空&#xff09; 唯一索引&#xff1a;表User 假设有account 字段 &#xff0c;用户名不重复 &#xff08;唯一 可以为空&#xff09; 复合索引&#xff1a;where() 的条件 用户名&#xff0c;密码 …

MySQL:函数

提醒&#xff1a; 设定下面的语句是在数据库名为 db_book里执行的。 创建user_info表 注意&#xff1a;pwd为密码字段&#xff0c;这里使用了VARCHAR(128)类型&#xff0c;为了后面方便对比&#xff0c;开发项目里一般使用char(32)&#xff0c;SQL语句里使用MD5加密函数 USE db…

【博图TIA-Api】通过Excel自动新建文件夹和导入FB块

【博图TIA-Api】通过Excel自动新建文件夹和导入FB块 说明思路准备获取Excel表格内文件名和FB块名等信息新建文件夹部分筛分获取的文件夹数据&#xff0c;去掉重复内容创建文件夹 导入FB块导出FB块的xml文件查找需要放置的文件夹导入块 说明 续上一篇文章&#xff0c;这次是根据…

多线程 --- [ 线程池、线程安全、其他常见的锁 ]

目录 1. 线程池 模块一&#xff1a;线程的封装 模块二&#xff1a;线程池的封装 模块三&#xff1a;互斥量的封装 (RAII风格) 模块四&#xff1a;任务的封装 模块五&#xff1a;日志的封装 模块六&#xff1a;时间的封装 模块六&#xff1a;主函数 模块七&#xff1a…

备战蓝桥杯---状态压缩DP进阶题1

我们来看一看一道比较难的问题&#xff08;十分十分的巧妙&#xff09;&#xff1a; 显然我们应该一行一行放&#xff0c;又竖的会对下一行产生影响&#xff0c;我们令横着放为0&#xff0c;竖着放的上方为1. 对于下一行&#xff0c;前一行放1的下面为0&#xff0c;但是会出现…

【Redis | 第九篇】一篇文章看懂Redis持久化机制

文章目录 9.一篇文章看懂Redis持久化机制9.1Redis的两种持久化机制9.1.1为什么有持久化&#xff1f; 9.2RDB机制9.2.1介绍9.2.2触发机制&#xff08;1&#xff09;save命令触发&#xff08;2&#xff09;bgsave命令触发&#xff08;3&#xff09;自动触发 9.2.3执行流程9.2.4优…

C++知识点总结(22):模拟算法真题 ★★★★☆《卡牌游戏》《移动距离》

一、卡牌游戏 1. 审题 题目描述 A , B , C A,B,C A,B,C 三人在玩一个卡牌游戏&#xff0c;规则如下&#xff1a; 游戏开始时&#xff0c; 3 3 3 人分别会得到若干张手牌, 每张牌上写着 a&#xff0c;b&#xff0c;c 中某一个字母。手牌的顺序严格按照输入顺序排列&#xff0c…

前端【技术类】资源学习网站整理(那些年的小网站)

学习网站整理 值得分享的视频博主&#xff1a;学习网站链接 百度首页的资源收藏里的截图&#xff08;排列顺序没有任何意义&#xff0c;随性而已~&#xff09;&#xff0c;可根据我标注的关键词百度搜索到这些网站呀&#xff0c;本篇末尾会一一列出来&#xff0c;供大家学习呀 …

彻底搞懂回溯算法(例题详解)

目录 什么是回溯算法&#xff1a; 子集问题&#xff1a; 子集问题II(元素可重复但不可复选): 组合问题&#xff1a; 组合问题II(元素可重复但不可复选): 排列问题&#xff1a; 排列问题II(元素可重复但不可复选): 什么是回溯算法&#xff1a; 「回溯是递归的副产品&…

最小生成树---Kruskal算法

最小生成树定义&#xff1a; 给定一张边带权的无向图 G(V,E)&#xff0c;其中 V 表示图中点的集合&#xff0c;E 表示图中边的集合。 由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树&#xff0c;其中边的权值之和最小的生成树被称为无向图 G…