汪图南
  • RAG

    • RAG
  • 快速入门
  • 高级技巧
前端面试之道
  • 打包工具

    • Webpack
    • Rollup
  • TypeScript

    • TypeScript基础
    • TypeScript类型挑战
  • CSS预编译器

    • SASS
  • 自动化测试

    • Vue应用测试
  • Vue2.0源码分析
  • Vue3.0源码分析
  • 数据结构和算法(基础)
  • LeetCode(刷题)
  • JavaScript书籍

    • 你不知道的JavaScript(上)
    • 你不知道的JavaScript(中下)
    • JavaScript数据结构和算法
    • JavaScript设计模式与开发实践
    • 深入理解ES6
  • Git书籍

    • 精通Git
Github
  • RAG

    • RAG
  • 快速入门
  • 高级技巧
前端面试之道
  • 打包工具

    • Webpack
    • Rollup
  • TypeScript

    • TypeScript基础
    • TypeScript类型挑战
  • CSS预编译器

    • SASS
  • 自动化测试

    • Vue应用测试
  • Vue2.0源码分析
  • Vue3.0源码分析
  • 数据结构和算法(基础)
  • LeetCode(刷题)
  • JavaScript书籍

    • 你不知道的JavaScript(上)
    • 你不知道的JavaScript(中下)
    • JavaScript数据结构和算法
    • JavaScript设计模式与开发实践
    • 深入理解ES6
  • Git书籍

    • 精通Git
Github
  • 介绍

    • 介绍和参考
  • 源码目录设计和架构设计

    • 设计
  • Rollup构建版本

    • Rollup基础知识
    • Vue中的Rollup构建
  • 从入口到构造函数整体流程

    • 整体流程
    • initGlobalAPI流程
    • initMixin流程
    • stateMixin流程
    • eventsMixin流程
    • lifecycleMixin流程
    • renderMixin流程
  • 响应式原理

    • 介绍
    • 前置核心概念
    • props处理
    • methods处理
    • data处理
    • computed处理
    • watch处理
    • 深入响应式原理
    • 依赖收集
    • 派发更新
    • nextTick实现原理
    • 变化侦测注意事项
    • 变化侦测API实现
  • 虚拟DOM和VNode

    • 虚拟DOM
    • VNode介绍
    • Diff算法
  • 组件化

    • 介绍
    • $mount方法
    • render和renderProxy
    • createElement
    • createComponent
    • 合并策略
    • update和patch
    • 组件生命周期
    • 组件注册
  • 编译原理

    • 介绍
    • compileToFunctions
    • parse模板解析
    • optimize优化
    • codegen代码生成
  • 扩展

    • 扩展
    • directive指令
    • filter过滤器
    • event事件处理
    • v-model
    • 插槽
    • Keep-Alive
    • Transition
    • Transition-Group
    • Vue.use插件机制
  • Vue-Router

    • 介绍
    • 路由安装
    • matcher介绍
    • 路由切换
    • 内置组件
    • 路由hooks钩子函数
  • Vuex

    • 介绍
    • Vuex安装
    • Vuex初始化
    • Vuex辅助API
    • Store实例API

eventsMixin流程

在使用Vue做开发的时候,我们一定经常使用到$emit、$on、$off和$once等几个实例方法,eventsMixin主要做的就是在Vue.prototype上定义这四个实例方法:

export function eventsMixin (Vue) {
  // 定义$on
  Vue.prototype.$on = function (event, fn) {}

  // 定义$once
  Vue.prototype.$once = function (event, fn) {}

  // 定义$off
  Vue.prototype.$off = function (event, fn) {}

  // 定义$emit
  Vue.prototype.$emit = function (event) {}
}

通过以上代码,我们发现eventsMixin()所做的事情就是使用发布-订阅模式来处理事件,接下来让我们先使用发布-订阅实现自己的事件中心,随后再来回顾源码。

$on的实现

$on方法的实现比较简单,我们先来实现一个基础版本的:

function Vue () {
  this._events = Object.create(null)
}

Vue.prototype.$on = function (event, fn) {
  if (!this._events[event]) {
    this._events[event] = []
  }
  this._events[event].push(fn)
  return this
}

接下来对比一下Vue源码中,关于$on的实现:

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  const vm: Component = this
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn)
    }
  } else {
    (vm._events[event] || (vm._events[event] = [])).push(fn)
    // optimize hook:event cost by using a boolean flag marked at registration
    // instead of a hash lookup
    if (hookRE.test(event)) {
      vm._hasHookEvent = true
    }
  }
  return vm
}

代码分析:

  1. 我们发现在Vue源码中,$on方法还接受一个数组event,这其实是在Vue2.2.0版本以后才有的,当传递一个event数组时,会通过遍历数组的形式递归调用$on方法。
  2. 我们还发现,所有$on的事件全部绑定在_events私有属性上,这个属性其实是在我们上面已经提到过的initEvents()方法中被定义的。
export function initEvents (vm) {
  vm._events = Object.create(null)
}

$emit的实现

我们先来实现一个简单的$emit方法:

Vue.prototype.$emit = function (event) {
  const cbs = this._events[event]
  if (cbs) {
    const args = Array.prototype.slice.call(arguments, 1)
    for (let i = 0; i < cbs.length; i++) {
      const cb = cbs[i]
      cb && cb.apply(this, args)
    }
  }
  return this
}

接下来,我们使用$emit和$on来配合测试事件的监听和触发:

const app = new Vue()
app.$on('eat', (food) => {
  console.log(`eating ${food}!`)
})
app.$emit('eat', 'orange')
// eating orange!

最后我们来看Vue源码中关于$emit的实现:

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)
    const info = `event handler for "${event}"`
    for (let i = 0, l = cbs.length; i < l; i++) {
      invokeWithErrorHandling(cbs[i], vm, args, vm, info)
    }
  }
  return vm
}

代码分析:

  1. 从整体上看,$emit实现方法非常简单,第一步从_events对象中取出对应的cbs,接着一个个遍历cbs数组、调用并传参。
  2. invokeWithErrorHandling代码中会使用try/catch把我们函数调用并执行的地方包裹起来,当函数调用出错时,会执行Vue的handleError()方法,这种做法不仅更加友好,而且对错误处理也非常有用。

$off的实现

$off方法的实现,相对来说比较复杂一点,因为它需要根据不同的传参做不同的事情:

  • 当没有提供任何参数时,移除全部事件监听。
  • 当只提供event参数时,只移除此event对应的监听器。
  • 同时提供event参数和fn回调,则只移除此event对应的fn这个监听器。

在了解了以上功能点后,我们来实现一个简单的$off方法:

Vue.prototype.$off = function (event, fn) {
  // 没有传递任何参数
  if (!arguments.length) {
    this._events = Object.create(null)
    return this
  }
  // 传递了未监听的event
  const cbs = this._events[event]
  if (!cbs) {
    return this
  }
  // 没有传递fn
  if (!fn) {
    this._events[event] = null
    return this
  }
  // event和fn都传递了
  let i = cbs.length
  let cb
  while (i--) {
    cb = cbs[i]
    if (cb === fn) {
      cbs.splice(i, 1)
      break
    }
  }
  return this
}

接下来,我们撰写测试代码:

const app = new Vue()
function eatFood (food) {
  console.log(`eating ${food}!`)
}
app.$on('eat', eatFood)
app.$emit('eat', 'orange')
app.$off('eat', eatFood)
// 不执行回调
app.$emit('eat', 'orange')

最后我们来看Vue源码中关于$off的实现:

Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
  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++) {
      vm.$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
  }
  // 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
}

$once的实现

关于$once方法的实现比较简单,可以简单的理解为在回调之后立马调用$off,因此我们来实现一个简单的$once方法:

Vue.prototype.$once = function (event, fn) {
  function onFn () {
    this.$off(event, onFn)
    fn.apply(this, arguments)
  }
  this.$on(event, onFn)
  return this
}

接着我们对比一下Vue源码中的$once方法:

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

注意:在源码中$once的实现是在回调函数中使用fn绑定了原回调函数的引用,在上面已经提到过的$off方法中也同样进行了cb.fn === fn的判断。

在实现完以上几种方法后,我们可以得到eventsMixin如下流程图:

最后更新时间: 2025/5/6 15:36
贡献者: wangtunan
Prev
stateMixin流程
Next
lifecycleMixin流程