Vue v-model 指令详解

Vue 提供了v-model 指令来帮助用户方便的处理表单元素的数据绑定和用户输入内容同步到数据上(双向绑定)。
用法如下:


初始test的值会绑定到input的value中
当用户修改input的内容
test的变量也同时更新。
"_c('input',{directives:[{name:"model",rawName:"v-model",value:(test),expression:"test"}] ,attrs:{"type":"text"},domProps:{"value":(test)}, on:{"input":function($event){if($event.target.composing)return; test=$event.target.value}}})"

上面是vue 为v-model生成的render 代码,我们可以看到v-model 最终除了绑定默认的值外,还添加了一个input事件,input事件将input的值再重新传给绑定的值。所以v-model其实是一个双向绑定的语法糖
转换方法
function genDefaultModel ( el, value, modifiers ) { var type = el.attrsMap.type; // warn if v-bind:value conflicts with v-model // except for inputs with v-bind:type { var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value']; var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']; if (value$1 && !typeBinding) { var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'; warn$1( binding + "=\"" + value$1 + "\" conflicts with v-model on the same element " + 'because the latter already expands to a value binding internally', el.rawAttrsMap[binding] ); } }var ref = modifiers || {}; var lazy = ref.lazy; var number = ref.number; var trim = ref.trim; var needCompositionGuard = !lazy && type !== 'range'; // lazy 修饰符会强制将event改为change var event = lazy ? 'change' : type === 'range' ? RANGE_TOKEN : 'input'; var valueExpression = '$event.target.value'; if (trim) { // trim修饰符会处理值的前后空格 valueExpression = "$event.target.value.trim()"; } if (number) { // number修饰符会尝试将数字进行number转换,失败了还保留原值 valueExpression = "_n(" + valueExpression + ")"; }var code = genAssignmentCode(value, valueExpression); if (needCompositionGuard) { //生成input事件代码 code = "if($event.target.composing)return; " + code; }addProp(el, 'value', ("(" + value + ")")); addHandler(el, event, code, null, true); if (trim || number) { addHandler(el, 'blur', '$forceUpdate()'); } }function genAssignmentCode ( value, assignment ) { var res = parseModel(value); if (res.key === null) { return (value + "=" + assignment) } else { return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")") } }

如果我们给一个自定义组件添加v-model
因为v-model默认是需要绑定在具有input或者change事件的表单元素上的。所以对于非表单元素比如封装了表单元素的自定义组件,v-model是需要我们自己来实现这个语法糖的事件传递的,具体做法参考下面代码:
Vue.component('base-input', { props: { ‘value’: String }, template: ` ` })


组件v-model生成的中间代码
"_c('div',{attrs:{"id":"app"}}, [_c('test',{model:{value:(test), callback:function ($$v) { test=$$v },expression:"test"}})],1)"

Vue v-model 指令详解
文章图片

最终model里的callback会转化为组件vnode里的 on事件map里了。
转换方法:
function transformModel (options, data) { var prop = (options.model && options.model.prop) || 'value'; var event = (options.model && options.model.event) || 'input' ; (data.attrs || (data.attrs = {}))[prop] = data.model.value; var on = data.on || (data.on = {}); var existing = on[event]; var callback = data.model.callback; if (isDef(existing)) { if ( Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) { on[event] = [callback].concat(existing); } } else { // callback 传入到on的map里了 on[event] = callback; } }

所以在组件内部我们要将form元素的 input事件(或者radio、checkbox的change事件)通过$emit的方法传出来。外面的组件上vue已经帮我们默认添加好了input事件来接受参数。
v-model 完整的流程代码:
1.处理v-model指令生成代码
function genDirectives (el, state) { var dirs = el.directives; if (!dirs) { return } var res = 'directives:['; var hasRuntime = false; var i, l, dir, needRuntime; for (i = 0, l = dirs.length; i < l; i++) { dir = dirs[i]; needRuntime = true; // 这里取model的gen方法 处理v-model此时 dir.name = "model" var gen = 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); } 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.isDynamicArg ? dir.arg : ("\"" + (dir.arg) + "\""))) : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},"; } } if (hasRuntime) { return res.slice(0, -1) + ']' } }

【Vue v-model 指令详解】不同的form表单元素绑定会有一些处理细节上的差别)
function model ( el, dir, _warn ) { debugger warn$1 = _warn; var value = https://www.it610.com/article/dir.value; var modifiers = dir.modifiers; var tag = el.tag; var type = el.attrsMap.type; { // inputs with type="file" are read only and setting the input's // value will throw an error. if (tag === 'input' && type === 'file') { warn$1( "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" + "File inputs are read only. Use a v-on:change listener instead.", el.rawAttrsMap['v-model'] ); } } // 组件上的v-model的处理 if (el.component) { genComponentModel(el, value, modifiers); // component v-model doesn't need extra runtime return false } else if (tag === 'select') { // select 标签的处理 genSelect(el, value, modifiers); } else if (tag === 'input' && type === 'checkbox') { // input type为checkbox的处理 genCheckboxModel(el, value, modifiers); } else if (tag === 'input' && type === 'radio') {// input type为radio的处理 genRadioModel(el, value, modifiers); } else if (tag === 'input' || tag === 'textarea') {// input type为textarea的处理 genDefaultModel(el, value, modifiers); } else if (!config.isReservedTag(tag)) {// 非常规标签,按照自定义组件处理 genComponentModel(el, value, modifiers); // component v-model doesn't need extra runtime return false } else { warn$1( "<" + (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 metadata return true }

    推荐阅读