![295a6bdbbf8a55a09387128c3b3bc57e.png](https://i-blog.csdnimg.cn/blog_migrate/9089684cabc4e5f6cd4463b7098568af.jpeg)
近期项目开发中遇到的一个bug, 大致代码类似是这样的...
<template>
<div id="app">
<h1>{{message}}</h1>
<button @click="changeFlag">点我更改{{flag}}</button>
<button @click="flag || doSomething">Say hello.</button>
<p v-for="(item, index) in result" :key="index">
{{item}}
</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Welcome to Vue!',
flag: false,
result: []
};
},
methods: {
doSomething() {
this.result.push(`点我触发函数 flag:${this.flag} type: ${typeof(this.flag)}`)
},
changeFlag() {
this.flag = !this.flag
}
}
};
</script>
![9eef894dc1ff89a8e865a21ec9ae8348.png](https://i-blog.csdnimg.cn/blog_migrate/2e46eabc202c97f3dddc588cc9e833b8.png)
问题现象大致就是点击 say hello 按钮始终无响应, doSomething 事件始终无法触发。但是单看代码逻辑好像也没什么问题(当flag值为true时直接截断,当为false时触发doSomething事件)。
初步怀疑可能是转换为字符串导致 Boolean("false")=== true 但是总不能靠凭空猜测吧(此猜想后面证明是错误的!!!)。正好借此机会看一下 Vue源码 事件解析 部分找下原因。
为方便理解 后面展示的代码有做一定的简化&注释
模板解析
/src/compiler/parser/index.js
export const dirRE = /^v-|^@|^:|^#/
const modifierRE = /.[^.]]+(?=[^]]*$)/g
export const bindRE = /^:|^.|^v-bind:/
export const onRE = /^@|^v-on:/
// 解析模板属性 用@click="doSomething"为例
function processAttrs (el) {
const list = el.attrsList // 获取所有属性列表
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name // 这里是事件名称 @click
value = list[i].value // 这里是生命的函数名称 doSomething
if (dirRE.test(name)) { // 匹配v-或者@开头的指令
modifiers = parseModifiers(name.replace(dirRE, '')) // parseModifiers('click')
if (modifiers) { // 清除修饰器
name = name.replace(modifierRE, '')
}
if (bindRE.test(name)) { // v-bind
...
} else if (onRE.test(name)) { // v-on
...
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
} else { // normal directives
...指令相关逻辑
}
} else {
...
}
}
}
事件处理
/src/compiler/helpers.js
export function addHandler (el,name,value,modifiers) {
modifiers = modifiers || emptyObject
if (modifiers.right) {
...鼠标右键事件名称处理
} else if (modifiers.middle) {
...鼠标滚轮事件名称处理
}
// check capture modifier
if (modifiers.capture) {
delete modifiers.capture
name = prependModifierMarker('!', name, dynamic) // 给事件名加'!' 来代表capture修饰符
}
if (modifiers.once) {
delete modifiers.once
name = prependModifierMarker('~', name, dynamic) // 给事件名加'~' 来代表once修饰符
}
if (modifiers.passive) {
delete modifiers.passive
name = prependModifierMarker('&', name, dynamic) // 给事件名称加 '&',来代表passive修饰符
}
let events // 用来记录绑定的事件
if (modifiers.native) {
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
}
// 绑定的事件可以有多个或单个 并处理事件顺序等
const handlers = events[name]
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
代码生成
/src/compiler/codegen/index.js
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
// 根据不同的类型做不同的处理 我们这里只需关注大部分普通模板编译部分
export function genElement (el: ASTElement, state: CodegenState): string {
let data
...
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})` // _c(tagName,data,children) 创建元素vNode方法
...
return data
}
// genData中做了很多的判断处理 我们这里着重关注events和nativeEvents
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
...
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}
...
return data
}
// 遍历解析好的AST树,拿到event对象属性,并根据属性上的事件对象拼接成字符串。
export function genHandlers (
events: ASTElementHandlers,
isNative: boolean
): string {
const prefix = isNative ? 'nativeOn:' : 'on:'
let staticHandlers = ``
let dynamicHandlers = ``
for (const name in events) {
const handlerCode = genHandler(events[name])
if (events[name] && events[name].dynamic) {
dynamicHandlers += `${name},${handlerCode},`
} else {
staticHandlers += `"${name}":${handlerCode},`
}
}
staticHandlers = `{${staticHandlers.slice(0, -1)}}`
if (dynamicHandlers) {
return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0, -1)}])`
} else {
return prefix + staticHandlers
}
}
const fnExpRE = /^([w$_]+|([^)]*?))s*=>|^function(?:s+[w$]+)?s*(/
const fnInvokeRE = /([^)]*?);*$/
const simplePathRE = /^[A-Za-z_$][w$]*(?:.[A-Za-z_$][w$]*|['[^']*?']|["[^"]*?"]|[d+]|[[A-Za-z_$][w$]*])*$/
function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {
if (!handler) {
return 'function(){}'
}
if (Array.isArray(handler)) {
return `[${handler.map(handler => genHandler(handler)).join(',')}]`
}
const isMethodPath = simplePathRE.test(handler.value) // @click="doSomething"
const isFunctionExpression = fnExpRE.test(handler.value) // @click="() => {}" or @click="function(){}"
const isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')) // @click="doSomething($event)"
// 在没有修饰符的情况下
if (!handler.modifiers) {
// 符合函数名称规则 直接返回函数名称
if (isMethodPath || isFunctionExpression) {
return handler.value
}
// 不符合则需要包一层 例如:@click="doSomething()" or @click="num++"
return `function($event){${
isFunctionInvocation ? `return ${handler.value}` : handler.value
}}` // inline statement
} else {
// 有修饰符的情况 eg:会对字符换函数做一下额外逻辑添加
}
}
看完事件解析的源码之后我们再回过头来看看bug中的事件解析流程
- vue通过processAttrs方法将 click 解析为name 同时将 "flag || doSomething" 解析为value
<button @click="flag || doSomething">Say hello.</button>
2. 再通过addHandler方法将click放入到events数组中
3. 再通过genHandler 处理为函数字符串, genHandlers综合处理,genData情况判断,genElement返回模板编译vNode 的 _c 方法 , 最后generate方法生成render函数返回
// 符合函数名称规则 直接返回函数名称
if (isMethodPath || isFunctionExpression) {
return handler.value
}
// 不符合则需要包一层 例如:@click="doSomething()" or @click="num++"
return `function($event){${
isFunctionInvocation ? `return ${handler.value}` : handler.value
}}`
注意这一行 isFunctionInvocation ? `return ${handler.value}` : handler.value 后面部分并没有当做方法执行 因为我们的 "flag || doSomething" 均为字符串
所以结论就出来了 首先事件的解析流程都是将我们的方法处理为方法字符串后进行处理的 并非之前的猜想flag会一直当做字符串来处理 而是方法并没有执行导致 所以, 上面我们这样的写法是存在问题的
<button @click="flag || doSomething">Say hello.</button>
应改成
<button @click="flag || doSomething()">Say hello.</button>
完整的demo在这里
https://codepen.io/jiyangwei/pen/XWKoMROcodepen.io相关文档:
- 初始化阶段(initEvents) | Vue源码系列-Vue中文社区
- 深入剖析Vue源码 - 揭秘Vue的事件机制