v-model数据绑定分析

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

描述

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

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Vue</title>
  5. </head>
  6. <body>
  7. <div id="app"></div>
  8. </body>
  9. <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
  10. <script type="text/javascript">
  11. var vm = new Vue({
  12. el: "#app",
  13. data: {
  14. msg: ""
  15. },
  16. template: `
  17. <div>
  18. <div>Message is: {{ msg }}</div>
  19. <input v-model="msg">
  20. </div>
  21. `
  22. })
  23. </script>
  24. </html>

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

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

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

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Vue</title>
  5. </head>
  6. <body>
  7. <div id="app"></div>
  8. </body>
  9. <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
  10. <script type="text/javascript">
  11. var vm = new Vue({
  12. el: "#app",
  13. data: {
  14. msg: ""
  15. },
  16. template: `
  17. <div>
  18. <div>Message is: {{ msg }}</div>
  19. <input :value="msg" @input="msg = $event.target.value">
  20. </div>
  21. `
  22. })
  23. </script>
  24. </html>

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

  • .trim: 输入首尾空格过滤。
  • .lazy: 取代input事件而监听change事件。
  • .number: 输入字符串转为有效的数字,如果这个值无法被parseFloat()解析,则会返回原始的值。
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Vue</title>
  5. </head>
  6. <body>
  7. <div id="app"></div>
  8. </body>
  9. <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
  10. <script type="text/javascript">
  11. var vm = new Vue({
  12. el: "#app",
  13. data: {
  14. msg: 0
  15. },
  16. template: `
  17. <div>
  18. <div>Message is: {{ msg }}</div>
  19. <div>Type is: {{ typeof(msg) }}</div>
  20. <input v-model.number="msg" type="number">
  21. </div>
  22. `
  23. })
  24. </script>
  25. </html>

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

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Vue</title>
  5. </head>
  6. <body>
  7. <div id="app"></div>
  8. </body>
  9. <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script>
  10. <script type="text/javascript">
  11. Vue.component("u-input", {
  12. model: {
  13. prop: "message",
  14. event: "input"
  15. },
  16. props: {
  17. message: {
  18. type: String
  19. },
  20. },
  21. template: `
  22. <div>
  23. <input :value="message" @input="$emit('input', $event.target.value)">
  24. </div>
  25. `
  26. })
  27. var vm = new Vue({
  28. el: "#app",
  29. data: {
  30. msg: ""
  31. },
  32. template: `
  33. <div>
  34. <div>Message is: {{ msg }}</div>
  35. <u-input v-model="msg"></u-input>
  36. </div>
  37. `
  38. })
  39. </script>
  40. </html>

分析

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

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

  1. // dev/src/compiler/index.js line 11
  2. export const createCompiler = createCompilerCreator(function baseCompile (
  3. template: string,
  4. options: CompilerOptions
  5. ): CompiledResult {
  6. const ast = parse(template.trim(), options) // 生成AST
  7. if (options.optimize !== false) {
  8. optimize(ast, options) // 优化AST
  9. }
  10. const code = generate(ast, options) // 生成代码 即render字符串
  11. return {
  12. ast,
  13. render: code.render,
  14. staticRenderFns: code.staticRenderFns
  15. }
  16. })

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

  1. // dev/src/compiler/codegen/index.js line 43
  2. export function generate (
  3. ast: ASTElement | void,
  4. options: CompilerOptions
  5. ): CodegenResult {
  6. const state = new CodegenState(options)
  7. const code = ast ? genElement(ast, state) : '_c("div")'
  8. return {
  9. render: `with(this){return ${code}}`, // render字符串
  10. staticRenderFns: state.staticRenderFns
  11. }
  12. }
  13. // dev/src/compiler/codegen/index.js line 55
  14. export function genElement (el: ASTElement, state: CodegenState): string {
  15. // ...
  16. data = genData(el, state)
  17. // ...
  18. }
  19. // dev/src/compiler/codegen/index.js line 219
  20. export function genData (el: ASTElement, state: CodegenState): string {
  21. // ...
  22. const dirs = genDirectives(el, state)
  23. // ...
  24. }

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

  1. // dev/src/compiler/codegen/index.js line 309
  2. function genDirectives (el: ASTElement, state: CodegenState): string | void {
  3. const dirs = el.directives // 获取指令
  4. if (!dirs) return
  5. let res = 'directives:['
  6. let hasRuntime = false
  7. let i, l, dir, needRuntime
  8. for (i = 0, l = dirs.length; i < l; i++) { // 遍历指令
  9. dir = dirs[i]
  10. needRuntime = true
  11. const gen: DirectiveFunction = state.directives[dir.name] // 对于v-model来说 const gen = state.directives["model"];
  12. if (gen) {
  13. // compile-time directive that manipulates AST.
  14. // returns true if it also needs a runtime counterpart.
  15. needRuntime = !!gen(el, dir, state.warn)
  16. }
  17. if (needRuntime) {
  18. hasRuntime = true
  19. res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
  20. dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
  21. }${
  22. dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
  23. }${
  24. dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
  25. }},`
  26. }
  27. }
  28. if (hasRuntime) {
  29. return res.slice(0, -1) + ']'
  30. }
  31. }

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

  1. // dev/src/platforms/web/compiler/directives/model.js line 14
  2. export default function model (
  3. el: ASTElement,
  4. dir: ASTDirective,
  5. _warn: Function
  6. ): ?boolean {
  7. warn = _warn
  8. const value = dir.value
  9. const modifiers = dir.modifiers
  10. const tag = el.tag
  11. const type = el.attrsMap.type
  12. if (process.env.NODE_ENV !== 'production') {
  13. // inputs with type="file" are read only and setting the input's
  14. // value will throw an error.
  15. if (tag === 'input' && type === 'file') {
  16. warn(
  17. `<${el.tag} v-model="${value}" type="file">:\n` +
  18. `File inputs are read only. Use a v-on:change listener instead.`,
  19. el.rawAttrsMap['v-model']
  20. )
  21. }
  22. }
  23. // 分支处理
  24. if (el.component) {
  25. genComponentModel(el, value, modifiers)
  26. // component v-model doesn't need extra runtime
  27. return false
  28. } else if (tag === 'select') {
  29. genSelect(el, value, modifiers)
  30. } else if (tag === 'input' && type === 'checkbox') {
  31. genCheckboxModel(el, value, modifiers)
  32. } else if (tag === 'input' && type === 'radio') {
  33. genRadioModel(el, value, modifiers)
  34. } else if (tag === 'input' || tag === 'textarea') {
  35. genDefaultModel(el, value, modifiers)
  36. } else if (!config.isReservedTag(tag)) {
  37. genComponentModel(el, value, modifiers)
  38. // component v-model doesn't need extra runtime
  39. return false
  40. } else if (process.env.NODE_ENV !== 'production') {
  41. warn(
  42. `<${el.tag} v-model="${value}">: ` +
  43. `v-model is not supported on this element type. ` +
  44. 'If you are working with contenteditable, it\'s recommended to ' +
  45. 'wrap a library dedicated for that purpose inside a custom component.',
  46. el.rawAttrsMap['v-model']
  47. )
  48. }
  49. // ensure runtime directive metadata
  50. return true
  51. }

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

  1. // dev/src/platforms/web/compiler/directives/model.js line 127
  2. function genDefaultModel (
  3. el: ASTElement,
  4. value: string,
  5. modifiers: ?ASTModifiers
  6. ): ?boolean {
  7. const type = el.attrsMap.type
  8. // warn if v-bind:value conflicts with v-model
  9. // except for inputs with v-bind:type
  10. // value与v-model冲突则发出警告
  11. if (process.env.NODE_ENV !== 'production') {
  12. const value = el.attrsMap['v-bind:value'] || el.attrsMap[':value']
  13. const typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']
  14. if (value && !typeBinding) {
  15. const binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'
  16. warn(
  17. `${binding}="${value}" conflicts with v-model on the same element ` +
  18. 'because the latter already expands to a value binding internally',
  19. el.rawAttrsMap[binding]
  20. )
  21. }
  22. }
  23. // 修饰符处理
  24. const { lazy, number, trim } = modifiers || {}
  25. const needCompositionGuard = !lazy && type !== 'range'
  26. const event = lazy
  27. ? 'change'
  28. : type === 'range'
  29. ? RANGE_TOKEN
  30. : 'input'
  31. let valueExpression = '$event.target.value'
  32. if (trim) {
  33. valueExpression = `$event.target.value.trim()`
  34. }
  35. if (number) {
  36. valueExpression = `_n(${valueExpression})`
  37. }
  38. let code = genAssignmentCode(value, valueExpression)
  39. if (needCompositionGuard) {
  40. code = `if($event.target.composing)return;${code}`
  41. }
  42. addProp(el, 'value', `(${value})`)
  43. addHandler(el, event, code, null, true)
  44. if (trim || number) {
  45. addHandler(el, 'blur', '$forceUpdate()')
  46. }
  47. }
  48. // dev/src/compiler/directives/model.js line 36
  49. export function genAssignmentCode (
  50. value: string,
  51. assignment: string
  52. ): string {
  53. const res = parseModel(value)
  54. if (res.key === null) {
  55. return `${value}=${assignment}`
  56. } else {
  57. return `$set(${res.exp}, ${res.key}, ${assignment})`
  58. }
  59. }

每日一题

  1. https://github.com/WindrunnerMax/EveryDay

参考

  1. https://cn.vuejs.org/v2/api/#v-model
  2. https://www.jianshu.com/p/19bb4912c62a
  3. https://www.jianshu.com/p/0d089f770ab2
  4. https://cn.vuejs.org/v2/guide/forms.html
  5. https://juejin.im/post/6844903784963899400
  6. https://juejin.im/post/6844903999414485005
  7. https://segmentfault.com/a/1190000021516035
  8. https://segmentfault.com/a/1190000015848976
  9. https://github.com/haizlin/fe-interview/issues/560
  10. 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. [PyTorch 学习笔记] 6.2 Normalization

    本章代码: https://github.com/zhangxiann/PyTorch_Practice/blob/master/lesson6/bn_and_initialize.py https: ...

  2. LeetCode.518 零钱兑换Ⅱ(记录)

    518题是背包问题的变体,也称完全背包问题. 解法参考了该篇文章,然后对自己困惑的地方进行记录. 下面是该题的描述: 有一个背包,最大容量为 amount,有一系列物品 coins,每个物品的重量为 ...

  3. 学习STM32的一些记录_创建库函数版本的工程

    1.新建一个文件夹,用于存放MDK的工程所有文件.例如新建文件夹Template. 2.在Template下新建一个USER文件夹,用于存放工程. 3.打开MDK5,新建工程,目录在USER下. 4. ...

  4. django之安装和项目创建

    dos界面下安装django 自动下载和安装:cmd:pip3 install  django 手动安装: 1.登录django官网下载django 2.下载地址:https://www.django ...

  5. Unit5:广播

    静态广播 1.定义 public class TestBroadCast extends BroadcastReceiver { @Override public void onReceive(Con ...

  6. 面试官:哪些场景会产生OOM?怎么解决?

    这个面试题是一个朋友在面试的时候碰到的,什么时候会抛出OutOfMemery异常呢?初看好像挺简单的,其实深究起来考察的是对整个JVM的了解,而且这个问题从网上可以翻到一些乱七八糟的答案,其实在总结下 ...

  7. Cobalt Strike后渗透安装和初步使用

    Cobalt Strike安装 系统要求 Cobalt Strike要求Java 1.8,Oracle Java ,或OpenJDK . 如果你的系统上装有防病毒产品,请确保在安装 Cobalt St ...

  8. SpringBoot框架:配置文件application.properties和application.yml的区别

    一.格式 1.application.properties格式: server.port=8080 server.servlet.context-path=/cn spring.datasource. ...

  9. 疑难杂症 | Excel VBA锁定指定单元格区域

    背景:锁定EXCEL表头 一.手动操作流程 其基本逻辑并不赋值,手动操作流程是: 1.取消所有单元格的"锁定"格式 CTRL+A,选中全部的单元格→单击右键→设置单元格格式→保护→ ...

  10. Redis 发布订阅,小功能大用处,真没那么废材!

    今天小黑哥来跟大家介绍一下 Redis 发布/订阅功能. 也许有的小伙伴对这个功能比较陌生,不太清楚这个功能是干什么的,没关系小黑哥先来举个例子. 假设我们有这么一个业务场景,在网站下单支付以后,需要 ...