v-model数据绑定分析

v-modelVue提供的指令,其主要作用是可以实现在表单<input><textarea><select>等元素以及组件上创建双向数据绑定,其本质上就是一种语法糖,既可以直接定义在原生表单元素,也可以支持自定义组件。在组件的实现中,可以配置子组件接收的prop名称,以及派发的事件名称实现组件内的v-model双向绑定。

描述

可以用v-model指令在表单<input><textarea><select>元素上创建双向数据绑定,其会根据控件类型自动选取正确的方法来更新元素,以<input>作为示例使用v-model

<!DOCTYPE html>
<html>
<head>
<title>Vue</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: "#app",
data: {
msg: ""
},
template: `
<div>
<div>Message is: {{ msg }}</div>
<input v-model="msg">
</div>
`
})
</script>
</html>

当不使用v-model语法糖时,可以自行实现一个双向绑定,实际上v-model在内部为不同的输入元素使用不同的property并抛出不同的事件:

  • inputtextarea元素使用value propertyinput事件。
  • checkboxradio元素使用checked propertychange事件。
  • select元素将value作为prop并将change作为事件。

同样以<input>作为示例而不使用v-model实现双向绑定。

<!DOCTYPE html>
<html>
<head>
<title>Vue</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: "#app",
data: {
msg: ""
},
template: `
<div>
<div>Message is: {{ msg }}</div>
<input :value="msg" @input="msg = $event.target.value">
</div>
`
})
</script>
</html>

对于v-model还有修饰符用以控制用户输入:

  • .trim: 输入首尾空格过滤。
  • .lazy: 取代input事件而监听change事件。
  • .number: 输入字符串转为有效的数字,如果这个值无法被parseFloat()解析,则会返回原始的值。
<!DOCTYPE html>
<html>
<head>
<title>Vue</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: "#app",
data: {
msg: 0
},
template: `
<div>
<div>Message is: {{ msg }}</div>
<div>Type is: {{ typeof(msg) }}</div>
<input v-model.number="msg" type="number">
</div>
`
})
</script>
</html>

当使用自定义组件时,在组件上的v-model默认会利用名为valueprop和名为input的事件,但是像单选框、复选框等类型的输入控件可能会将value attribute用于不同的目的,此时可以使用model选项可以用来避免这样的冲突。

<!DOCTYPE html>
<html>
<head>
<title>Vue</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
Vue.component("u-input", {
model: {
prop: "message",
event: "input"
},
props: {
message: {
type: String
},
},
template: `
<div>
<input :value="message" @input="$emit('input', $event.target.value)">
</div>
`
})
var vm = new Vue({
el: "#app",
data: {
msg: ""
},
template: `
<div>
<div>Message is: {{ msg }}</div>
<u-input v-model="msg"></u-input>
</div>
`
})
</script>
</html>

分析

Vue源码的实现比较复杂,会处理各种兼容问题与异常以及各种条件分支,文章分析比较核心的代码部分,精简过后的版本,重要部分做出注释,commit idef56410

v-model属于Vue的指令,所以从编译阶段开始分析,在解析到指令之前,Vue的解析阶段大致流程:解析模版字符串生成AST、优化语法树AST、生成render字符串。

// dev/src/compiler/index.js line 11
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options) // 生成AST
if (options.optimize !== false) {
optimize(ast, options) // 优化AST
}
const code = generate(ast, options) // 生成代码 即render字符串
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})

对指令的处理就在生成render字符串的过程,也就是generate函数的处理过程,在generate中调用genElement -> genData -> genDirectives,文章主要从genDirectives函数进行分析。

// dev/src/compiler/codegen/index.js line 43
export 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}}`, // render字符串
staticRenderFns: state.staticRenderFns
}
} // dev/src/compiler/codegen/index.js line 55
export function genElement (el: ASTElement, state: CodegenState): string {
// ...
data = genData(el, state)
// ...
} // dev/src/compiler/codegen/index.js line 219
export function genData (el: ASTElement, state: CodegenState): string {
// ...
const dirs = genDirectives(el, state)
// ...
}

在生成AST阶段,也就是parse阶段,v-model被当做普通的指令解析到el.directives中,genDrirectives方法就是遍历el.directives,然后获取每一个指令对应的方法,对于v-model而言,在此处获取的是{name: "model", rawName: "v-model" ...},通过state找到model指令对应的方法model()并执行该方法。

// dev/src/compiler/codegen/index.js line 309
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] // 对于v-model来说 const gen = state.directives["model"];
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) + ']'
}
}

model方法主要是根据传入的参数对tag的类型进行判断,调用不同的处理逻辑。

// dev/src/platforms/web/compiler/directives/model.js line 14
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 (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']
)
}
} // 分支处理
if (el.component) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
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)
// component v-model doesn't need extra runtime
return 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 metadata
return true
}

genDefaultModel函数先处理了modifiers修饰符,其不同主要影响的是eventvalueExpression的值,对于<input>标签eventinputvalueExpression$event.target.value,然后去执行genAssignmentCode去生成代码,以及添加属性值与事件处理。

// dev/src/platforms/web/compiler/directives/model.js line 127
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:type
// value与v-model冲突则发出警告
if (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]
)
}
} // 修饰符处理
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()')
}
} // dev/src/compiler/directives/model.js line 36
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})`
}
}

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://cn.vuejs.org/v2/api/#v-model
https://www.jianshu.com/p/19bb4912c62a
https://www.jianshu.com/p/0d089f770ab2
https://cn.vuejs.org/v2/guide/forms.html
https://juejin.im/post/6844903784963899400
https://juejin.im/post/6844903999414485005
https://segmentfault.com/a/1190000021516035
https://segmentfault.com/a/1190000015848976
https://github.com/haizlin/fe-interview/issues/560
https://ustbhuangyi.github.io/vue-analysis/v2/extend/v-model.html

v-model数据绑定分析的更多相关文章

  1. P,V操作实例分析

    刚开始学习操作系统的时候,就听说PV操作,简单说说PV操作. ●  P(S): S=S-1 如果S≥0,则该进程继续执行:               S<0,进程暂停执行,放入信号量的等待队列 ...

  2. Java FutureTask&lt;V&gt; 源码分析 Android上的实现

    FutureTask类提供了可取消的异步计算,并且可以利用开始和取消计算的方法.查询计算是否完成的方法和获取计算结果的方法. 首先看一下继承关系 public class FutureTask< ...

  3. yii2 源码分析 model类分析 (五)

    模型类是数据模型的基类.此类继承了组件类,实现了3个接口 先介绍一下模型类前面的大量注释说了什么: * 模型类是数据模型的基类.此类继承了组件类,实现了3个接口 * 实现了IteratorAggreg ...

  4. 中文情感分析 glove+LSTM

    最近尝试了一下中文的情感分析. 主要使用了Glove和LSTM.语料数据集采用的是中文酒店评价语料 1.首先是训练Glove,获得词向量(这里是用的300d).这一步使用的是jieba分词和中文维基. ...

  5. 剖析gcc -v输出

    分析gcc -v的详细信息的意义 首先我们需要清楚一点,我们并不能完全弄清楚gcc -v的所有信息,因为毕竟我们并不是GCC编译器集合的实现者,对于这些信息,他们才是最清楚的.由于我们不能将所有的信息 ...

  6. DBA_Oracle LogMiner分析重做和归档日志(案例)

    2014-08-19 Created By BaoXinjian

  7. YUV和RGB格式分析

    做嵌入式项目的时候,涉及到YUV视频格式到RGB图像的转换,虽然之前有接触到RGB到都是基于opencv的处理,很多东西并不需要我们过多深入的去探讨,现在需要完全抛弃现有的算法程序,需要从内存中一个字 ...

  8. OpenRisc-41-or1200的cache模块分析

    引言 为CPU提供足够的,稳定的指令流和数据流是计算机体系结构设计中两个永恒的话题.为了给CPU提供指令流,需要设计分支预测机构,为了给CPU提供数据流,就需要设计cache了.其实,无论是insn还 ...

  9. ASP.NET三层架构的分析

    BLL   是业务逻辑层   Business   Logic   Layer DAL   是数据访问层   Data   Access   Layer ASP.NET的三层架构(DAL,BLL,UI ...

  10. WinDbg分析DUMP文件

    1. 如何生成dump文件?     原理:通过SetUnhandledExceptionFilter设置捕获dump的入口,然后通过MiniDumpWriteDump生成dump文件:       ...

随机推荐

  1. Redis总结(二)C#中如何使用redis

    上一篇讲述了安装redis<Redis总结(一)Redis安装>,同时也大致介绍了redis的优势和应用场景.本篇着重讲解.NET中如何使用redis和C#. Redis官网提供了很多开源 ...

  2. Android 本地加载网页与显示网络图片

    有时候需要在应用程序里展示一些网页,但是需求里又明确指出,不允许打开系统浏览器,显然也不可能去编写一个浏览器出来,这时就需要使用 WebView控件,借助它我们就可以在自己的应用程序里嵌入一个浏览器, ...

  3. jquery 获取浏览器可视窗口大小,滚动条高度

    alert($(window).height()); //浏览器时下窗口可视区域高度 alert($(document).height()); //浏览器时下窗口文档的高度 alert($(docum ...

  4. baas &amp; API 网关

    最近一段时间一直在做API 网关的工作.清晰看到当前云下Baas将会是主要方向,而API网关会是一把利剑. 本人正在规划API网关,有兴趣的可以一起探讨:hotwheels_bo@163.com

  5. python下载时报错 Errno 10060] A connection attempt failed because the connected party did not properly respond after a period of time

    def downloadXml(isExists,filedir,filename): if not isExists: os.mkdir(filedir) local = os.path.join( ...

  6. 矢量量化(VQ)

    作者:桂. 时间:2017-05-31  21:14:56 链接:http://www.cnblogs.com/xingshansi/p/6925955.html 前言 VQ(Vector Quant ...

  7. Spring Security4实例(Java config 版) —— Remember-Me

    本文源码请看这里 相关文章: Spring Security4实例(Java config版)--ajax登录,自定义验证 Spring Security提供了两种remember-me的实现,一种是 ...

  8. 使用js修改url地址参数

    该方法可以修改url的参数. 例如将 www.baidu.com 修改为 www.baidu.com?name=123 操作为: window.location.href = changeURLArg ...

  9. 51nod 1294 修改数组

    若a[i]-i(i从1开始)的值小于0,那么a[i]必须改变 若a[i]-i的值大于等于0,将a[i]-i存入新的数组中,求出新数组的最长非严格上升子序列,所得即最多的,不用改变的数. #includ ...

  10. Web中的四大作用域对象

    request:请求对象 类型:HttpServletRequest session:表示一次会话,可以处理一个用户多个页面之间的请求 application:标识web应用上下文,类型:Servle ...