• 11.2 组件使用v-model

    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本质上还是一个子父组件通信的语法糖。