前言
v-model即我们常说的双向绑定,但一定不能跟数据响应原理混为一谈,因为数据响应式是通过数据的改变去驱动视图渲染,而双向绑定除了可以数据驱动DOM渲染,DOM的变化反过来也可以影响数据,是一个双向的关系。
案例
v-model作用在DOM元素
举个🌰:
let vm = new Vue({
el: '#app',
template: '<div>'
+ '<input v-model="message" placeholder="edit me">' +
'<p>Message is: {{ message }}</p>' +
'</div>',
data() {
return {
message: ''
}
}
})
当我们在表单中输入文案,那么message也会跟着变化,OK,让我们来一探究竟。
编译阶段
模版首先要被编译,首先是 parse 阶段, v-model 被当做普通的指令解析到 el.directives 中,然后在 codegen 阶段,执行 genData 的时候,会执行
源码: src/compiler/codegen/index.js
/**
* 根据AST元素节点的属性构造出一个data对象字符串,在后边创建VNode的时候会作为参数传人
*/
export function genData (el: ASTElement, state: CodegenState): string {
const dirs = genDirectives(el, state)
}
看一下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
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i]
needRuntime = true
/* 遍历,获取每一个指令对应的方法 */
const gen: DirectiveFunction = state.directives[dir.name]
if (gen) {
/* 执行gen()也就在执行对应的指令方法 */
needRuntime = !!gen(el, dir, state.warn)
}
}
}
genDrirectives 方法就是遍历 el.directives,然后获取每一个指令对应的方法, 那么我们看一下directives的定义:
源码:src/platforms/web/compiler/directives/index.js
import model from './model'
import text from './text'
import html from './html'
export default {
model,
text,
html
}
那么很明显我们找到model的对应的源码地址:src/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
if (el.component) {
/* 给组件设置v-model */
genComponentModel(el, value, modifiers)
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers)
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers)
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers)
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers)
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers)
return false
}
return true
}
也就是说我们执行 needRuntime = !!gen(el, dir, state.warn) 就是在执行 model 函数,它会根据 AST 元素节点的不同情况去执行不同的逻辑,对于我们这个 case 而言,它会命中 genDefaultModel(el, value, modifiers) 的逻辑,稍后我们也会介绍组件的处理,我们来看一下 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'
const event = lazy
? 'change'
: type === 'range'
? RANGE_TOKEN
: 'input'
let valueExpression = '$event.target.value'
if (trim) {
valueExpression = `$event.target.value.trim()`
}
if (number) {
valueExpression = `_n(${valueExpression})`
}
let code = genAssignmentCode(value, valueExpression)
if (needCompositionGuard) {
code = `if($event.target.composing)return;${code}`
}
addProp(el, 'value', `(${value})`)
addHandler(el, event, code, null, true)
if (trim || number) {
addHandler(el, 'blur', '$forceUpdate()')
}
}
我们一步一步分析genDefaultModel 函数,首先先处理了 modifiers,它的不同主要影响的是 event 和 valueExpression 的值,对于我们的例子,event 为 input,valueExpression 为 $event.target.value。然后去执行 genAssignmentCode 去生成代码,它的定义在 src/compiler/directives/model.js 中
export function genAssignmentCode (
value: string,
assignment: string
): string {
const res = parseModel(value)
if (res.key === null) {
return `${value}=${assignment}`
} else {
return `$set(${res.exp}, ${res.key}, ${assignment})`
}
}
该方法首先对 v-model 对应的 value 做了解析,它处理了非常多的情况,对我们的例子,value 就是 messgae,所以返回的 res.key 为 null,然后我们就得到 ${value}=${assignment}
,也就是
message=$event.target.value
然后我们又命中了 needCompositionGuard 为 true 的逻辑,
if (needCompositionGuard) {
code = `if($event.target.composing)return;${code}`
}
所以最终的 code 为
if($event.target.composing)return;message=$event.target.value
code 生成完后,又执行了 2 句非常关键的代码:
addProp(el, 'value', `(${value})`)
addHandler(el, event, code, null, true)
这实际上就是 input 实现 v-model 的精髓,通过修改 AST 元素,给 el 添加一个 prop,相当于我们在 input 上动态绑定了 value,又给 el 添加了事件处理,相当于在 input 上绑定了 input 事件,其实转换成模板如下
<input
v-bind:value="message"
v-on:input="message=$event.target.value">
其实就是动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message 设置为目标值,这样实际上就完成了数据双向绑定了,所以说 v-model 实际上就是语法糖。
v-model作用在组件
先来个例子
let Child = {
template: '<div>'
+ '<input :value="value" @input="updateValue" placeholder="edit me">' +
'</div>',
props: ['value'],
methods: {
updateValue(e) {
this.$emit('input', e.target.value)
}
}
}
let vm = new Vue({
el: '#app',
template: '<div>' +
'<child v-model="message"></child>' +
'<p>Message is: {{ message }}</p>' +
'</div>',
data() {
return {
message: ''
}
},
components: {
Child
}
})
可以看到,父组件引用 child 子组件的地方使用了 v-model 关联了数据 message;而子组件定义了一个 value 的 prop,并且在 input 事件的回调函数中,通过 this.$emit('input', e.target.value) 派发了一个事件,为了让 v-model 生效,这两点是必须的。
然后,我们会走到src/platforms/web/compiler/directives/model.js
else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers);
return false
}
genComponentModel 函数定义在 src/compiler/directives/model.js 中:
/* 生成组件的v-model */
export function genComponentModel (
el: ASTElement,
value: string,
modifiers: ?ASTModifiers
): ?boolean {
const { number, trim } = modifiers || {}
const baseValueExpression = '?v'
let valueExpression = baseValueExpression
if (trim) {
valueExpression =
`(typeof ${baseValueExpression} === 'string'` +
`? ${baseValueExpression}.trim()` +
`: ${baseValueExpression})`
}
if (number) {
valueExpression = `_n(${valueExpression})`
}
const assignment = genAssignmentCode(value, valueExpression)
el.model = {
value: `(${value})`,
expression: JSON.stringify(value),
callback: `function (${baseValueExpression}) {${assignment}}`
}
}
genComponentModel 的逻辑很简单,对我们的例子而言,生成的 el.model 的值为:
el.model = {
callback:'function (?v) {message=?v}',
expression:'"message"',
value:'(message)'
}
那么在 genDirectives 之后,genData 函数中有一段逻辑如下:
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}
那么父组件最终生成的 render 代码如下:
with(this){
return _c('div',[_c('child',{
model:{
value:(message),
callback:function (?v) {
message=?v
},
expression:"message"
}
}),
_c('p',[_v("Message is: "+_s(message))])],1)
}
可以看到生成了一个model的对象,value就是我们的message,callback回调就是将目标值赋值给了message;
然后在创建子组件 vnode 阶段,会执行 createComponent 函数,它的定义在 src/core/vdom/create-component.js 中:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
其中会对 data.model 的情况做处理,执行 transformModel(Ctor.options, data) 方法:
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.props || (data.props = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
if (isDef(on[event])) {
on[event] = [data.model.callback].concat(on[event])
} else {
on[event] = data.model.callback
}
}
我们可以看到,组件的v-model,它的子组件默认接收的prop是value,默认事件是input,那这个方法的作用也就是给 data.props 添加 data.model.value(双绑的值),并且给data.on 添加 data.model.callback(触发双绑的事件),那么我们的例子扩展如下:
data.props = {
value: (message),
}
data.on = {
input: function (?v) {
message=?v
}
}
其实就相当于我们在这样编写父组件:
let vm = new Vue({
el: '#app',
template: '<div>' +
'<child :value="message" @input="message=arguments[0]"></child>' +
'<p>Message is: {{ message }}</p>' +
'</div>',
data() {
return {
message: ''
}
},
components: {
Child
}
})
子组件传递的 value 绑定到当前父组件的 message,同时监听自定义 input 事件,当子组件派发 input 事件的时候,父组件会在事件回调函数中修改 message 的值,同时 value 也会发生变化,子组件的 input 值被更新。
这就是典型的 Vue 的父子组件通讯模式,父组件通过 prop 把数据传递到子组件,子组件修改了数据后把改变通过 $emit 事件的方式通知父组件,所以说组件上的 v-model 也是一种语法糖。
另外我刚刚也讲到了,我们注意到组件 v-model 的实现,子组件的 value prop 以及派发的 input 事件名是可配的,可以看到 transformModel 中对这部分的处理:
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
// ...
}
也就是说可以在定义子组件的时候通过 model 选项配置子组件接收的 prop 名以及派发的事件名,举个例子:
let Child = {
template: '<div>'
+ '<input :value="msg" @input="updateValue" placeholder="edit me">' +
'</div>',
props: ['msg'],
model: {
prop: 'msg',
event: 'change'
},
methods: {
updateValue(e) {
this.$emit('change', e.target.value)
}
}
}
let vm = new Vue({
el: '#app',
template: '<div>' +
'<child v-model="message"></child>' +
'<p>Message is: {{ message }}</p>' +
'</div>',
data() {
return {
message: ''
}
},
components: {
Child
}
})
子组件修改了接收的 prop 名以及派发的事件名,然而这一切父组件作为调用方是不用关心的,这样做的好处是我们可以把 value 这个 prop 作为其它的用途。
最后感谢黄老师的文章:ustbhuangyi.github.io/vue-analysi…
后记
找工作,面试官非常看重个人的技术沉淀,如果说你有自己的博客(记录自己日常的一些技术分析等),开源(针对公司业务开发的组件,插件等,或者个人为了学习新技术而搭建的一套完整的技术产品,如电商系统等),那么这会让面试官眼前一亮,最起码会给人一种你是热爱技术的感觉,第一印象会加很多分,拿本人的开源项目来说:
- 基于Taro3的虚拟列表:github.com/tingyuxuan2…
- 基于cesium的热力图组件:github.com/cesium-plug…
star虽说没有成千上万,但至少证明你在沉淀,对技术是有追求的!