vue 组件通信 事件处理

22 篇文章 0 订阅
9 篇文章 0 订阅

本文主要是通过源码了解,vue是怎样把事件传递给子组件并能够给正常调用的。

首先看一个超级简单的demo

// 组件
var aDom = Vue.component('aDom', {
    template: ...,
    methods: {
        xxx() {
            this.$emit('aaa')
        }
    }
})
// vm
const vm = new Vue({
    el: '#app',
    template: `
        <div>
            <a-dom @aaa="aaaFun" />
        </div>
    `,
    methods: {
        aaaFun(){console.log('函数')}
    }
})

demo中有一个a-dom组件,父级向里边传入了一个aaa函数,个a-dom组件中通过this.$emit('aaa')调用了它,这是一个最简单的事件通信方法
跳过不相干的步骤,我们从创建a-dom组件的虚拟节点处的源码开始说起

调用:$emit

想了解他的实现原理,可以拆开Event Bus的整体流程,从$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
}

invokeWithErrorHandling函数就是一个通过apply|call调用并抛出错误的通用函数,文章最底下会简略说明该函数)

可以看到,$emit方法实际就是找到vm._events[event]并调用该素组中的所有方法,那我们现在的目标就是找到vm._events中的数据怎么存储的

存储 $on

$on函数的目的是把函数放入vm._event

  • 首先,判断形参event是否为一个数组(一个函数需要存多次),如果是数组,对其循环并再次执行vm.$on(event[i], fn)
  • 然后在push前会首先判断vm._events[event]是否有值,如果没有就赋值一个空数组((vm._events[event] || (vm._events[event] = [])).push(fn)
  • 最后进行push,在调用vm.$emit的时候就可以获取到了
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) { // 如果是数组,循环再执行$on
        for (let i = 0, l = event.length; i < l; i++) {
            vm.$on(event[i], fn)
        }
    } else {
        (vm._events[event] || (vm._events[event] = [])).push(fn) // 创建数组并进行push
        if (hookRE.test(event)) {
            vm._hasHookEvent = true
        }
    }
    return vm
}

存储的函数有了,那现在只要知道是怎样调用$on的就可以理解事件处理的整个流程了,z这个就需要从头开始说起…

_init初始化

首先我们要看下在模板渲染后组件是怎样获取到父级组件的方法的 下边是简略版的_init函数,忽略其他的,我们只要看两个函数

  • initInternalComponent(vm, options) :获取$options(在这里拿到aaa方法)
  • initEvents(vm) :初始化组件传入的方法(此方法为核心)
Vue.prototype._init = function (options?: Object) {
    const vm: Component = this // new后的this
    // a uid
    // 每个实例用_uid标记
    vm._uid = uid++
    vm._isVue = true
    // 合并选项
    // _isComponent表示是当前要处理的是一个component,这是在vdom/create-components.js里createComponentInstanceForVnode函数中定义的
    if (options && options._isComponent) {
        initInternalComponent(vm, options) // 创建$options并赋值父级传过来的数据
    } else {
        vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)
    }
    vm._renderProxy = vm
    // expose real self
    vm._self = vm
    initLifecycle(vm) // 初始化组件的父子关系 $parent $children $root
    initEvents(vm) // 初始化组件传入的方法 @xxx="xxx"
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm) // 初始化data,props,watch,computed,methods
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

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

initInternalComponent

如果是组件(options && options._isComponent)就会进入该函数
initInternalComponent函数的目的是创建 o p t i o n s 并 赋 值 父 级 传 过 来 的 数 据 。 把 ‘ p r o p s ‘ 、 ‘ l i s t e n e r s ‘ 等 内 容 存 到 ‘ v m . options并赋值父级传过来的数据。把`props`、`listeners`等内容存到`vm. optionspropslistenersvm.options上,此时你在子组件中this.$options`就可以看到了~

详见代码:

// 创建$options并赋值父级传过来的数据
function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  const parentVnode = options._parentVnode
  ...
  opts._parentListeners = vnodeComponentOptions.listeners // 组件监听器
  ...
}

核心 initEvents

initEvents方法是初始化事件的入口函数,在该方法中首先初始化_events对象,该对象是用来存储事件的,然后通过上一个方法里创建的$options上是否有_parentListeners来判断该组件是否绑定了事件,是否继续执行初始化事件方法

export function initEvents (vm: Component) {
  vm._events = Object.create(null) // 初始化vm._events
  vm._hasHookEvent = false // hook-event标记
  // init parent attached events
  const listeners = vm.$options._parentListeners // 所有父级传入的事件
  if (listeners) {
    // 处理组件标签上所带的事件
    updateComponentListeners(vm, listeners)
  }
}

updateComponentListeners

该函数内部其实只是临时存储了下当前组件并调用updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)函数,我们需要注意的是在调用时传的几个参数

  1. listeners:事件对象
  2. oldListeners:更新时传的旧事件对象
  3. add 调用vm.$on:向上边创建的事件对象vm._events中添加事件的函数
function add (event, fn) {
    target.$on(event, fn)
}
  1. remove调用vm.$off:对应vm.$on移除事件的函数
function remove (event, fn) {
    target.$off(event, fn)
}
  1. createOnceHandler:处理带有.once修饰符的函数(调用一次后就执行$off)
function createOnceHandler (event, fn) {
    const _target = target
    return function onceHandler () {
        const res = fn.apply(null, arguments)
        if (res !== null) {
          _target.$off(event, onceHandler)
        }
    }
}
  1. vm:vm…

updateComponentListeners函数源码

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

updateListeners

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) // 处理修饰符
    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)
    } 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)
    }
  }
}

updateListeners函数的代码可以分为两个for in循环,并且做了三件事

  1. 处理修饰符(第一个循环)
  2. 创建/更新函数调用器(第一个循环)
  3. 组件更新时删除无用事件(第二个循环)

1. 处理修饰符

...
event = normalizeEvent(name) // 处理修饰符
...
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
    }
})

normalizeEvent函数主要是用来处理修饰符,比如once的修饰符在编译后中对应的就是“~”(@xxx.once="xxx"),具体修饰符对应的前缀可以看官方文档中的修饰符介绍

执行完毕后会返回一个名叫event的对象,包括name和修饰符对应的布尔值

2. 创建/更新函数调用器

...
if (isUndef(cur)) {
    报个错.log
} 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)
} else if (cur !== old) { // 更新函数调用器
    old.fns = cur
    on[name] = old
}

这段代码可以粗略的分为组件创建时和组件更新时两个不同的执行时间解释

创建时
以文章最开头的例子来说
我们来看for in循环的nameaaaFun的循环内,在组件创建时肯定没有老函数(oldOn.aaaFun),所以会进入else if (isUndef(old)),此时cur的值为aaaFun(){console.log('函数')}会进入if (isUndef(cur.fns))去创建函数调用器

创建函数调用器 createFnInvoker

createFnInvoker函数会返回一个调用器函数,这个调用器函数内部会判断是否是多个函数(fnsArray类型)并调用invokeWithErrorHandling方法执行该函数
invokeWithErrorHandling函数就是一个通过apply|call调用并抛出错误的通用函数,文章最底下会简略说明该函数)

/**
 * 创建函数调用器
 */
export function createFnInvoker (fns: Function | Array<Function>, vm: ?Component): Function {
    function invoker () {
        const fns = invoker.fns
        if (Array.isArray(fns)) {
            const cloned = fns.slice()
            for (let i = 0; i < cloned.length; i++) {
                invokeWithErrorHandling(cloned[i], null, arguments, vm, `v-on handler`)
            }
        } else {
            // return handler return value for single handlers
            return invokeWithErrorHandling(fns, null, arguments, vm, `v-on handler`)
        }
    }
    invoker.fns = fns
    return invoker
}

然后if (isTrue(event.once))判断once去调用上一个函数传进来的createOnceHandler(上边已经说过该函数,在这里就跳过了),最后再调用上个函数传进来的add来将函数添加到_event中,我们的目的就达到了

更新时

更新时的逻辑很简单,进入这个函数时,cur的值都是父组件传进来的函数,所以只需要将老的函数的调用器中存储的函数(old.fns)换成当前的cur就可以了

old.fns = cur
on[name] = old

3. 组件更新时删除无用事件

组件更新时,如果有新传入的事件会和创建组件时走相同的逻辑,有变化的事件会走上边说的更新函数调用器,最后剩下无用的事件,在这里调用上个函数传入的remove将他删除掉

for (name in oldOn) {
    if (isUndef(on[name])) {
        event = normalizeEvent(name)
        remove(event.name, oldOn[name], event.capture)
    }
}

附加:invokeWithErrorHandling函数

invokeWithErrorHandling函数就是一个通过apply|call调用并抛出错误的通用函数,他的传参分别是【调用的函数,this,传参[Array],vm,报错信息】

export function invokeWithErrorHandling (
    handler: Function, // function
    context: any, // this
    args: null | any[], // null|any
    vm: any, // vm
    info: string // info
) {
    let res
    try {
        res = args ? handler.apply(context, args) : handler.call(context) // 调用函数
        if (res && !res._isVue && isPromise(res) && !res._handled) {
            res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
            res._handled = true
        }
    } catch (e) {
        handleError(e, vm, info)
    }
    return res
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值