Vue 事件原理(从源码角度带你分析)(3)

612 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

之前我们已经介绍了event的编译过程(点击这里跳转),接下来我们分析在Vue初始化和更新的过程中event是如何生成的。

event生成之原生DOM事件

Vueevent事件分为原生DOM事件与自定义事件,在普通元素节点上只能绑定原生DOM事件。Vue内部的DOM事件在初次更新的时候如何生成,在更新元素节点时DOM事件如何更新,下面我们来通过源码看一下这个过程:

1.初始化

无论是在DOM节点初始化还是更新的时候都会执行updateDOMListeners函数来初始化事件和更新事件,源码如下:

updateDOMListeners

function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  // 储存新节点的on属性
  const on = vnode.data.on || {}
  // 储存旧节点的on属性,初始化的时候为空
  const oldOn = oldVnode.data.on || {}
  // 当前节点的elm
  target = vnode.elm
  // 处理v-model,这里跳过
  normalizeEvents(on)
  // 执行updateListeners
  updateListeners(on, oldOn, add, remove, vnode.context)
  target = undefined
}

初始化的时候,oldVnode未定义,这里会做下缓存,再将当前的真实节点储存在target中,执行updateListeners

updateListeners:

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  vm: Component
) {
  let name, def, cur, old, event
  // 遍历on对象
  for (name in on) {
    // 拿到新值
    def = cur = on[name]
    // 拿到旧值,初始化的时候为空
    old = oldOn[name]
    // 解析修饰符,返回解析对象
    event = normalizeEvent(name)
    ......
    if (isUndef(cur)) {
      // cur未定义,报错
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    } else if (isUndef(old)) {
      // 初始化的时候old不存在,进入该逻辑
      if (isUndef(cur.fns)) {
        // 初始化的时候cur上未定义fns,进入该逻辑,调用createFnInvoker,cur为参数
        cur = on[name] = createFnInvoker(cur)
      }
      // 调用add方法,将event对象属性当做参数传入
      add(event.name, cur, event.once, event.capture, event.passive, event.params)
    } else if (cur !== old) {
      // 更新的时候进入该逻辑
      old.fns = cur
      on[name] = old
    }
  }
  // 遍历oldOn, 初始化的时候没有oldOn
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

// 之前编译的过程中,将相应的修饰符转换成了'&'、'~'、'!',这里转换出来,处理成对象返回
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
  }
})

这里会遍历编译生成的on对象中,拿到其中的事件,解析其中的修饰符,返回事件对象,调用createFnInvoker函数生成最终的执行函数:

createFnInvoker

export function createFnInvoker (fns: Function | Array<Function>): Function {
  // 定义invoker函数
  function invoker () {
    // 拿到fns,也就是传入的函数
    const fns = invoker.fns
    // 如果是函数数组,遍历数组,依次执行里面的函数
    if (Array.isArray(fns)) {
      const cloned = fns.slice()
      for (let i = 0; i < cloned.length; i++) {
        cloned[i].apply(null, arguments)
      }
    } else {
    // 定义的普通函数,直接调用
      // return handler return value for single handlers
      return fns.apply(null, arguments)
    }
  }
  // 传入的函数赋值给fns
  invoker.fns = fns
  // 返回invoker函数
  return invoker
}

再将生成的函数及解析后的事件对象当做参数传入add函数,调用add函数:

add

function add (
  event: string,
  handler: Function,
  once: boolean,
  capture: boolean,
  passive: boolean
) {
  // 包装一层,异步执行的时候,进入宏任务
  handler = withMacroTask(handler)
  // 如果定义了once,将函数做一层包装,执行一次后销毁该函数
  if (once) handler = createOnceHandler(handler, event, capture)
  // 这儿绑定原生事件
  target.addEventListener(
    event, // 事件名
    handler,  // 函数
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a (macro) task instead of a microtask.
 */
// 包装一层,使用宏任务执行函数
export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

// 给函数包装一下,函数只能执行一次
function createOnceHandler (handler, event, capture) {
  const _target = target // save current target element in closure
  return function onceHandler () {
    const res = handler.apply(null, arguments)
    if (res !== null) {
      remove(event, onceHandler, capture, _target)
    }
  }
}

add函数的作用就是给实际的DOM节点添加原生事件。

2.更新

更新的过程也同样会执行updateDOMListeners函数:

updateDOMListeners

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  vm: Component
) {
  let name, def, cur, old, event
  // 遍历on对象
  for (name in on) {
    // 拿到新值
    def = cur = on[name]
    // 拿到旧值,初始化的时候为空
    old = oldOn[name]
    // 解析修饰符,返回解析对象
    event = normalizeEvent(name)
    ......
    if (isUndef(cur)) {
      ......
    } else if (isUndef(old)) {
      ......
    } else if (cur !== old) {
      // 更新的时候进入该逻辑
      old.fns = cur
      on[name] = old
    }
  }
  // 遍历oldOn, 初始化的时候没有oldOn
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

function remove (
  event: string,
  handler: Function,
  capture: boolean,
  _target?: HTMLElement
) {
  // 执行原生DOM操作方法移除事件
  (_target || target).removeEventListener(
    event,
    handler._withTask || handler,
    capture
  )
}

更新的过程比较简单,判断新的DOM方法与老的方法不相同的时候,会将老方法的fns属性指向更新的方法,这也是为什么初始化事件方法的时候需要调用createFnInvoker给函数做个包装。然后会遍历老节点上的方法,当新节点没有定义相同的方法的时候,移除该方法。

小结

event的原生DOM事件的生成和更新过程还是较为简单的,都是对编译过程中生成的event进行处理,结合不同的修饰符,通过原生的DOM操作方法将事件函数挂载到元素节点上。

下一节我们分析自定义事件的生成过程。