【Vue2.0源码学习】实例方法篇-事件相关方法

简介: 【Vue2.0源码学习】实例方法篇-事件相关方法

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 = this
    if (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 = this
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const 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
    // all
    if (!arguments.length) {
        vm._events = Object.create(null)
        return vm
    }
    // array of events
    if (Array.isArray(event)) {
        for (let i = 0, l = event.length; i < l; i++) {
            this.$off(event[i], fn)
        }
        return vm
    }
    // specific event
    const cbs = vm._events[event]
    if (!cbs) {
        return vm
    }
    if (!fn) {
        vm._events[event] = null
        return vm
    }
    if (fn) {
        // specific handler
        let cb
        let i = cbs.length
        while (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] = null
    return vm
}

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

if (fn) {
    // specific handler
    let cb
    let i = cbs.length
    while (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 = this
    function on () {
        vm.$off(event, on)
        fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$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方法的内部原理。

目录
相关文章
|
自然语言处理 Python
wordcloud:自定义背景图片,生成词云
wordcloud:自定义背景图片,生成词云
wordcloud:自定义背景图片,生成词云
|
自然语言处理 JavaScript
【Vue2.0源码学习】模板编译篇-模板解析(代码生成阶段)
【Vue2.0源码学习】模板编译篇-模板解析(代码生成阶段)
125 0
|
小程序 JavaScript
小程序自定义弹窗禁止底部内容滚动(滚动穿透问题)
小程序自定义弹窗禁止底部内容滚动(滚动穿透问题)
1930 0
修改elementui table 组件滚动条样式
修改elementui table 组件滚动条样式
263 0
|
8月前
|
数据采集 监控 数据可视化
智能数据建设与治理 Dataphin试用评测
本文是一位产品经理对阿里云DataPhin的使用评测,主要围绕数据治理与资产运营展开。文中详细解析了智能数据建模、数据标准管理等核心功能,以及数据地图和数据质量监控带来的效率提升。同时指出权限管理和第三方工具集成等方面的待优化点,并提出增加沙箱环境、行业案例库等建议,为新用户提供参考。整体评价显示,DataPhin在提升工作效率和降低人力成本方面表现出色,但仍需进一步完善细节功能以满足复杂场景需求。
|
编解码 前端开发 JavaScript
【长文慎入】一文吃透React SSR服务端同构渲染
前段时间一直在研究 react ssr技术,然后写了一个完整的 ssr开发骨架。今天写文,主要是把我的研究成果的精华内容整理落地,另外通过再次梳理希望发现更多优化的地方,也希望可以让更多的人少踩一些坑,让更多的人理解和掌握这个技术。 相信看过本文(前提是能对你的胃口,也能较好的消化吸收)你一定会对 react ssr服务端渲染技术有一个深入的理解,可以打造自己的脚手架,更可以用来改造自己的实际项目,当然这不仅限于 react ,其他框架都一样,毕竟原理都是相似的。
1728 0
|
小程序 前端开发
【微信小程序-原生开发】实用教程22 - 绘制图表(引入 echarts,含图表的懒加载-获取到数据后再渲染图表,多图表加载等技巧)
【微信小程序-原生开发】实用教程22 - 绘制图表(引入 echarts,含图表的懒加载-获取到数据后再渲染图表,多图表加载等技巧)
1094 0
|
存储 C++ 容器
c++实现哈希桶
这篇文章回顾了闭散列的概念,指出在数据冲突时,闭散列会自动寻找后续未占用的位置插入数据。然而,这种方法可能导致某些元素状态变为删除,从而在查找时产生问题。为了解决这个问题,文章介绍了拉链法(哈希桶)作为改进策略。拉链法在每个哈希表位置上维护一个链表,冲突的数据挂载在相应位置的链表上。文章详细描述了拉链法的插入、查找和删除操作,并提供了相关代码示例。在插入过程中,当负载因子达到1时,哈希表会进行扩容,同时避免了频繁创建和销毁节点,提高了效率。最后,文章通过测试代码展示了拉链法的正确性。
|
缓存 JavaScript API
Vue新一代状态管理工具—Pinia—都2023年了,快学起来吧!
Vue新一代状态管理工具—Pinia—都2023年了,快学起来吧!
245 0
|
缓存 网络协议
DNS协议 是什么?DNS 完整的查询过程是怎样的
DNS协议 是什么?DNS 完整的查询过程是怎样的
516 0