目标:1、了解v-model的本质。2、了解v-model的实现原理。
我们知道Vue的核心特性之一是双向绑定,vue的响应式原理是实现了数据->视图,接下来我们要学习 视图->数据的原理。
v-model是一个指令,限制在<input>、<select>、<textarea>、components中使用,修饰符.lazy(取代 input 监听 change 事件)、.number(输入字符串转为有效的数字)、.trim(输入首尾空格过滤)。它其实是一个语法糖,接下来我们就来分析 v-model 的实现原理。
为了更加直观,我们结合一个例子来分析:
v-model简单demo,在input标签上设置了v-model属性,绑定message编译-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)。
1、遍历 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) 。
2、获取到指令方法就执行。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)
genAssignmentCodegenAssignmentCode,根据参数描述去生成代码。该方法首先对 v-model 对应的 value 做了解析,它处理了非常多的情况,对我们的例子,value 就是 messgae,所以返回的 res.key 为 null,然后我们就得到 ${value}=${assignment},也就是 message=$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,并给其添加了事件处理其实就是动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message 设置为目标值,这样实际上就完成了数据双向绑定了,所以说 v-model 实际上就是语法糖。
再回到 genDirectives,它接下来的逻辑就是根据指令生成一些 data 的代码:
生成data代码对我们的例子而言,最终生成的 render 代码如下:
最终生成的 render 代码关于事件的处理我们之前的章节已经分析过了,所以对于 input 的 v-model 而言,完全就是语法糖,并且对于其它表单元素套路都是一样,区别在于生成的事件代码会略有不同。
v-model 除了作用在表单元素上,新版的 Vue 还把这一语法糖用在了组件上,接下来我们来分析它的实现。
组件:
为了更加直观,我们也是通过一个例子分析:
父组件引用child子组件的地方使用了v-model关联了数据message。而子组件定义了一个value的prop,并且在input事件的回调函数中,通过this.$emit('input', e.target.value)派发了一个事件可以看到,父组件引用 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 函数,并命中如下逻辑:
model 函数中命中的逻辑genComponentModel 函数定义在 src/compiler/directives/model.js :
genComponentModel函数genComponentModel 的逻辑很简单,对我们的例子而言,生成的 el.model 的值为:
生成的 el.model那么在 genDirectives 之后,genData 函数中有一段逻辑如下:
genData那么父组件最终生成的 render 代码如下:
render 代码然后在创建子组件 vnode 阶段,会执行 createComponent 函数,它的定义在 src/core/vdom/create-component.js 中:
createComponent其中会对 data.model 的情况做处理,执行 transformModel(Ctor.options, data) 方法:
transformModeltransformModel 逻辑很简单,给 data.props 添加 data.model.value,并且给data.on 添加 data.model.callback,对我们的例子而言,扩展结果如下:
transformModel结果其实就相当于我们在这样编写父组件:
相当于这样子组件传递的 value 绑定到当前父组件的 message,同时监听自定义 input 事件,当子组件派发 input 事件的时候,父组件会在事件回调函数中修改 message 的值,同时 value 也会发生变化,子组件的 input 值被更新。
这就是典型的 Vue 的父子组件通讯模式,父组件通过 prop 把数据传递到子组件,子组件修改了数据后把改变通过 $emit 事件的方式通知父组件,所以说组件上的 v-model 也是一种语法糖。
另外我们注意到组件 v-model 的实现,子组件的 value prop 以及派发的 input 事件名是可配的,可以看到 transformModel 中对这部分的处理:
子组件的 value prop 以及派发的 input 事件名是可配的也就是说可以在定义子组件的时候通过 model 选项配置子组件接收的 prop 名以及派发的事件名,举个例子:
子组件修改了接收的 prop 名以及派发的事件名,然而这一切父组件作为调用方是不用关心的,这样做的好处是我们可以把 value 这个 prop 作为其它的用途。总结
那么至此,v-model 的实现就分析完了,我们了解到它是 Vue 双向绑定的真正实现,但本质上就是一种语法糖,它即可以支持原生表单元素,也可以支持自定义组件。在组件的实现中,我们是可以配置子组件接收的 prop 名称,以及派发的事件名称。
网友评论