数据响应是通过数据的改变去驱动 DOM 视图的变化
双向绑定除了数据驱动 DOM 外, DOM 的变化反过来影响数据,是一个双向关系
Vue 中通过 v-model 来实现双向绑定
v-model 即可以作用在普通表单元素上,又可以作用在组件上,它其实是一个语法糖
表单元素
例子
let vm = new Vue({el: '#app',template: '<div>'+ '<input v-model="message" placeholder="edit me">' +'<p>Message is: {{ message }}</p>' +'</div>',data() {return {message: ''}}})
从编译阶段分析,首先是 parse 阶段, v-model 被当做普通的指令解析到 el.directives 中,然后在 codegen 阶段,执行 genData 的时候,会执行 const dirs = genDirectives(el, state)
定义在 src/compiler/codegen/index.js 中
function genDirectives (el: ASTElement, state: CodegenState): string | void {const dirs = el.directivesif (!dirs) returnlet res = 'directives:['let hasRuntime = falselet i, l, dir, needRuntime// 遍历el.directivesfor (i = 0, l = dirs.length; i < l; i++) {dir = dirs[i]needRuntime = true// 获取每一个指令对应的方法// 指令方法实际上是在实例化CodegenState时通过option传入的,option是编译相关的配置,它在不同的平台下配置不同const gen: DirectiveFunction = state.directives[dir.name]if (gen) {// compile-time directive that manipulates AST.// returns true if it also needs a runtime counterpart.needRuntime = !!gen(el, dir, state.warn) // model函数}// 根据指令生成一些data代码if (needRuntime) {hasRuntime = trueres += `{name:"${dir.name}",rawName:"${dir.rawName}"${dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''}${dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''}${dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''}},`}}if (hasRuntime) {return res.slice(0, -1) + ']'}}
option在web环境下的定义在src/platforms/web/compiler/options.js中
export const baseOptions: CompilerOptions = {expectHTML: true,modules,directives,isPreTag,isUnaryTag,mustUseProp,canBeLeftOpenTag,isReservedTag,getTagNamespace,staticKeys: genStaticKeys(modules)}
directives定义在src/platforms/web/compiler/directives/index.js中
export default {model,text,html}
v-model对应的 directive 函数是在 src/platforms/web/compiler/directives/model.js 中定义的 model 函数
export default function model (el: ASTElement,dir: ASTDirective,_warn: Function): ?boolean {warn = _warnconst value = dir.valueconst modifiers = dir.modifiersconst tag = el.tagconst type = el.attrsMap.typeif (process.env.NODE_ENV !== 'production') {// inputs with type="file" are read only and setting the input's// value will throw an error.if (tag === 'input' && type === 'file') {warn(`<${el.tag} v-model="${value}" type="file">:\n` +`File inputs are read only. Use a v-on:change listener instead.`,el.rawAttrsMap['v-model'])}}// 根据AST元素节点的不同情况去执行不同的逻辑if (el.component) {genComponentModel(el, value, modifiers)// component v-model doesn't need extra runtimereturn 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)// component v-model doesn't need extra runtimereturn false} else if (process.env.NODE_ENV !== 'production') {warn(`<${el.tag} v-model="${value}">: ` +`v-model is not supported on this element type. ` +'If you are working with contenteditable, it\'s recommended to ' +'wrap a library dedicated for that purpose inside a custom component.',el.rawAttrsMap['v-model'])}// ensure runtime directive metadatareturn true}
也就是说执行 needRuntime = !!gen(el, dir, state.warn) 就是在执行 model 函数
genDefaultModel
function genDefaultModel (el: ASTElement,value: string,modifiers: ?ASTModifiers): ?boolean {const type = el.attrsMap.type// warn if v-bind:value conflicts with v-model// except for inputs with v-bind:typeif (process.env.NODE_ENV !== 'production') {const value = el.attrsMap['v-bind:value'] || el.attrsMap[':value']const typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']if (value && !typeBinding) {const binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'warn(`${binding}="${value}" conflicts with v-model on the same element ` +'because the latter already expands to a value binding internally',el.rawAttrsMap[binding])}}// 处理了modeifiers,它的不同主要影响的是event和valueExpression的值const { lazy, number, trim } = modifiers || {}const needCompositionGuard = !lazy && type !== 'range'// 对于当前的例子 event为input valueExpression为$event.target.valueconst 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})`}// 执行genAssignmentCode去生成代码let code = genAssignmentCode(value, valueExpression)if (needCompositionGuard) {// 最终code为 if($event.target.composing) return;message=$event.target.valuecode = `if($event.target.composing)return;${code}`}addProp(el, 'value', `(${value})`)addHandler(el, event, code, null, true)if (trim || number) {addHandler(el, 'blur', '$forceUpdate()')}}
genAssignmentCode定义在src/compiler/directives/model.js中
/*** Cross-platform codegen helper for generating v-model value assignment code.*/export function genAssignmentCode (value: string,assignment: string): string {// 对v-model对应的value做解析// 例子中 value就是message res.key为nullconst res = parseModel(value)if (res.key === null) {return `${value}=${assignment}` // 也就是message=$event.target.value} else {return `$set(${res.exp}, ${res.key}, ${assignment})`}}
code生成后
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 实际上就是语法糖
对例子而言最终生成的render代码如下
with(this) {return _c('div',[_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}}),_c('p',[_v("Message is: "+_s(message))])])}
组件
例子
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 生效,这两点是必须的
从编译阶段说起,对于父组件而言,在编译阶段会解析v-model 指令,依然会执行genData函数中的genDirectives函数,接着执行 src/platforms/web/compiler/directives/model.js 中定义的 model 函数,并命中如下逻辑
else if (!config.isReservedTag(tag)) {genComponentModel(el, value, modifiers)// component v-model doesn't need extra runtimereturn false}
genComponentModel函数定义在src/compiler/directives/model.js中
/*** Cross-platform code generation for component v-model*/export function genComponentModel (el: ASTElement,value: string,modifiers: ?ASTModifiers): ?boolean {const { number, trim } = modifiers || {}const baseValueExpression = '$$v'let valueExpression = baseValueExpressionif (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}}`}}
对例子而言生成的el.model的值为
el.model = {callback:'function ($$v) {message=$$v}',expression:'"message"',value:'(message)'}
在 genDirectives 之后,genData 函数中有一段逻辑如下
// component v-modelif (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)}
然后在创建子组件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 {// ...// transform component v-model data into props & events// 对data.model的情况做处理if (isDef(data.model)) {transformModel(Ctor.options, data)}// extract propsconst propsData = extractPropsFromVNodeData(data, Ctor, tag)// ...// extract listeners, since these needs to be treated as// child component listeners instead of DOM listenersconst listeners = data.on// ...const vnode = new VNode(`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,data, undefined, undefined, undefined, context,{ Ctor, propsData, listeners, tag, children },asyncFactory)// ...return vnode}
transformModel
// transform component v-model info (value and callback) into// prop and event handler respectively.function transformModel (options, data: any) {const prop = (options.model && options.model.prop) || 'value'const event = (options.model && options.model.event) || 'input'// 给data.props添加data.model.value,并且给data.on添加data.model.callback;(data.attrs || (data.attrs = {}))[prop] = data.model.valueconst on = data.on || (data.on = {})const existing = on[event]const callback = data.model.callbackif (isDef(existing)) {if (Array.isArray(existing)? existing.indexOf(callback) === -1: existing !== callback) {on[event] = [callback].concat(existing)}} else {on[event] = 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 中对这部分的处理
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 作为其它的用途
