vue2源码解析之事件系统$on

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

1. 前言

事件系统在 vue 中是非常常用的, 但是我对它一直是会用的地步, 看过文章和源码, 对它算是有过较为深入的了解, 今天试着看看能不能将它的来龙去脉搞清楚(欢迎评论啊~~)

2. 源码

事件系统由$on,$once,$off,$emit四个Vue原型上的方法组成, 是在vue初始化的时候eventsMixin混入的

const hookRE = /^hook:/
复制代码

2.1 $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++) {
      // 如果是数组就递归调用自己, 所以可以写出
      // this.$on(
      //   [
      //     ['a', 'b'],
      //     ['cd', 'ef'],
      //   ],
      //   fn
      // )
      // 这样奇怪的代码, 来监听`a`,`b`,`cd`,`ef`四个事件
      vm.$on(event[i], fn)
    }
  } else {
    // 把需要监听的事件存放到数组里
    // 最后vm._events 会长成这样
    // vm._events = {
    //   a: [fn1, fn2],
    //   b: [fn3, fn4],
    //   cd: [fn5],
    //   ef: [fn6],
    // }
    // 并且可以看到, 这里并没有对fn去重, 所以
    // vue.$on('a', fn)
    // vue.$on('a', fn)
    // vue.$emit('a')
    // fn会执行两次
    ;(vm._events[event] || (vm._events[event] = [])).push(fn)
  }
  return vm
}
复制代码

2.2 $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
}
复制代码

2.3 $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
}
复制代码

2.4 $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
}
复制代码

Vue中只有这一套事件系统, 可以看出事件的绑定和触发必须在同一个Vue实例(组件)上, 那么父子间传值看起来好像是在父组件上绑定的事件, 而在子组件上触发的事件, 到底是怎么回事呢? 下面我来讨论一下

3. 父子间传值

先组装一个简单的父子嵌套组件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./assets/vue-2.6.14.js"></script>
  </head>

  <body>
    <div id="app">
      <a-btn @test-event="testEventHandler"></a-btn>
    </div>
    <script>
      const ABtn = {
        name: 'a-btn',
        template: `<button @click="clickBtn">按钮</button>`,
        methods: {
          clickBtn() {
            this.$emit('test-event')
          },
        },
      }
      const vue = new Vue({
        components: {
          ABtn,
        },
        template: '#app',
        methods: {
          testEventHandler() {
            console.log(this)
          },
        },
      })
      vue.$mount('#app')
    </script>
  </body>
</html>
复制代码

3.1 找出父组件 vnode

我们首先看看template生成的渲染函数render长啥样, 在父组件选项中添加

const options = {
  // ... 原有参数
  beforeMount() {
    console.log(this.$options.render.toString())
    // 打印如下渲染函数, 就长下面这样
    // function anonymous() {
    //   with (this) {
    //     return _c('a-btn', { on: { 'test-event': testEventHandler } })
    //   }
    // }
  },
}
复制代码

然后我们再跟一下, 看看渲染函数, 生成的vnode长啥样

$mount时会调vm._render,然后会调到this.$options.render

注意渲染函数_c中第二个参数, 跟进去

// steps - 1 找到_c定义
vm._c = function (a, b, c, d) {
  return createElement(vm, a, b, c, d, false)
}
// steps - 2 跟进 createElement
function createElement(/* 先不在意参数 */) {
  /* 过程也不要在意 */
  return _createElement()
}
// steps - 3 跟进 _createElement
function _createElement(/* 先不在意参数 */) {
  // tag 是 a-btn
  if (typeof tag === 'string') {
    if (config.isReservedTag(tag)) {
    } else if (/* 这里是 */ true) {
      // component 这里要跟进去,
      vnode = createComponent()
    }
  }
  return vnode
}
//  steps - 4 跟进 createComponent
function createComponent(/* 先不在意参数 */) {
  // 从 _c 第二个参数中拿到on
  var listeners = data.on
  var vnode = new VNode(
    'vue-component-' + Ctor.cid + (name ? '-' + name : ''),
    data,
    undefined,
    undefined,
    undefined,
    context,
    {
      Ctor: Ctor,
      propsData: propsData,
      listeners: listeners, // 记住这个是第7个参数
      tag: tag,
      children: children,
    },
    asyncFactory
  )

  return vnode
}
//  steps - 5 跟进 VNode
var VNode = function VNode(
  tag,
  data,
  children,
  text,
  elm,
  context,
  componentOptions, // 这里
  asyncFactory
) {
  this.componentOptions = componentOptions
}
复制代码

跟到最后, 我们得到, 父组件的渲染函数生成的vnode长这样

const vnode = {
  // ... 其它属性
  componentOptions: {
    listeners: {
      'test-event': testEventHandler, // 注意 testEventHandler 是父组件中的处理函数, with(this) 时的this指向的是父组件
    },
  },
}
复制代码

3.2 怎么把事件监听到子组件上去的

在上面我们已经得到了事件在父组件生成的vnode的样子, 下面我们看看最后是怎样把事件监听到子组件上去的

初始子组件

//  steps - 1 跟进 初始化
Vue.prototype._init = function (options) {
  if (options && options._isComponent) {
    // 这里要进入看一下
    initInternalComponent(vm, options)
  }
  // 跟进去
  initEvents(vm)
}
//  steps - 2 跟进 initInternalComponent
function initInternalComponent(vm, options) {
  var opts = (vm.$options = Object.create(vm.constructor.options))
  // 拿到父组件vnode
  var parentVnode = options._parentVnode
  var vnodeComponentOptions = parentVnode.componentOptions
  // 拿到父组件上的 事件监听, 保存在了 vm.$options._parentListeners 上
  opts._parentListeners = vnodeComponentOptions.listeners
}
//  steps - 3 跟进 initEvents
function initEvents(vm) {
  // 这里是保存子组件上的所有监听的事件, 刚开始为空, 可以看上面的源码解析部分
  vm._events = Object.create(null)
  // 拿到从`initInternalComponent`中保存到vm.$options._parentListeners上的父组件上监听的事件
  var listeners = vm.$options._parentListeners
  if (listeners) {
    // 这个跟进去
    updateComponentListeners(vm, listeners)
  }
}
//  steps - 4 跟进 updateComponentListeners
function updateComponentListeners(vm, listeners, oldListeners) {
  // 注意这里的target是子组件了
  target = vm
  // 再进去
  updateListeners(
    listeners,
    oldListeners || {},
    add,
    remove$1,
    createOnceHandler,
    vm
  )
  target = undefined
}
//  steps - 5 跟进 updateListeners
function updateListeners(on, oldOn, add, remove$$1, createOnceHandler, vm) {
  var name, def$$1, cur, old, event
  for (name in on) {
    def$$1 = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)
    if (isUndef(cur)) {
    } else if (isUndef(old)) {
      // 初始化监听时, oldOn是空对象, 所以全部都走到这里了
      // 跟进去
      add(event.name, cur, event.capture, event.passive, event.params)
    }
  }
}
//  steps - 6 跟进 add
function add(event, fn) {
  // 结束, 子组件通过 $on 监听的事件, 处理函数fn 是在父组件上注册的
  target.$on(event, fn)
}
复制代码

4. 写一下例子

4.1 公共事件eventBus

const eventBus = new Vue()
const arrowFn = (...args) => {
  // 这里可以传入多个参数
  // 使用了箭头函数, 回调时绑定不了this了
  console.log(args, this)
}
eventBus.$on('test-arrow', arrowFn)
eventBus.$emit('test-arrow', 'arrow参数1', 'arrow参数2')
// 打印
eventBus.$off('test-arrow', arrowFn)
eventBus.$emit('test-arrow', 'arrow参数1', 'arrow参数2')
// 事件已经被注销了

const commonFn = function (...args) {
  // 这里可以传入多个参数
  // 这里绑定this指向Vue实例
  console.log(args, this)
}
eventBus.$once('test-common', commonFn)
eventBus.$emit('test-common', 'common参数1', 'common参数2')
// 事件被触发一次就被注销了
eventBus.$emit('test-common', 'common参数1', 'common参数2')
复制代码

image.png

4.2 父子之间传值

5. 最后

这篇文章分析了Vue事件系统的源码, 并且跟出了父组件上的@事件其实是绑定在子组件上的. 下面列一下我前面写的几篇文章

  1. vue2 源码解析之 nextTick
  2. 代码片段之 js 限流调度器
  3. 数据结构与算法之链表(1)

欢迎加我微信和我交流呀 yangdonglin520l

猜你喜欢

转载自juejin.im/post/7016705138530189349