前言
当提及到v-model
时,我们都会想到它时一个语法糖,相当于在使用该指令的元素或者组件上绑定了props
中的value
值以及注册了input
事件。但其实深入挖掘其中,可以学习到更多的东西,下面就看一下相关的Vue
源码吧。
从编译出发
以下面的例子出发分析:
new Vue({
el:'#app',
template:'<input v-model="value">'
})
在初始化时,Vue
构造函数会执行this._init
,然后this._init
内部通过vm.$mount()
调用Vue.prototype.$mount
的方法。Vue.prototype.$mount
的方法会因Vue
构建版本而不同。
Vue
构建版本分完整版
和运行时的版本
,完整版
包括编译器
和运行时的版本
。如果需要在代码编译模板 (比如传入一个字符串给 template
选项,或挂载到一个元素上并以其 DOM
内部的 HTML
作为模板),就将需要加上编译器,即完整版:
// 需要编译器
new Vue({
template: '<div>{{ hi }}</div>'
})
// 不需要编译器
new Vue({
render (h) {
return h('div', this.hi)
}
})
对于我们现在例子的情况,需要用到完整版
的Vue.prototype.$mount
(定义在src\platforms\web\entry-runtime-with-compiler.js
中)来作分析,当render
未定义但template
已定义时,会通过compileToFunction
解析template
以获取render
,其内部会调用baseCompile
作为主要的解析函数,如下所示:
function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 1.把模板转换成ast抽象语法树
// 抽象语法树,用来以树形的方式描述代码结构
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 2.优化抽象语法树
optimize(ast, options)
}
// 3.把抽象语法树生成字符串形式的js代码
const code = generate(ast, options)
return {
ast,
// 渲染函数
render: code.render,
// 静态渲染函数,生成静态VNode树
staticRenderFns: code.staticRenderFns
}
}
至此,我们可以知道编译的三个步骤:
- parse : 解析模板字符串生成
AST
语法树 - optimize : 优化语法树,主要时标记静态节点,提高更新页面的性能
- codegen : 生成js代码,主要是
render
函数和staticRenderFns
函数
我们接着开头的例子去代入到这些步骤中:
parse
parse
过程中,会对模板使用大量的正则表达式去进行解析。开头的例子会被解析成以下AST节点:
ast={
'type': 1, // type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本
'tag': 'input',
'directives': [
{
'name': 'model',
'value': 'value',
'modifier': undefined
}
],
attrsMap:{
'v-model':'value'
},
// ...还有很多属性省略...
}
optimize
过程在此不做分析,因为本例子没有静态节点
codegen
首先,查看codegen
过程中const code = generate(ast, options)
中,generate
源码如下所示:
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
/** CodegenState构造函数是根据options生成一个处理AST语法树的代码生成器
* CodegenState类的构造函数中有以下逻辑
* class CodegenState{
constructor(){
this.directives = extend(extend({}, baseDirectives), options.directives)
}
* }
* 其中baseDirectives用于处理v-on,v-bind,v-cloak指令
* options为编译相关的配置,不同的平台下option不同,
* 在web环境下,option会来自src\platforms\web\compiler\options.js,
* 而option['directives']则引用自src\platforms\web\compiler\directives\index.js
* 里面包括对v-model,v-text,v-html指令的处理
*/
const state = new CodegenState(options)
/**
*
*/
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
genElement
方法中有大量处理当前AST节点属性的逻辑,根据节点属性生成各种生成函数,在我们的例子中,只需要执行里面的genData
,而genData
中针对el.directives
会调用genDirectives
处理。genDirectives
源码如下所示:
function genDirectives (el: ASTElement, state: CodegenState): string | void {
const dirs = el.directives
if (!dirs) return
let res = 'directives:['
let hasRuntime = false
let i, l, dir, needRuntime
// 遍历el.directives
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i]
needRuntime = true
// 调用CodegenState实例的directives对应的指令处理函数进行处理
const gen: DirectiveFunction = state.directives[dir.name]
if (gen) {
needRuntime = !!gen(el, dir, state.warn)
}
if (needRuntime) {
hasRuntime = true
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${
dir.arg ? `,arg:"${dir.arg}"` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
}
if (hasRuntime) {
return res.slice(0, -1) + ']'
}
}
现在可以知道,处理v-model
是CodegenState
实例中的directives['model']
,接下来我们看一下其相关代码:
rc\platforms\web\compiler\directives\model.js
export default function model (
el: ASTElement,
dir: ASTDirective,
_warn: Function
): ?boolean {
warn = _warn
const value = dir.value
const modifiers = dir.modifiers
const tag = el.tag
const type = el.attrsMap.type
// 以分情况来处理v-model
// 1. 如果节点是组件实例
if (el.component) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
// 2. 如果节点的html标签为select
} else if (tag === 'select') {
genSelect(el, value, modifiers)
// 3. 如果节点的html标签为input且type属性为'checkbox'
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers)
// 4. 如果节点的html标签为input且type属性为'radio'
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers)
// 5. 如果节点的html标签为input且type属性为'textarea'
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers)
// 6. 检查标记是否被保留,以便它不能被注册为组件。这是依赖于平台的,在web端此值恒为false
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
}
// ensure runtime directive metadata
return true
}
继续以我们的例子做分析,此时执行genDefaultModel
函数,该函数源码如下:
function genDefaultModel (
el: ASTElement,
value: string,
modifiers: ?ASTModifiers
): ?boolean {
const type = el.attrsMap.type
const { lazy, number, trim } = modifiers || {}
const needCompositionGuard = !lazy && type !== 'range'
// 如果v-model.lazy则选择监听change事件,
// input事件的触发机制是,当输入框内容发生变化时
// change事件的触发机制是,当输入框失去焦点后且输入框内容发生变化时
const event = lazy
? 'change'
: type === 'range'
? RANGE_TOKEN
: 'input'
// valueExpression是用于event事件触发后,把valueExpression赋值到v-model绑定的值上,也就是value
let valueExpression = '$event.target.value'
// 如果v-model.trim,则事件触发把event.target.value赋值前调用trim()
if (trim) {
valueExpression = `$event.target.value.trim()`
}
// 如果v-model.number,则事件触发把event.target.value赋值前先调用parseFloat转换
// target._n = toNumber;
// function toNumber (val) {
// var n = parseFloat(val);
// return isNaN(n) ? val : n
// }
if (number) {
valueExpression = `_n(${valueExpression})`
}
/**
* genAssignmentCode (value: string,assignment: string): string {
const res = parseModel(value)
// 如果v-model="value"
if (res.key === null) {
return `${value}=${assignment}`
// 如果v-model="value",即绑定的是一个对象中的属性
} else {
return `$set(${res.exp}, ${res.key}, ${assignment})`
}
}
*/
let code = genAssignmentCode(value, valueExpression)
if (needCompositionGuard) {
code = `if($event.target.composing)return;${code}`
}
// 添加到prop中
addProp(el, 'value', `(${value})`)
// 添加到on事件中
addHandler(el, event, code, null, true)
// 以上两个处理过程相当于把节点处理为<input :value="value" @input="value=$event.target.value">
if (trim || number) {
addHandler(el, 'blur', '$forceUpdate()')
}
}
以上针对lazy
,number
,trim
修饰符的处理在其余五种情况下也几乎一样。
genDirectives
在model
函数执行完后,接下来的操作会生成directives
数据。directives
数据将会作为生成VNode
需要的参数传入到render
函数内部调用的createElement
函数中。
直至codegen
过程结束,生成的render
代码如下所示:
function render() {
with(this) {
return _c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (value),
expression: "value"
}],
domProps: {
"value": (value)
},
on: {
"input": function ($event) {
if ($event.target.composing) return;
value = $event.target.value
}
}
})
}
}
model函数中的多种情况
这里我直接说一下其他情况下,v-bind
和v-on
会对应哪些值和事件,这些情况对应的函数的源码我就不做分析了,基本和genDefaultModel
都大同小异。
1. 如果节点是组件实例
此时,v-model
会转换成绑定(v-bind
)value
值和监听(v-on
)input
事件。另外,如果在组件里定义了model
属性,则会绑定和监听其中设置的值,例如:
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
// ...省略
`
})
此时该组件以<base-checkbox v-model="lovingVue"></base-checkbox>
的方式调用时,v-model
会转换成绑定checked
值和监听change
事件。
2. 如果节点为普通元素
节点类型 | 绑定值(v-bind) | 监听事件(v-on) |
---|---|---|
input[type="radio"] | checked | change |
select | value | change |
input[type="checkbox"] | checked | change |
input | value | input |
textarea | value | input |
后记
支支吾吾想了很久,后记还是写 "希望大家多多支持" 吧。