11.2 组件使用v-model

最后我们简单说说在父组件中使用v-model,可以先看结论,组件上使用v-model本质上是子父组件通信的语法糖。先看一个简单的使用例子。

  1. var child = {
  2. template: '<div><input type="text" :value="value" @input="emitEvent">{{value}}</div>',
  3. methods: {
  4. emitEvent(e) {
  5. this.$emit('input', e.target.value)
  6. }
  7. },
  8. props: ['value']
  9. }
  10. new Vue({
  11. data() {
  12. return {
  13. message: 'test'
  14. }
  15. },
  16. components: {
  17. child
  18. },
  19. template: '<div id="app"><child v-model="message"></child></div>',
  20. el: '#app'
  21. })

父组件上使用v-model, 子组件默认会利用名为 valueprop 和名为 input 的事件,当然像select表单会以其他默认事件的形式存在。分析源码的过程也大致类似,这里只列举几个特别的地方。

AST生成阶段和普通表单控件的区别在于,当遇到child时,由于不是普通的html标签,会执行getComponentModel的过程,而getComponentModel的结果是在AST树上添加model的属性。

  1. function model() {
  2. if (!config.isReservedTag(tag)) {
  3. genComponentModel(el, value, modifiers);
  4. }
  5. }
  6. function genComponentModel (el,value,modifiers) {
  7. var ref = modifiers || {};
  8. var number = ref.number;
  9. var trim = ref.trim;
  10. var baseValueExpression = '$$v';
  11. var valueExpression = baseValueExpression;
  12. if (trim) {
  13. valueExpression =
  14. "(typeof " + baseValueExpression + " === 'string'" +
  15. "? " + baseValueExpression + ".trim()" +
  16. ": " + baseValueExpression + ")";
  17. }
  18. if (number) {
  19. valueExpression = "_n(" + valueExpression + ")";
  20. }
  21. var assignment = genAssignmentCode(value, valueExpression);
  22. // 在ast树上添加model属性,其中有value,expression,callback属性
  23. el.model = {
  24. value: ("(" + value + ")"),
  25. expression: JSON.stringify(value),
  26. callback: ("function (" + baseValueExpression + ") {" + assignment + "}")
  27. };
  28. }

最终AST树的结果:

  1. {
  2. model: {
  3. callback: "function ($$v) {message=$$v}"
  4. expression: ""message""
  5. value: "(message)"
  6. }
  7. }

经过对AST树的处理后,回到genData$2的流程,由于有了model属性,父组件拼接的字符串会做进一步处理。

  1. function genData$2 (el, state) {
  2. var data = '{';
  3. var dirs = genDirectives(el, state);
  4. ···
  5. // v-model组件的render函数处理
  6. if (el.model) {
  7. data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
  8. }
  9. ···
  10. return data
  11. }

因此,父组件最终的render函数表现为:

  1. "_c('child',{model:{value:(message),callback:function ($$v) {message=$$v},expression:"message"}})"

子组件的创建阶段照例会执行createComponent,其中针对model的逻辑需要特别说明。

  1. function createComponent() {
  2. // transform component v-model data into props & events
  3. if (isDef(data.model)) {
  4. // 处理父组件的v-model指令对象
  5. transformModel(Ctor.options, data);
  6. }
  7. }
  1. function transformModel (options, data) {
  2. // prop默认取的是value,除非配置上有model的选项
  3. var prop = (options.model && options.model.prop) || 'value';
  4. // event默认取的是input,除非配置上有model的选项
  5. var event = (options.model && options.model.event) || 'input'
  6. // vnode上新增props的属性,值为value
  7. ;(data.attrs || (data.attrs = {}))[prop] = data.model.value;
  8. // vnode上新增on属性,标记事件
  9. var on = data.on || (data.on = {});
  10. var existing = on[event];
  11. var callback = data.model.callback;
  12. if (isDef(existing)) {
  13. if (
  14. Array.isArray(existing)
  15. ? existing.indexOf(callback) === -1
  16. : existing !== callback
  17. ) {
  18. on[event] = [callback].concat(existing);
  19. }
  20. } else {
  21. on[event] = callback;
  22. }
  23. }

transformModel的逻辑可以看出,子组件vnode会为data.props 添加 data.model.value,并且给data.on 添加data.model.callback。因此父组件v-model语法糖本质上可以修改为'<child :value="message" @input="function(e){message = e}"></child>'

显然,这种写法就是事件通信的写法,这个过程又回到对事件指令的分析过程了。因此我们可以很明显的意识到,组件使用v-model本质上还是一个子父组件通信的语法糖。