你真的了解 v-model 吗?

1,511 阅读5分钟

众所周知,v-model 是 Vue.js 中实现的一个语法糖,和 Vue.js 中推崇的单向数据流表现不一致,用于实现所谓的双向绑定。

但看似简单的 v-model 具体是怎么做到双向绑定的,为了满足下好奇心,不得不深入到源码中看一看。

v-model 的使用情景分为两种:直接用到 inputtextarea 等输入控件中;用于自定义组件中。之所以分为这两类是因为它们在 Vue 源码中的实现的有差异的。

在输入控件中使用 v-model

<div id="app">
    <input type="text" v-model="aa" >
</div>
<script>
export default {
    data() {
        return {
            aa: 'hello'
        }
    }
}
</script>

写一个如上的单页面组件,首先会发生什么?

如果我们用的是不带编译器版本的 Vue , 那么 vue-loader 会将其这个文件编译为 一个 render 函数,结果如下:

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

着重看 input 被编译出来的结果:

 [_c('input',
 	{directives:[{name:"model",rawName:"v-model",value:(aa),expression:"aa"}],
     attrs:{"type":"text"},domProps:{"value":(aa)},
     on:{"input":function($event){if($event.target.composing)return;aa=$event.target.value}}})]

从上面就可以大概看出所谓语法糖的由来了,首先在为 input 设置了名为 value 的属性,再在 on 中写了一个 input 事件。看上去也很简单,但这些属性和事件又是怎么和真实 DOM 挂上钩的?从下面几个点分析:

  • 组件挂载的具体逻辑在源码中的 “src\core\vdom\patch.js”,然后我们重点看其中的 createEl 函数,它负责将 VNode 转为真实 DOM,节点赋值和事件监听都发生在这个过程中。其中的关键就是它调用了 invokeCreateHooks 函数,这个函数负责调用组件 create 之前的所有钩子函数(不是生命周期钩子函数),具体逻辑如下:

    // 执行平台相关的 DOM 属性操作以及事件监听操作
    for (let i = 0; i < cbs.create.length; ++i) {
      cbs.create[i](emptyNode, vnode)
    }
    

    可能你又要问这个 cbs.create 是哪来的?由于不同平台下 DOM 的有关操作差异很大,为了实现跨平台, Vue 将这些操作都单独封装起来。如果要看 web 平台相关的详细逻辑可以去 “src\platforms\web\runtime\modules\index.js” 中查找。今天我们只看 domProps 和事件监听的处理:

    // src\platforms\web\runtime\modules\dom-props.js
    export default {
      create: updateDOMProps,
      update: updateDOMProps
    }
    

    可以看到这个文件输出了一个对象,这个对象又有一个 create 属性, invokeCreateHooks 函数中调用的函数正是这个属性的值 updateDOMPropsupdateDOMProps 负责处理 innerTextinnerHTML, value 等逻辑。

    // src\platforms\web\runtime\modules\events.js
    export default {
      create: updateDOMListeners,
      update: updateDOMListeners
    }
    

    同样的,事件监听的逻辑也类似。updateDOMListeners 根据 VNode.data.on 中的逻辑处理事件监听器的更新,挂载和删除。除此之外,directives 中编译出的 v-model 实现方式与前面一致,Vue 中有对 v-model 这个执行做特殊处理,解决了一些浏览器兼容性上的问题,以及不同输入控件的兼容问题,有兴趣的小伙伴可以看看源码中 “src\platforms\web\runtime\directives\model.js” 的处理。

在自定义组件中使用 v-model

<div id="app">
    <compo v-model="aa" />
</div>
<script>
export default {
    data() {
        return {
            aa: 'hello'
        }
    }
}
</script>
// compo
  const compo = Vue.component('compo', {
    template: '<input :value="$attrs.value" @input="handleInput" />',
    methods: {
      handleInput(e) {
        this.$emit('input', e.target.value)
      }
    },
  })

compo 中就能清楚的看到用于两种情景下 v-model 的差异,需要在组件中的input元素上手动去绑定一个 @input 事件,并让其给父组件传递一个 input 事件。先看看这种情况下编译出来的结果:

(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('compo',{model:{value:(aa),callback:function (?v) {aa=?v},expression:"aa"}})],1)}
})
// compo 编译结果
[_c('compo',{model:{value:(aa),callback:function (?v) {aa=?v},expression:"aa"}})]

大家发现问题没有,组件中的 v-model 并没有编译出 input 事件,只是一个简单的 callback。那要如何在源码中找到响应处理呢?

组件由 render 函数变为 VNode 的主要逻辑在 “src\core\vdom\create-component.js” 中的 createComponent 函数,以后出现类似问题都可以从这个函数入手。这个函数中对组件的 model 属性有如下的特殊处理:

  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }
  
// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data: any) {
  const prop = (options.model && options.model.prop) || 'value'
  const event = (options.model && options.model.event) || 'input'
  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value
  const on = data.on || (data.on = {})
  const existing = on[event]
  const callback = data.model.callback
  if (isDef(existing)) {
    if (
      Array.isArray(existing)
        ? existing.indexOf(callback) === -1
        : existing !== callback
    ) {
      on[event] = [callback].concat(existing)
    }
  } else {
    on[event] = callback
  }
}

现在回头看 callback 是怎么和 input 事件挂钩是不是一目了然了,同时里面还有组件中 model 属性的配置,比如,不想要绑定 input 事件,要处理 change 事件,也是在这段源码中实现的。

所以,我们回头来总结下:

  • v-model 是什么?

    v-model 是 Vue 中内置的一个指令

  • 为什么使用 v-model

    它方便呀~~一个 v-model 搞定 value@input ,一句话解决的问题,绝不用两句

  • 怎么使用 v-model ?

    <input v-model="cc />"
    // 相当于
    <input :value="cc" @input="handleInput"
    <compo v-model="cc" />
    
  • v-model 是怎样实现的?

    v-model 在组件中和在 DOM 元素中的实现是不同的:在 DOM 元素中,编译器会根据真实 DOM 创建虚拟 DOM ,而虚拟 DOM 中会包含 domPropson 两个属性,然后将 domProps 中的 value 值赋给 DOM 做初始值,并为 DOM 的 input 事件绑定 on 中的 input 函数;在组件中,编译器会编译出 model 属性,model 属性包含了 value 值和 callback。而组件实例化时,会根据组件预定义的 model 配置将 model.value 传入组件的 prop ,并将 callback 作为组件自定义事件中 input 事件的回调 。