众所周知,v-model 是 Vue.js 中实现的一个语法糖,和 Vue.js 中推崇的单向数据流表现不一致,用于实现所谓的双向绑定。
但看似简单的 v-model 具体是怎么做到双向绑定的,为了满足下好奇心,不得不深入到源码中看一看。
v-model 的使用情景分为两种:直接用到 input 或 textarea 等输入控件中;用于自定义组件中。之所以分为这两类是因为它们在 Vue 源码中的实现的有差异的。
在输入控件中使用 v-model[HTML] 纯文本查看 复制代码 <div id="app">
<input type="text" v-model="aa" >
</div>
<script>
export default {
data() {
return {
aa: 'hello'
}
}
}
</script>
写一个如上的单页面组件,首先会发生什么? 如果我们用的是不带编译器版本的 Vue , 那么 vue-loader 会将其这个文件编译为 一个 render 函数,结果如下: [JavaScript] 纯文本查看 复制代码 (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 被编译出来的结果: [JavaScript] 纯文本查看 复制代码 [_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 之前的所有钩子函数(不是生命周期钩子函数),具体逻辑如下:
[JavaScript] 纯文本查看 复制代码 // 执行平台相关的 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 和事件监听的处理:
[JavaScript] 纯文本查看 复制代码 // src\platforms\web\runtime\modules\dom-props.js
export default {
create: updateDOMProps,
update: updateDOMProps
}
可以看到这个文件输出了一个对象,这个对象又有一个 create 属性, invokeCreateHooks 函数中调用的函数正是这个属性的值 updateDOMProps。 updateDOMProps 负责处理 innerText, innerHTML, value 等逻辑。
[JavaScript] 纯文本查看 复制代码 // 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
[HTML] 纯文本查看 复制代码 <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 事件。先看看这种情况下编译出来的结果:
[JavaScript] 纯文本查看 复制代码 (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 属性有如下的特殊处理:
[JavaScript] 纯文本查看 复制代码 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 中会包含 domProps 和 on 两个属性,然后将 domProps 中的 value 值赋给 DOM 做初始值,并为 DOM 的 input 事件绑定 on 中的 input 函数;在组件中,编译器会编译出 model 属性,model 属性包含了 value 值和 callback。而组件实例化时,会根据组件预定义的 model 配置将 model.value 传入组件的 prop ,并将 callback 作为组件自定义事件中 input 事件的回调 。
|