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<V> 源码分析 Android上的实现

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

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

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

  4. PE文件加节感染之Win32.Loader.bx.V病毒分析

    一.病毒名称:Win32.Loader.bx.V 二.分析工具:IDA 5.5.OllyDebug.StudPE 三.PE文件加节感染病毒简介 PE病毒感染的方式比较多,也比较复杂也比较难分析,下面就 ...

  5. 中文情感分析 glove+LSTM

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

  6. 剖析gcc -v输出

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

  7. 不可错过的效能利器「GitHub 热点速览 v.22.39」

    如果你是一名前端工程师且维护着多个网站,不妨试试本周榜上有名的 HTML-first 的 Qwik,提升网站访问速度只用一招.除了提升网站加载速度的 Qwik,本周周榜上榜的 Whisper 也是一个 ...

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

    2014-08-19 Created By BaoXinjian

  9. YUV和RGB格式分析

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

随机推荐

  1. Python测试框架pytest命令行参数用法

    在Shell执行pytest -h可以看到pytest的命令行参数有这10大类,共132个 序号 类别 中文名 包含命令行参数数量 1 positional arguments 形参 1 2 gene ...

  2. C#开发PACS医学影像处理系统(三):界面布局之工具栏

    工具栏布局采用WPF中Grid作为容器,按钮采用自定义样式和图标,并采用Separator分割线: XAML设计器代码: 其中  Style="{StaticResource ButtonS ...

  3. python appium app ui 自动化

    1.下载Android-SDK.夜神安卓模拟器.Appium.adb命令执行包并配置环境变量 2.打开夜神 3.打开Appium进行配置,配置如下 4.启动命令,到夜神bin目录下执行 adb con ...

  4. (课堂笔记)第三章:F5 LTM 负载均衡理论

    BIG-IP LTM负载均衡理论 ------F5 BIG-IP LTM负载均衡策略---------- 1.1 LTM VS工作模式F5 BIG-IP LTM的内部对于数据包的处理方式,即是VS的工 ...

  5. python中unittest参数化

    先安装 pip install parameterized 在测试用例类中,@parameterized.parameterized.expand(这里面传一个列表),这个就是参数化的意思

  6. 算法数据结构 | 三个步骤完成强连通分量分解的Kosaraju算法

    强连通分量分解的Kosaraju算法 今天是算法数据结构专题的第35篇文章,我们来聊聊图论当中的强连通分量分解的Tarjan算法. Kosaraju算法一看这个名字很奇怪就可以猜到它也是一个根据人名起 ...

  7. 移动web开发之rem适配布局

    移动web开发之rem适配布局 方案: 页面布局文字能否随着屏幕大小变化而变化 流式布局和flex布局主要针对于宽度布局,那高度如何布局? 怎样让屏幕发生变化的时候元素高度和宽度等比例缩放? 1. r ...

  8. Magicodes.IE之导入导出筛选器

    总体设计   Magicodes.IE是一个导入导出通用库,支持Dto导入导出以及动态导出,支持Excel.Word.Pdf.Csv和Html.在本篇教程,笔者将讲述如何使用Magicodes.IE的 ...

  9. Java随谈(一)魔术数字、常量和枚举

    本文适合对 Java 或 C 有一些了解的用户阅读,推荐阅读时间15分钟. 导言 写这个系列的原因? 我曾经听过一种说法,如果不了解Liunx的网络通讯,就很难理解理解Java的IO:如果不知道Jav ...

  10. git server“丢失”commit问题探究

    1 背景 gitlab某仓库有同事发现部分代码文件内容丢失,具体表现 A. dev分支commit信息是连续的,看不出明显的大时间范围批量丢失 B. 以SuncardCashier/control/C ...