我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
今天讲讲 v-on 指令的底层实现原理。在 Vue 中,v-on 指令有两种用法,第一种是将 v-on 指令使用在自定义组件上,例如:<my-component v-on:myEvent="doSomething"></my-component>,使用 v-on 指令监听了组件的 myEvent 事件,回调函数是 doSomething,当在组件中执行 this.$emit('myEvent') 时,会触发执行 doSomething 函数,有没有发现这和我上一篇文章中的 vm.$on 和 vm.$emit 挺像的,没错,他们的底层实现原理的确很相似。第二种用法是将 v-on 指令用在原生的元素上,可以实现在 DOM 元素上绑定原生的事件,例如 <div v-on:click="doSomething"></div>,当点击该 div 元素时,会触发执行 doSomething 回调函数。接下来,对这两种用法分别进行解析。
1,v-on 在自定义组件上使用
看这一小节前,最好先看看我的上一篇文章。
我们以下面的代码为例。
Vue.component('component-a', {
template: '<h1>我是组件</h1>'
})
new Vue({
el: '#app',
data() {
return {
}
},
methods: {
nameChangeHandler(){
console.log("name change handler")
}
},
template: `
<div id="app">
<component-a @nameChange="nameChangeHandler"></component-a>
</div>
`
})
1-1,模板字符串 >>> vnode
生成的 vnode 如下所示,children 数组中的第一个 vnode 元素就是使用的 component-a 组件对应的 vnode,该 vnode 的 componentOptions.listeners 属性对象保存着在父节点中给当前组件绑定的事件和回调函数。
在 patch() 方法进行页面的渲染时,如果发现处理的 vnode 节点是组件节点的话,则会进入创建组件的逻辑,每个组件都有一个 Vue 实例与之对应。
1-2,createComponentInstanceForVnode
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
parentElm?: ?Node,
refElm?: ?Node
): Component {
const vnodeComponentOptions = vnode.componentOptions
// options 是创建组件 vue 实例的参数对象
const options: InternalComponentOptions = {
_isComponent: true,
parent,
propsData: vnodeComponentOptions.propsData,
_componentTag: vnodeComponentOptions.tag,
_parentVnode: vnode,
// 将 vnode.componentOptions.listeners 赋值到 options._parentListeners
_parentListeners: vnodeComponentOptions.listeners,
_renderChildren: vnodeComponentOptions.children,
_parentElm: parentElm || null,
_refElm: refElm || null
}
// 创建组件对应的 vue 实例,并且以 options 为参数
return new vnodeComponentOptions.Ctor(options)
}
1-3,Vue.prototype._init
上面调用的 new vnodeComponentOptions.Ctor(options) 就是下面的 VueComponent 函数,该函数的内部会执行 _init 函数。
const Sub = function VueComponent (options) {
this._init(options)
}
_init 函数中简要代码如下:
Vue.prototype._init = function (options?: Object) {
// vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
const vm: Component = this
initInternalComponent(vm, options)
// 初始化与事件有关的属性以及处理父组件绑定到当前组件的方法。
initEvents(vm)
}
function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// 将 options._parentListeners 保存到 vm.$options._parentListeners
opts._parentListeners = options._parentListeners
}
_init 函数内部首先执行 initInternalComponent(vm, options) 函数,该函数的作用是将 options 中的众多属性保存到 vm.$options 中,其中就包括 _parentListeners 属性,initInternalComponent 函数执行完成后,会执行 initEvents(vm),对在父组件绑定到子组件上的事件和响应函数进行处理。
1-4,initEvents 函数
export function initEvents (vm: Component) {
// 初始化 vue 实例中的 _events 属性,该属性用于保存:
// 1,vm.$on() 绑定的事件和响应函数。
// 2,父组件在子组件上绑定的事件和响应函数。
vm._events = Object.create(null)
vm._hasHookEvent = false
// 获取父组件在子组件上绑定的事件集合对象,{ eventName1: callback1, ...... }
const listeners = vm.$options._parentListeners
// 如果父组件的确在子组件上绑定了事件的话,执行 updateComponentListeners 函数
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
1-5,updateComponentListeners 函数
在 updateComponentListeners 函数中,会调用 updateListeners 函数,这里的 add 和 remove 方法通过调用上一篇博客中的 $once、$on、$off 完成功能。也就是说,在组件上使用 v-on 指令绑定事件及回调函数的底层和 vm.$on 函数一样,都是将事件和回调函数存储到 vm._events 中,且都可以在子组件中通过 this.$emit("eventName") 抛出对应的事件,进而触发执行对应的回调函数。
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, vm)
}
// 添加事件,借助 $once 和 $on 完成功能
function add (event, fn, once) {
if (once) {
target.$once(event, fn)
} else {
target.$on(event, fn)
}
}
// 移除事件,借助 $off 完成功能
function remove (event, fn) {
target.$off(event, fn)
}
1-6,updateListeners 函数
该函数的解释都在注释中,看注释即可。
// 对比 on 与 oldOn,然后根据对比的结果调用 add 方法或者 remove 方法执行绑定或解绑事件
// 该函数的一大特点是:add 和 remove 函数与 updateListeners 函数解耦,它们作为参数传递到
// updateListeners 方法中,updateListeners 方法主要做 on 与 oldOn 的比较。
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
vm: Component
) {
let name, cur, old, event
// 遍历处理 on 中的事件
for (name in on) {
// 根据事件名称(例如:click)获取对应的回调函数
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 回调函数未定义,cur 回调函数定义了的话,说明当前的事件是新增的
// 需要执行 add 方法进行事件的绑定
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur)
}
add(event.name, cur, event.once, event.capture, event.passive)
} else if (cur !== old) {
// 如果 cur 和 old 都定义了,并且 cur !== old 的话,则执行到这里
//
// 执行到这里的情形是:之前和现在 DOM 元素都绑定了 name 事件,但是绑定的回调函数不一样,
// 所以需要对执行的回调函数进行更新。更新的方式也很简单,将 cur 赋值到 old.fns 即可,
// 至于为什么这样就能改变绑定的回调函数,看 createFnInvoker 函数的源码注释
old.fns = cur
on[name] = old
}
}
// 遍历处理 oldOn 中的事件
for (name in oldOn) {
// 如果当前遍历的事件在 on 中不存在的话
// 说明该事件以前绑定了,而最新的状态没有绑定,此时需要执行 remove 进行该事件的解绑操作
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
这里,还需要特别注意的一点是:上述的处理都是在初始化组件 Vue 实例的阶段做的事。
2,v-on 在 DOM 元素上使用
在讲这部分内容之前,建议先看看我的这篇博客 — Vue源码阅读(19):自定义指令的源码解析 中的第一小节知识补充部分。
在 patch 的过程中,除了更新 DOM 元素中的内容外,还会更新 DOM 很多其他的东西,例如:directives、ref、attrs、class、events、style 等,更新 events 的代码定义在 src/platforms/web/runtime/modules/events.js 文件中,我们直接看源码。
/* @flow */
import { isDef, isUndef } from 'shared/util'
import { updateListeners } from 'core/vdom/helpers/index'
import { withMacroTask, isIE, supportsPassive } from 'core/util/index'
import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model'
let target: HTMLElement
// 实现事件修饰符 .once 的方法
function createOnceHandler (handler, event, capture) {
const _target = target // save current target element in closure
// 返回一个包装函数,当事件触发的时候,执行的就是这个返回的包装函数
// 该包装函数执行时,内部会触发执行真正的回调函数,回调函数执行一次后,
// 后续元素就不需要再绑定 event 事件了,所以执行 remove 方法解绑 event 事件
return function onceHandler () {
const res = handler.apply(null, arguments)
if (res !== null) {
// res !== null 的时候,才会执行 remove 方法,这主要是为了解决一个 bug,
// issues 看这里:https://github.com/vuejs/vue/issues/4846
remove(event, onceHandler, capture, _target)
}
}
}
// 绑定事件
function add (
event: string,
handler: Function,
once: boolean,
capture: boolean,
passive: boolean
) {
handler = withMacroTask(handler)
// 如果事件绑定使用了 .once 的话,则给 handler 加一层包装
if (once) handler = createOnceHandler(handler, event, capture)
// 这里只是调用浏览器提供的 API --- node.addEventListener 绑定事件
// target 就是使用了 v-on 指令的 DOM 元素
target.addEventListener(
event,
handler,
supportsPassive
? { capture, passive }
: capture
)
}
// 解绑事件
function remove (
event: string,
handler: Function,
capture: boolean,
_target?: HTMLElement
) {
// 调用浏览器提供的 API --- node.removeEventListener 解绑事件
(_target || target).removeEventListener(
event,
handler._withTask || handler,
capture
)
}
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
// 如果 oldVnode 和 vnode 中都没有事件对象的话,说明之前没有绑定任何事件,现在也没有新增绑定事件
// 因此不需要做事件的绑定和解绑操作,直接 return 即可。
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
// 获取 vnode 和 oldVnode 中的事件对象
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
// vnode.elm 是 vnode 在页面上对应的真实 DOM 节点
target = vnode.elm
normalizeEvents(on)
// 更新元素上的事件
// 内部的机制是:对比 on 与 oldOn,然后根据对比的结果调用 add 方法或者 remove 方法执行绑定或解绑事件
updateListeners(on, oldOn, add, remove, vnode.context)
}
export default {
create: updateDOMListeners,
update: updateDOMListeners
}
该文件的最后导出了一个对象,对象中有两个属性,分别是 create 和 update,这说明当 DOM 元素刚创建和更新的时候都会触发执行后面的 updateDOMListeners,该函数用于更新 DOM 元素的事件绑定。
updateDOMListeners 函数的具体解释看注释即可,该函数的最后也会执行 updateListeners 函数,只不过作为参数的 add 和 remove 是当前文件中独有的,用于绑定和解绑 DOM 元素上的事件。updateListeners 函数的内容看上面的 1-6 小节。
add 函数的最后通过执行原生的 node.addEventListener() 方法绑定事件,而 remove 函数通过执行 node.removeEventListener() 方法解绑事件。而且,当我们绑定事件时,使用了 once 事件修饰符的话,还会进行一层 createOnceHandler 函数的处理,这部分源码的详细解释看注释。
3,结语
以上就是 v-on 指令的底层实现原理,下一篇博客讲 v-model 指令。