Vue 初始化之 initEvents

2022/11/15 Vue

上一篇看了 Vue 初始化生命周期的一些内外部的属性,这篇继续往下看,来看看 initEvents,初始化事件相关。

在 Vue 中,在父组件中使用子组件时可以给子组件上注册一些事件,这些事件即包括使用 v-on 或@注册的自定义事件,以及一些浏览器原生的事件(.native)。

export function initEvents(vm: Component) {
  // 创建一个原型为null的新对象,用来存放父组件绑定在当前组件上的事件
  vm._events = Object.create(null);
  // 父组件是否通过 "@hook:XXX" 把钩子函数绑定在当前组件上
  vm._hasHookEvent = false;
  // 初始化父组件添加的事件
  // _parentListeners 就是父组件注册的事件
  const listeners = vm.$options._parentListeners;
  if (listeners) {
    // 如果存在,执行 updateComponentListeners
    updateComponentListeners(vm, listeners);
  }
}

initEvents 在初始化的时候,创建一个空对象来存放父组件绑定的事件,把_hasHookEvent 设置为 false,判断父组件注册的事件,如果有,就执行 updateComponentListeners。但是在 Vue 初始化的时,没有父组件,updateComponentListeners 在初始化子组件的时候执行。

不过我们也来看一下 updateComponentListeners 这个函数

let target: any;

export function updateComponentListeners(
  vm: Component,
  listeners: Object,
  oldListeners?: Object | null,
) {
  target = vm;
  updateListeners(
    listeners,
    oldListeners || {}, 
    add, 
    remove, 
    createOnceHandler, 
    vm
  );
  target = undefined;
}

从上面的代码来看,updateListeners 就是调用了 updateListeners 方法,传入了一些参数。我们来看看这些参数:

function add(event, fn) {
  // 执行 vm.$on 方法
  target.$on(event, fn);
}

add 方法其实就是执行了 vm.$on 方法,$on 方法在 eventsMixin 函数里

const hookRE = /^hook:/;
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  // 缓存this
  const vm: Component = this;
  // 如果 event 是数组,那就遍历的调用本身来处理
  if (isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn);
    }
  } else {
    // 把 event 和回调 fn 传入到 _events 中
    (vm._events[event] || (vm._events[event] = [])).push(fn);
    // 这个bool标志位来表明是否存在钩子,
    // 而不需要通过哈希表的方法来查找是否有钩子,
    // 这样做可以减少不必要的开销,优化性能
    // 主要是为了判断有没有父组件的@hook:这样的钩子事件
    if (hookRE.test(event)) {
      vm._hasHookEvent = true;
    }
  }
  // 返回vm
  return vm;
};

通过上面的代码,我们可以看到,Vue.$on事件主要就是做了一件事,就是把event 放到_events数组里,等emit的时候去调用。

function remove(event, fn) {
  // 执行 vm.$off 方法,取消 event 的 fn 的回调
  target.$off(event, fn)
}

remove方法调用了$off方法,我们看看 $off 方法

Vue.prototype.$off = function (
  event?: string | Array<string>,
  fn?: Function
): Component {
  // 缓存this
  const vm: Component = this
  // 如果没有传递参数的话,则移除所有注册的事件和回调函数
  if (!arguments.length) {
    vm._events = Object.create(null)
    return vm
  }
  // 如果 event 是数组的话,则遍历 event 数组,并依次调用 this.$off(event[i], fn)
  if (isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$off(event[i], fn)
    }
    return vm
  }
  // 单个的 event 事件,通过 vm._events[event] 获取 event 事件的回调函数数组
  const cbs = vm._events[event!]
  // 如果 cbs 不存在的话,说明没有注册 event 事件,直接 return 返回即可
  if (!cbs) {
    return vm
  }
  // 如果没有回调函数,就把当前事件的所有回调都删除
  if (!fn) {
    vm._events[event!] = null
    return vm
  }
  // 删除某个回调 fn
  let cb
  let i = cbs.length
  while (i--) {
    cb = cbs[i]
    if (cb === fn || cb.fn === fn) {
      cbs.splice(i, 1)
      break
    }
  }
  // 返回vm
  return vm
}

$off 方法就是删除 _events 里的某个事件某个回调或者全部回调函数

createOnceHandler 函数,是用来处理.once 修饰符修饰的事件的

// 创建只执行一次的 once 事件,返回一个函数体
function createOnceHandler(event, fn) {
  // 缓存 target
  const _target = target
  return function onceHandler() {
    // 调用定义的回调
    const res = fn.apply(null, arguments)
    if (res !== null) {
      // 如果返回值不为 null 则取消监听
      // 返回 null 则 .once 修饰符失效
      _target.$off(event, onceHandler)
    }
  }
}

这个函数创建一个只执行一次的 once 事件,返回一个函数,去执行事件的回调。

接下来,我们来看updateListeners这个函数。

export function updateListeners(
  on: Object, // listeners
  oldOn: Object, // old listener
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component
) {
  let name, cur, old, event
  // 遍历新 listeners 的事件
  for (name in on) {
    cur = on[name] // 获取新 listener 里的 事件
    old = oldOn[name] // 获取旧 listener 里的 事件
    event = normalizeEvent(name) // 规范化 event
    // 如果 cur 不存在
    if (isUndef(cur)) {
      // 非生产环境 报错提示
      __DEV__ &&
        warn(
          `Invalid handler for event "${event.name}": got ` + String(cur),
          vm
        )
    } else if (isUndef(old)) {
      // 第一次创建

      // 如果 当前事件 的 fns 属性不存在
      if (isUndef(cur.fns)) {
        // 给 cur 和 on[name] 都创建一个回调
        cur = on[name] = createFnInvoker(cur, vm)
      }
      // 如果带 once 操作符
      if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture)
      }
      // 调用传入的add函数
      // updateComponentListeners 和 updateDOMListeners 调用的 add 方法不同,
      // updateDOMListeners 里的 add 方法在 src/platforms/web/runtime/modules/events.ts
      // updateComponentListeners 里的 add 方法 src/core/instance/events.ts
      add(event.name, cur, event.capture, event.passive, event.params)
    } else if (cur !== old) {
      // 更新时触发, 条件是 cur 和 old 不同
      // 修改回调函数
      old.fns = cur
      // 保留引用关系
      on[name] = old
    }
  }
  // 在更新阶段
  // 如果新 listeners 中没有当前事件名
  // 删除旧 listeners 里的当前事件
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      // 删除 name 对应的方法
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

这个函数的作用是对比listeners和oldListeners的不同,并调用参数中提供的add和remove进行相应的注册事件和卸载事件。

首先遍历新的 listeners,通过name规范化event

// 判断事件是否有 once、capture、passive 等修饰符
const normalizeEvent = cached(
  (
    name: string
  ): {
    name: string
    once: boolean
    capture: boolean
    passive: boolean
    handler?: Function
    params?: Array<any>
  } => {
    const passive = name.charAt(0) === '&'
    name = passive ? name.slice(1) : name
    const once = name.charAt(0) === '~' // Prefixed last, checked first
    name = once ? name.slice(1) : name
    const capture = name.charAt(0) === '!'
    name = capture ? name.slice(1) : name
    return {
      name,
      once,
      capture,
      passive
    }
  }
)

拿到规范化的event的name 之后,就去判断,如果新的事件在旧的 listeners里没有,代表第一次注册,就createFnInvoker 生成一个 invoker。

// 创建一个 invoker,然后 invoker 的 fns 属性等于 fns
export function createFnInvoker(
  fns: Function | Array<Function>,
  vm?: Component
): Function {
  
  function invoker() {
    const fns = invoker.fns
    // 回调是数组
    if (isArray(fns)) {
      // 缓存一下fns
      const cloned = fns.slice()
      for (let i = 0; i < cloned.length; i++) {
        invokeWithErrorHandling(
          cloned[i],
          null,
          arguments as any,
          vm,
          `v-on handler`
        )
      }
    } else {
      // return handler return value for single handlers
      return invokeWithErrorHandling(
        fns,
        null,
        arguments as any,
        vm,
        `v-on handler`
      )
    }
  }
  invoker.fns = fns
  return invoker
}

createFnInvoker函数定义了一个invoker函数,给invoker函数添加了一个属性fns,属性值就是定义事件的回调函数;最终返回invoker函数并赋值给on[name]

如果有 once 修饰符 ,就去调用传入的 createOnceHandler 生成只执行一次的handler

// 如果带 once 操作符
if (isTrue(event.once)) {
  cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}

对于第一次创建会调用传入的add函数,并通过$on 给components 绑定事件。而这里的 add 方法有4个参数,则是在处理 updateDOMListeners 的时候,对应 addEventListener 来给DOM 添加事件。

对于在 old 里存在的事件, 则是更新的时候:

// 更新时触发, 条件是 cur 和 old 不同
else if (cur !== old) {
  // 修改回调函数
  old.fns = cur
  // 保留引用关系
  on[name] = old
}

遍历old的所有事件,如果新事件中没有当前事件名,则通过remove删除之前绑定的事件。

// 如果新 listeners 中没有当前事件名
// 删除旧 listeners 里的当前事件
for (name in oldOn) {
  if (isUndef(on[name])) {
    event = normalizeEvent(name)
    // 删除 name 对应的方法
    remove(event.name, oldOn[name], event.capture)
  }
}

总结:

这篇文章主要来了解初始化Vue组件的事件,包括:@事件和自定义事件等,分析了initEvents函数的实现。了解vm 实例event 的 绑定,接触,以及存储的方式,事件都放在_events属性里,等$emit的时候,去触发回调函数