# 事件机制
# 模板中的 v-on/@
一般情况下,通过在模板中使用 v-on/@
监听事件,并在触发时运行一些 JavaScript 代码。
那么模板中的 v-on/@
会被怎样处理,以实现事件监听呢?
<div @click="handleClick"></div>
模板的编译时,除标签以外都会被认为是属性,所以在 start
钩子中创建的 AST 节点对象如下:
{
tag: 'div',
attrs: {
'@click': 'handleClick'
}
}
# 维护事件列表
而在 end
钩子中,就会对所有属性进行处理:
// el 是 AST 节点对象
// name 是事件名
// value 是方法名
const onRe = /^@|^v-on:/
if (onRe.test(name)) {
const events = el.events || (el.events = {})
name = name.replace(onRe, '')
events[name] = (events[name] || []).concat(value)
}
所以得到的 AST 节点如下:
{
tag: 'div',
events: {
click: ['handleClick']
}
}
# 修饰符
Vue
提供修饰符来简化事件的使用,那么如何实现修饰符的功能?
<div @click.stop="handleClick"></div>
解析修饰符:
const modifierRe = /\.[^.]+/g
function parseModifier(name) {
const match = name.match(modifierRe)
if (match) {
const ret = {}
match.forEach(m => {
ret[m.slice(1)] = true
})
return ret
}
}
所以维护事件列表时的代码就需要修改:
// el 是 AST 节点对象
// name 是事件名
// value 是方法名
const onRe = /^@|^v-on:/
const modifierRe = /\.[^.]+/g
const modifiers = parseModifier(name)
// 新增代码
if (modifiers) {
name = name.replace(modifierRe)
}
if (onRe.test(name)) {
name = name.replace(onRe, '')
// 更新代码
let events = el.events || (el.events = {})
// 新增代码
const handler = {
value: value.trim(),
modifiers: modifiers
}
events[name] = (events[name] || []).concat(handler)
}
所以得到的 AST 节点如下:
{
tag: 'div',
events: {
click: {
value: 'handleClick',
modifiers: {
stop: true
}
}
}
}
# 原生 DOM 事件
注:这一部分在
Vue
中实际是通过代码字符串的方式拼接,最后通过new Function()
等方式动态执行的。这里只做分析所以没有必要这么做。
通过模板编译,把模板中的 v-on/@
解析成了 JavaScript 对象,就下来就要在代码生成中嵌入当前平台上的事件方法(以监听事件为例):
// el 表示元素
for (const event in events) {
const handler = events[event]
el.addEventListener(event, handler.value)
}
但是还需要对修饰符进行处理,而且在 Vue
中事件的值是支持表达式的,如:
<div @click="visible = false"></div>
所以需要进行一些特殊处理:
for (const event in events) {
const handler = events[event]
el.addEventListener(event, function(event) {
if (handler.modifiers.stop) {
event.stopPropagation()
}
if (handler.modifiers.prevent) {
event.preventDefault()
}
// ...省略
// 判断是否是函数,此处简化了
if (/[a-zA-Z0-9_$]/.test(handler.value)) {
handler.value(event)
} else {
new Function(handle.value) // 此处只是举个例子,表示执行该 js 语句,如 a = 1 这样的语句
}
})
}
对于修饰符这一部分,有些修饰符并不是这么处理的, 如 capture
,因为 capture
在 web 平台下是 addEventListener()
的参数,对于这些修饰符,实际上在模板编译时会对事件名进行特殊的处理。以 capture
为例:
const onRe = /^@|^v-on:/
const modifierRe = /\.[^.]+/g
const modifiers = parseModifier(name)
if (modifiers) {
name = name.replace(modifierRe)
}
if (onRe.test(name)) {
name = name.replace(onRe, '')
// 新增代码
if (modifiers.capture) {
name = `!` + name
}
let events = el.events || (el.events = {})
const handler = {
value: value.trim(),
modifiers: modifiers
}
events[name] = (events[name] || []).concat(handler)
}
对应在代码生成时:
for (const event in events) {
const handler = events[event]
// 新增代码
let params = {}
if (event[0] = '!') {
params.capture = true
event = event.slice(1)
}
el.addEventListener(event, function(event) {
if (handler.modifiers.stop) {
event.stopPropagation()
}
if (handler.modifiers.prevent) {
event.preventDefault()
}
if (/[a-zA-Z0-9_$]/.test(handler.value)) {
handler.value(event)
} else {
new Function(handle.value) // 此处只是举个例子,表示执行该 js 语句,如 a = 1 这样的语句
}
}, params) // 新增代码
}
# 组件的事件
但是目前的方式都只适用于浏览器中的 DOM 标签元素,并不适用于 Vue
组件,所以需要对组件的事件特殊处理。
# 发布订阅模式
组件的事件机制非常简单,就是发布订阅模式:
Vue.prototype.$emit = function(event, ...args) {
const vm = this
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? cbs.slice(0) : cbs
cbs.forEach(cb => cb.apply(vm, args))
}
}
Vue.prototype.$on = function(event, handler) {
const vm = this
if (vm._events) {
vm._events = Object.create(null)
}
(vm._events[event] || vm._events[event] = []).push(handler)
}
Vue.prototype.$off = function(event, handler) {
const vm = this
// 移除所有事件
if (!arguments.length) {
vm._events = Object.create(null)
}
if (!handler) {
vm._events[event] = []
}
const cbs = vm._events[event]
const i = cbs.indexOf(handler)
cbs.splice(i, 1)
}
# 父、子组件间的处理
但是这样的机制只能运行在当前组件实例自身范围内,举个例子:
const vm = new Vue({
template: `
<div>
<comp-name @eventname="handleEvent"></comp-name>
</div>
`,
methods: {
handleEvent() {
// ...
}
}
})
对于这个组件实例来说:
- 子组件
comp-name
会emit
事件eventname
- 当前组件提供了处理函数
handleEvent
也就是或,事件的处理函数和事件的触发是在不同组件实例上,在阅读源码之前,我以为需要去父组件实例中寻找该方法,但实际上并不,那么该怎么做呢?
处理函数的作用域在初始化时,已经绑定在该组件了。所以问题的关键只有如何寻找到这个处理函数!
对于例子中的模板,最终 render()
函数返回的 VNode
树大致如下:
{
tag: 'div',
children: [
{
tag: 'vue-component-2-comp-name',
listeners: {
eventname: function handleEvent() {
// ...
}
}
}
]
}
注意 listeners
中事件的处理函数,并不是事件名,而是一个函数!因为 render()
函数中通过 with
绑定了作用域,所以在直接通过变量名就可以找到该函数。为了方便理解,我们直接把 render
绑定到组件的构造函数上(实际上并不如此,另外实际上需要通过代码字符串动态执行,此处这么做都是为了便于理解):
VueComponent.prototype.render = function () {
// 理解为 render 函数的作用域
with(vm) {
return createElement(
'div',
[
createElement('SubComp', {
on: {
"test": handleClick // 不是字符串,是变量名,这很关键
},
nativeOn: {
"click": function ($event) {
return handleClick($event)
}
}
})
]
)
}
}
所以当在子组件中处理该事件时,可以直接使用该函数,而并不需要去父组件中寻找了:
const vm = vnode.componentInstance
// 子组件的事件,可以在子组件实例的 `listeners` 属性上获取到
const listeners = vnode.listeners
for (const event in listeners) {
vm.$on(event, listeners[event])
}
整个过程非常简单!
← 异步更新队列 filter 的本质 →