开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情
模版编译
Vue 实例在挂载之前,有相当多的工作时进行模版编译,将 template 进行编译,解析成 AST 树,在转换成 render 函数,有了 render 函数之后才会进入实例挂载流程。对于事件而言,我们经常会使用 v-on 或者 @ 在模版上绑定事件,所以对于事件的第一步处理,就是在编译阶段对事件指令进行收集
指令收集
先来看看 Vue 在模版中绑定事件的简单用法
<div id="app">
<div v-on:click.stop="doThis">点击</div>
<span>{{count}}</span>
</div>
var vm = new Vue({
el: '#app',
data() {
return {
count: 1
}
},
methods: {
doThis() {
++this.count
}
}
})
针对上面的例子,来看下模版编译的基本过程
const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
在实例挂载中,已经对上面的代码进行了分析,而模版编译的过程就发生在 parse(template.trim(), options) , parse 方法的实现比较复杂,里面分支众多,我们现在主要关注与事件有关的 processAttrs 方法。该方法会对 html 元素中的属性进行解析,其中就包括了 v-on 属性
var onRE = /^@|^v-on:/;
var dirRE = /^v-|^@|^:/;
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
value = list[i].value
if (dirRE.test(name)) {
// mark element as dynamic
el.hasBindings = true
// modifiers
modifiers = parseModifiers(name.replace(dirRE, ''))
// support .foo shorthand syntax for the .prop modifier
if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
(modifiers || (modifiers = {})).prop = true
name = `.` + name.slice(1).replace(modifierRE, '')
} else if (modifiers) {
name = name.replace(modifierRE, '')
}
if (bindRE.test(name)) { // v-bind
// v-bind 指令, 后面具体分析
} else if (onRE.test(name)) { // v-on
name = name.replace(onRE, '')
isDynamic = dynamicArgRE.test(name)
if (isDynamic) {
name = name.slice(1, -1)
}
addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
} else { // normal directives
}
} else {
}
}
}
processAttrs 的逻辑较多,但相对容易理解,通过正则的方式, 拿到事件的类型、事件修饰符,并从属性列表中拿到事件回调。最终通过 addHandler 方法, 为 AST 树添加事件相关的属性,
function addHandler (
el: ASTElement,
name: string,
value: string,
modifiers: ?ASTModifiers,
important?: boolean,
warn?: ?Function,
range?: Range,
dynamic?: boolean
) {
modifiers = modifiers || emptyObject
// warn prevent and passive modifier
/* istanbul ignore if */
if (
process.env.NODE_ENV !== 'production' && warn &&
modifiers.prevent && modifiers.passive
) {
warn(
'passive and prevent can\'t be used together. ' +
'Passive handler can\'t prevent default event.',
range
)
}
// normalize click.right and click.middle since they don't actually fire
// this is technically browser-specific, but at least for now browsers are
// the only target envs that have right/middle clicks.
// 对特殊的事件修饰符进行拼接
if (modifiers.right) {
if (dynamic) {
name = `(${name})==='click'?'contextmenu':(${name})`
} else if (name === 'click') {
name = 'contextmenu'
delete modifiers.right
}
} else if (modifiers.middle) {
if (dynamic) {
name = `(${name})==='click'?'mouseup':(${name})`
} else if (name === 'click') {
name = 'mouseup'
}
}
// check capture modifier
if (modifiers.capture) {
delete modifiers.capture
name = prependModifierMarker('!', name, dynamic)
}
if (modifiers.once) {
delete modifiers.once
name = prependModifierMarker('~', name, dynamic)
}
/* istanbul ignore if */
if (modifiers.passive) {
delete modifiers.passive
name = prependModifierMarker('&', name, dynamic)
}
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]
/* istanbul ignore if */
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
}
最终在 AST 中事件的表现形式如下
代码生成
模版编译的最后一步是根据解析得到的 AST 树生成对应平台的渲染函数,也就是 render 函数,源码中调用的 generate 函数
const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
// 生成 render 函数
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
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
}
}
generate 函数的核心处理逻辑在于 genElement 中, genElement 函数会根据不同指令类型处理不同的分支,对于普通模版的编译会进入 genData 函数中处理,我们暂时先只关注针对事件的处理逻辑,在生成 AST 树时, AST 树中多了 events 属性,因此 getHandlers 会对 events 属性进行处理
function genData (el: ASTElement, state: CodegenState): string {
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false)},`
}
return data
}
getHandlers 会遍历解析好的 AST 树,拿到 events 对象属性,并根据属性上的属性对象拼接成字符串
function genHandlers (
events: ASTElementHandlers,
isNative: boolean
): string {
const prefix = isNative ? 'nativeOn:' : 'on:'
let staticHandlers = ``
let dynamicHandlers = ``
for (const name in events) {
// 遍历 AST 树上的 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
}
}
function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {
if (!handler) {
return 'function(){}'
}
// 事件绑定可以有多个,多个事件会在 AST 中以数组的形式存在,这里进行递归处理
if (Array.isArray(handler)) {
return `[${handler.map(handler => genHandler(handler)).join(',')}]`
}
// 事件的书写方式正则匹配
const isMethodPath = simplePathRE.test(handler.value) // doThis 事件只想 methods 中的8属性
const isFunctionExpression = fnExpRE.test(handler.value) // ()=>{} o 或者 function(){}
const isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')) // doThis($event)
// 没有修饰符的情况下
if (!handler.modifiers) {
// 判断是否符合函数定义的规范,如果符合,则直接返回函数名
if (isMethodPath || isFunctionExpression) {
return handler.value
}
/* istanbul ignore if */
if (__WEEX__ && handler.params) {
return genWeexHandler(handler.params, handler.value)
}
// 不符合函数规范时,通过函数封装的方式返回
return `function($event){${
isFunctionInvocation ? `return ${handler.value}` : handler.value
}}` // inline statement
}else {
// 包含修饰符的场景
}
}
上面代码中,三个正则匹配分别对应模版中事件的三种写法
-
<div @click="test"></div
-
<div @click="function(){}"></div><div @click="()=>{}"></div>
-
<div @click="test($event)"></div>
上述代码中,如果事件不带任何修饰符,并且满足正确的模版写法,则直接返回调用的事件名,如果不满足,则可能是 <div @click="console.log(11)"></div> , 写法,此时会封装到一个函数中。
对于包含修饰符的事件绑定,会通过事件修饰符获取到对应需要执行的脚本字符串,并添加到函数字符串中返回
let code = ''
let genModifierCode = ''
const keys = []
// 遍历 modifiers 上的修饰符
for (const key in handler.modifiers) {
if (modifierCode[key]) {
// 通过修饰符拿到需要执行的脚本字符串
genModifierCode += modifierCode[key]
// left/right
if (keyCodes[key]) {
keys.push(key)
}
} else if (key === 'exact') {
// 针对 exact 的特殊处理
const modifiers: ASTModifiers = (handler.modifiers: any)
genModifierCode += genGuard(
['ctrl', 'shift', 'alt', 'meta']
.filter(keyModifier => !modifiers[keyModifier])
.map(keyModifier => `$event.${keyModifier}Key`)
.join('||')
)
} else {
keys.push(key)
}
}
if (keys.length) {
code += genKeyFilter(keys)
}
// Make sure modifiers like prevent and stop get executed after key filtering
// 通过字符串拼接的方式,拼接需要执行的脚本字符串
if (genModifierCode) {
code += genModifierCode
}
// 根据三种不同的事件书写方式返回不同的字符串
const handlerCode = isMethodPath
? `return ${handler.value}($event)`
: isFunctionExpression
? `return (${handler.value})($event)`
: isFunctionInvocation
? `return ${handler.value}`
: handler.value
/* istanbul ignore if */
if (__WEEX__ && handler.params) {
return genWeexHandler(handler.params, code + handlerCode)
}
return `function($event){${code}${handlerCode}}`
const modifierCode: { [key: string]: string } = {
stop: '$event.stopPropagation();',
prevent: '$event.preventDefault();',
self: genGuard(`$event.target !== $event.currentTarget`),
ctrl: genGuard(`!$event.ctrlKey`),
shift: genGuard(`!$event.shiftKey`),
alt: genGuard(`!$event.altKey`),
meta: genGuard(`!$event.metaKey`),
left: genGuard(`'button' in $event && $event.button !== 0`),
middle: genGuard(`'button' in $event && $event.button !== 1`),
right: genGuard(`'button' in $event && $event.button !== 2`)
}
在经过这一步骤之后,与事件相关的代码生成就分析完了