Vue@2x , v-model 指令双向绑定原理

331 阅读3分钟

v-model 都做了什么


Vue 给我们提供了一些非常方便的内置指令,其中使用最多的应该就是 v-model指令了,但可能使用很频繁却鲜有人深入理解官方是怎么实现这个双向绑定指令的。 借空余时间就深入源码分析了Vue@2x, 从模板到完成DOM 元素的渲染都做了哪些事情。

我们在示例代码中定义了如下模板,主要就是给input 标签加了 v-model指令。当我们在输入框输入数据的时候,我们下面DIV的内容就会同步更新。这里面主要涉及了Vue 的2个核心一个是数据的双向绑定,另一个就是数据驱动视图。

  1. 当我们输入数据的时候,会触发v-model指令去更新实例属性 text
  2. 实例属性text 在初始化的时候经过Vue的初始化后就是一个响应式的数据,只要数据变化,就会驱动组件的重新渲染。

那 v-model 是怎么实现在输入框输入数据,然后去同步更新组件实例属性的呢?

<-- template --!>
<div class="app-wrap">
    <input type="text" v-model="text">
    <div class="slot">v-model: {{text}}</div>
</div>

<-- javascript --!>
const vm = new Vue({
  el: '#app',
  data: {
      text: 'Vue'
  }
});

模板经过编译后生成的ast 内容如下:

{
attrsList: []
attrsMap: {class: "app-wrap"}
children: [{
  attrs: [{}],
  attrsList: (2) [{}, {}],
  attrsMap: {
    type: "text",
    v-model: "text"
  },
  directives: [{
    arg: null,
    end: 98,
    isDynamicArg: false,
    modifiers: undefined,
    name: "model",
    rawName: "v-model",
    start: 84,
    value: "text"
  }],
    hasBindings: true
    plain: false
    start: 65
    static: false
    staticRoot: false
    tag: "input"
    type: 1
  },
  {type: 3, text: " ", start: 99, end: 106, static: true},
  {type: 1, tag: "div", attrsList: Array(0), attrsMap: {}, rawAttrsMap: {}, }
  ],
  parent: undefined
  plain: false
  rawAttrsMap: {class: {}}
  start: 0
  static: false
  staticClass: ""app-wrap""
  staticRoot: false
  tag: "div"
  type: 1
}

调用 compiler/codegen/index.js 中的 genDirectives 处理指令

generate -> genElement -> genData -> genDirectives

genDirectives 函数会在state.directives 中根据指令的名称去查找系统内部定义的策略,如果存在就用相应的策略去处理指令。代码实际体现如下:

const gen: DirectiveFunction = state.directives[dir.name]

// dir.name === 'model' --> 策略模式, 定义在 src/platforms/web/compiler/directives/* 的相关处理策略

// 当 dir.name === model --> src/platforms/web/compiler/directives/model.js

// 如果是 tag = input && type = text | textarea 就会给该元素添加一个 change(modifier = lazy) | input事件

经过codegen阶段由generate函数处理生成最后的render函数如下

"with(this) {
  return _c(
    'div',
    {staticClass:"app-wrap"},
    [      _c(        'input',        {          directives:[{            name:"model",            rawName:"v-model",            value:(text),            expression:"text"          }],
          attrs: {"type":"text"},
          domProps:{"value":(text)},
          on:{
            "input": function($event) {
              if($event.target.composing)
                return;
              text=$event.target.value
            }
          }
        }
      ),
      _v(" "),
      _c(
        'div',
        {staticClass:"slot"},
        [_v("v-model:"+_s(text))]
      )
    ]
  )
}"

在这个示例代码中,我们是给input 标签添加了v-model 的指令,所以最后相当于给input 标签 添加了一个value 属性,值为 v-model="key" 中的 key, 并给input 添加了一个change (v-model.lazy) 或input 事件监听事件变化。

当Vue 执行render 并挂载真实DOM的时候会给DOM绑定相应的属性和事件。

当元素在被创建的时候,会将定义在元素上的 钩子通过 src/core/vdom/patch.js 下的invokeCreateHooks函数执行 定义在platforms/runtime/directives/model.js下的钩子。