vue核心面试题:vue中的事件绑定原理

一、事件绑定有几种?

1.原生的事件绑定,原生 dom 事件的绑定,采用的是 addEventListener 实现。

2.组件的事件绑定,组件绑定事件采用的是 $on 方法 。

let compiler = require('vue-template-compiler'); // vue loader中的包
let r1 = compiler.compile('<div @click="fn()"></div>'); // 给普通标签绑定click事件
// 给组件绑定一个事件,有两种绑定方法
// 一种@click.native,这个绑定的就是原生事件
// 另一种@click,这个绑定的就是组件自定义事件
let r2 = compiler.compile('<my-component @click.native="fn" @click="fn1"></mycomponent>');
console.log(r1.render); // {on:{click}} 
console.log(r2.render); // {nativeOn:{click},on:{click}}
// 为什么组件要加native?因为组件最终会把nativeOn属性放到on的属性中去,这个on会单独处理
// 组件中的nativeOn 等价于 普通元素on,组件on会单独处理

二、怎么回答

vue中的事件绑定是有两种,一种是原生的事件绑定,另一种是组件的事件绑定。原生的事件绑定在普通元素上是通过@click进行绑定,在组件上是通过@click.native进行绑定,组件中的nativeOn是等价于on的。组件的事件绑定的@click是vue 中自定义的 $on 方法来实现的,必须有$emit才可以触发。

原生事件绑定原理:在runtime下的patch.js中createPatchFunction执行了之后再赋值给patch。createPatchFunction方法有两个参数,分别是nodeOps存放操作dom节点的方法和modulesmodules是有两个数组拼接起来的,modules拼接完的数组中有一个元素就是events,事件添加就发生在这里。events元素关联的就是events.js文件,在events中有一个updateDOMListeners方法,在events文件的结尾导出了一个对象,然后对象有一个属性叫做create,这个属性关联的就是updateDOMListeners方法。在执行createPatchFunction方法时,就会将这两个参数传入,在createPatchFunction方法中接收了一个参数backend,在该方法中一开始进行backend的解构,就是上面的nodeOps和modules参数,解构完之后进入for循环。在createPatchFunction开头定义了一个cbs对象。for循环遍历一个叫hooks的数组。hooks是文件一开头定义的一个数组,其中包括有create,for循环就是在cbs上定义一系列和hooks元素相同的属性,然后键值是一个数组,然后数组内容是modules里面的一些内容。这时就把events文件中导出来的create属性放在了cbs上。当我们进入首次渲染的时候,会执行到patch函数里面的createElm方法,这个方法中就会调用invokeCreateHooks函数,用来处理事件系统,这里就是真正准备进行原生事件绑定的入口。invokeCreateHooks方法中,遍历了cbs.create数组里面的内容。然后把cbs.create里面的函数全部都执行一次,cbs.create其中一个函数就是updateDOMListenersupdateDOMListeners就是用来添加事件的方法,在这方法中会根据vnode判断是否有定义一个点击事件。如果没有点击事件就return。有的话就继续执行,给on进行赋值,然后进行一些赋值操作,将vnode.elm赋值给target,elm这个属性就是指向vnode所对应的真实dom节点,这里就是把我们要绑定事件的dom结点进行缓存,接下来执行updateListeners方法在接下来执行updateListeners方法中调用了一个add的方法,然后在app方法中通过原生addEventListener把事件绑定到dom上。

组件事件绑定原理:在组件实例初始化会调用initMixin方法中的Vue.prototype._init,在init函数中,会通过initInternalComponent方法初始化组件信息,将自定义的组件事件放到_parentListeners上,下来就会调用initEvents来初始化组件事件,在initEvents中会实例上添加一个 _event对象,用于保存自定义事件,然后获取到 父组件给 子组件绑定的自定义事件,也就是刚才在初始化组件信息的时候将自定义的组件事件放在了_parentListeners上,这时候vm.$options._parentListeners就是自定义的事件。最后进行判断,如果有自定义的组件事件就执行updateComponentListeners方法进行事件绑定,在updateComponentListeners方法中会调用updateListeners方法,并传传一个add方法进行执行,这个add方法里就是$on方法。

三、源码

1.创建组件createComponent(src/core/vdom/create-component.js)

创建组件时,会去找data中有没有nativeOn,会将data.on赋值到listeners上单独处理,将data.nativeOn赋值给data.on

  const listeners = data.on
  data.on = data.nativeOn

(1)把 on 赋值给了 listeners 

(2)listeners 传给了 VNode 构造函数,保存到了 vnode.componentOptions

2.原生dom的绑定

Vue 在创建真是 dom 时会调用 createElm ,默认会调用 invokeCreateHooks

会遍历当前平台下相对的属性处理代码,其中就有 updateDOMListeners 方法,内部会传入 add 方法

vue 中绑定事件是直接绑定给真实 dom 元素的

(3)真正添加原生事件的方法:updateDOMListeners方法(src/platforms/web/runtime/modules/events.js)

function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
    // 根据vnode判断是否有定义一个点击事件。有的话就继续执行,没有就return
    // 是一个点击事件的话就会给on进行赋值,进行一系列的赋值操作
    if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
        return
    } 
    const on = vnode.data.on || {}
    const oldOn = oldVnode.data.on || {}
    // 这里把target指向dom结点
    target = vnode.elm
    normalizeEvents(on)
    // 接下来执行updateListeners方法 
    updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
    target = undefined
}
function add (name: string,  handler: Function,  capture: boolean,  passive: boolean ) {
    target.addEventListener( // 给当前的dom添加事件
        name,
        handler,
        supportsPassive ? { capture, passive } : capture  )
}

vue把vnode.elm赋值给target,我们知道elm这个属性就是指向vnode所对应的真实dom节点所以这里就是把我们要绑定事件的dom节点进行缓存 

(1)首次渲染,或者是更新都会发生在createPatchFunction(src/platform/web/runtime/patch.js)

// 触发createPatchFunction方法
export const patch: Function = createPatchFunction({ nodeOps, modules })
// createPatchFunction执行了之后再赋值给patch。这个patch就是上面返回的patch函数。它在其他地方被用在渲染视图上
modules = [
   ref,
   directives,
   attrs,
   klass,
   events,
   domProps,
   style,
   transition
]

createPatchFunction有两个参数,第一个参数是存放操作dom节点的方法,另一个就是modules,它由两个数组拼接起来,modules其实就是一个数组(数组内容看上面代码),其中有一个元素叫做events。我们的事件添加就发生在这里。

events文件最后它导出了一些东西。它导出了一个对象,然后对象有一个属性叫做create。

export default {
  create: updateDOMListeners,
  update: updateDOMListeners
}

(2)createPatchFunction初始化钩子方法(src/core/vdom/patch.js)

createPatchFunction接收了一个参数叫做backend。然后在函数开头,对backend进行解构,就是上面代码的nodeOps和modules参数。

// createPatchFunction 中的部分代码
export function createPatchFunction (backend) {
  const { modules, nodeOps } = backend // 对backend进行解构
  // hooks定义在文件开头
  // const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
  // 在cbs上定义一系列和hooks元素相同的属性,然后键值是一个数组,然后数组内容是modules里面的一些内容
  for (i = 0; i < hooks.length; ++i) { // 解构完之后进入for循环。
    cbs[hooks[i]] = [] // 定义了一个cbs对象
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  function createElm ( // 创建元素,生成真正的dom节点
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
     /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        // 调用处理的钩子
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }
   }
   // invokeCreateHooks 真正准备进行原生事件绑定的入口方法
   function invokeCreateHooks (vnode, insertedVnodeQueue) {
     // 遍历cbs.create数组里面的内容。然后把cbs.create里面的函数全部都执行一次
     // cbs.create其中一个函数就是updateDOMListeners
     for (let i = 0; i < cbs.create.length; ++i) {
       cbs.create[i](emptyNode, vnode)
     }
     i = vnode.data.hook // Reuse variable
     if (isDef(i)) {
       if (isDef(i.create)) i.create(emptyNode, vnode)
       if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
     }
   }
 } 

(4)updateListeners方法(src/core/vdom/helpers/update-listeners.js)

在updateListeners调用了一个add方法

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component
) {
  let name, def, cur, old, event
  for (name in on) {
    def = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)
    /* istanbul ignore if */
    if (__WEEX__ && isPlainObject(def)) {
      cur = def.handler
      event.params = def.params
    }
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    } else if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur, vm)
      }
      if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture)
      }
      add(event.name, cur, event.capture, event.passive, event.params) // 重点看add方法
    } else if (cur !== old) {
      old.fns = cur
      on[name] = old
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

(5)add方法就是通过addEventListener把事件绑定到dom上(src/platforms/web/runtime/modules/events.js

function add (
  name: string,
  handler: Function,
  capture: boolean,
  passive: boolean
) {
  // 这里的target指向dom结点,
  // 执行到这里的时候target已经被赋值了
  target.addEventListener(
    name,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

3.组件中绑定事件

组件绑定事件是通过 vue 中自定义的 $on 方法来实现的 

(4)updateComponentListeners 注册事件(src/core/instance/events.js)

export function updateComponentListeners (
    vm: Component,
    listeners: Object,
    oldListeners: ?Object
) {
    target = vm
    // 执行updateListeners方法
    updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
    target = undefined
}
function add (event, fn) {
    target.$on(event, fn)
}

所以组件中可以通过$on('click', () => {})绑定事件,$emit('click')触发事件

(1)组件实例初始化会调用initMixin方法中的Vue.prototype._init(src/core/instance/init.js)

Vue.prototype._init = function (options?: Object) {
    if (options && options._isComponent) {
      initInternalComponent(vm, options) // 初始化组件信息,增加组件选项
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm) // 初始化事件对象
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

(2)initInternalComponent初始化组件信息(src/core/instance/init.js)

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  // 保存节点等信息
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  // _parentVnode是外壳节点
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode
  // 保存父组件给子组件的关联数据
  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  // 将listeners 转移到了vm.$option._parentListeners 
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag
  // 保存渲染数据
  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

(3)initEvents初始化事件对象(src/core/instance/init.js)

export function initEvents (vm: Component) {
  // 给实例上添加一个 _event对象,用于保存自定义事件
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // 获取到 父组件给 子组件绑定的自定义事件,也就是刚才在初始化组件信息的时候将自定义的组件事件放在了_parentListeners上
  const listeners = vm.$options._parentListeners
  // 如果有自定义的组件事件就执行updateComponentListeners方法
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

(5)updateListeners方法(同原生事件绑定调用的updateListeners方法)

此时这儿updateListeners的传入的add方法就是

function add (event, fn) {
  target.$on(event, fn)
}

 

  • 15
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值