目标:1、了解v-model的本质。2、了解v-model的实现原理。
我们知道Vue的核心特性之一是双向绑定,vue的响应式原理是实现了数据->视图,接下来我们要学习 视图->数据的原理。
·v-model
是一个指令,限制在<input>、<select>、<textarea>、components
中使用,修饰符.lazy
(取代 input
监听change
事件)、.number
(输入字符串转为有效的数字)、.trim
(输入首尾空格过滤)。它其实是一个语法糖,接下来我们就来分析v-model
的实现原理。
为了更加直观,我们结合一个例子来分析:
编译-parser:
创建AST节点后会对节点做处理,其中对属性的处理会执行到processAttrs(el)
。其中会遍历节点上的attrsList
,拿到name
属性,先判断name
是否匹配模版指令的正则表达式(比如v-,:
),如果匹配到,给节点的hasBindings
属性设为true
(标志是动态节点)。
然后通过parseModifiers(name)
取到属性描述符 对象modifers
。接下来会对指令进行判断命中'v-bind'?'v-on'?'v-model'
。我们命中'v-model'
,把'v-model'
去掉,接着执addDirective(el,name,rawName,value,arg,modifiers)
,把我们传的参数添加到el.directives
数组中。我们把v-model
相关参数传入到el.directives
中,为后续codegen
准备。
编译-codegen:
genData函数中会执行const dirs = genDirectives(el, state)(src/compiler/codegen/index.js)。
-
遍历
el.directives
,获取每一个指令对应的方法。首先他会拿前面的directives属性,如果存在,会开始为之后拼接做准备,拼接res = ‘directives:[’。我们遍历directives,const gen = state.directives[dir.name],state
是codegenState
类(compiler/codegen/index.js)的一个实例,和编译息息相关,其中有一个directives实例对象,是由baseDirectives
和options.directives
做合并。options是和编译相关的配置(和编译平台有关)。state.directives
最终拿到的是一个[model,text,html]
,一个由三个对象组成的数组。在我们这里model对应一个model函数(platform/web/compiler/directives/model.js)。如果拿到了model函数,我们会执行这个model函数,needRuntime = !!gen(el, dir, state.warn)
。 -
获取到指令方法就执行。model(ast节点,directives)函数,首先去取得value值,modifers修饰符,标签名等。接来下,根据tag(input)和type(textarea)做判断执行不同的逻辑,在我们的例子中会命中
genDefaultModel(el, value, modifiers)
。genDefaultModel
方法,通过modifiers取到修饰符,根据修饰符的不同,影响event和valueExpression的值。对于我们的例子,event
为input
,valueExpression
为$event.target.value
。然后去执行 genAssignmentCode 去生成代码(src/compiler/directives/model.js)
genAssignmentCode,根据参数描述去生成代码。该方法首先对 v-model 对应的 value 做了解析,它处理了非常多的情况,对我们的例子,value 就是 messgae,所以返回的 res.key 为 null,然后我们就得到 value={value}=value={assignment},也就是 message=event.target.value。然后我们又命中了needCompositionGuard为true的逻辑,所以最终的code为if(event.target.value。然后我们又命中了 needCompositionGuard 为 true 的逻辑,所以最终的code为if(event.target.value。然后我们又命中了needCompositionGuard为true的逻辑,所以最终的code为if(event.target.composing)return;message=$event.target.value。
code 生成完后,又执行了 2 句非常关键的代码:
这实际上就是 input
实现v-model
的精髓,通过修改 AST 元素,给 el 添加一个 prop,相当于我们在 input 上动态绑定了 value,又给 el 添加了事件处理,相当于在 input 上绑定了 input 事件,其实转换成模板如下:
其实就是动态绑定了 input
的 value
指向了 messgae
变量,并且在触发input
事件的时候去动态把message
设置为目标值,这样实际上就完成了数据双向绑定了,所以说v-model
实际上就是语法糖。
再回到 genDirectives
,它接下来的逻辑就是根据指令生成一些 data 的代码:在这里插入图片描述
对我们的例子而言,最终生成的 render 代码如下:
v-model 除了作用在表单元素上,新版的 Vue 还把这一语法糖用在了组件上,接下来我们来分析它的实现。
组件:
为了更加直观,我们也是通过一个例子分析:
可以看到,父组件引用 child
子组件的地方使用了 v-model
关联了数据 message
;而子组件定义了一个value
的 prop
,并且在 input
事件的回调函数中,通过this.$emit('input', e.target.value)
派发了一个事件,为了让 v-model
生效,这两点是必须的。
接着我们从源码角度分析实现原理,还是从编译阶段说起,对于父组件而言,在编译阶段会解析v-modle
指令,依然会执行 genData
函数中的 genDirectives
函数,接着执行 src/platforms/web/compiler/directives/model.js 中定义的 model 函数,并命中如下逻辑:
genComponentModel
函数定义在 src/compiler/directives/model.js :
genComponentModel
的逻辑很简单,对我们的例子而言,生成的 el.model 的值为:
那么在 genDirectives
之后,genData 函数中有一段逻辑如下:
那么父组件最终生成的 render 代码如下:
然后在创建子组件 vnode
阶段,会执行 createComponent
函数,它的定义在 src/core/vdom/create-component.js 中:
其中会对 data.model
的情况做处理,执行 transformModel(Ctor.options, data)
方法:
transformModel
逻辑很简单,给 data.props
添加 data.model.value
,并且给data.on
添加 data.model.callback
,对我们的例子而言,扩展结果如下:
其实就相当于我们在这样编写父组件:
子组件传递的 value
绑定到当前父组件的 message
,同时监听自定义 input
事件,当子组件派发input
事件的时候,父组件会在事件回调函数中修改 message
的值,同时 value
也会发生变化,子组件的 input
值被更新。
这就是典型的 Vue
的父子组件通讯模式,父组件通过 prop
把数据传递到子组件,子组件修改了数据后把改变通过 $emit
事件的方式通知父组件,所以说组件上的 v-model
也是一种语法糖。
另外我们注意到组件 v-model
的实现,子组件的 value prop
以及派发的 input
事件名是可配的,可以看到 transformModel
中对这部分的处理:
也就是说可以在定义子组件的时候通过 model
选项配置子组件接收的 prop
名以及派发的事件名,举个例子:
总结
那么至此,v-model 的实现就分析完了,我们了解到它是 Vue 双向绑定的真正实现,但本质上就是一种语法糖,它即可以支持原生表单元素,也可以支持自定义组件。在组件的实现中,我们是可以配置子组件接收的 prop 名称,以及派发的事件名称。