# v-model 的浅析

事件相关请参阅这里

# 编译阶段

在编辑阶段中的codegen阶段,会调用genDirectives方法,部分定义如下:

function genDirectives(el, state) {
  const dirs = el.directives;
  if (!dirs) return;
  let res = 'directives:[';
  let hasRuntime = false;
  let i, l, dir, needRuntime;
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i];
    needRuntime = true;
    const gen: DirectiveFunction = state.directives[dir.name];
    if (gen) {
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.
      needRuntime = !!gen(el, dir, state.warn);
    }
    //....
  }
}

这里通过state.directives[dir.name]获取到当前指令的配置方法,并尝试执行。这里会获取到v-model的内置实现方法并执行,这段代码在src/platforms/web/compiler/directives/model.js内,定义如下:

function model(el, dir) {
  const value = dir.value;
  const modifiers = dir.modifiers;
  const tag = el.tag;
  const type = el.attrsMap.type;
  if (el.component) {
    genComponentModel(el, value, modifiers);
    // component v-model doesn't need extra runtime
    return false;
  } else if (tag === 'select') {
    genSelect(el, value, modifiers);
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers);
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers);
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers);
  } else if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers);
    // component v-model doesn't need extra runtime
    return false;
  }
}

可以看到这里针对不同的标签和组件都有不同的v-model实现方式。我们看一个默认的实现方式genDefaultModel

function genDefaultModel(el, value) {
  const type = el.attrsMap.type;

  const { lazy, number, trim } = modifiers || {};
  const needCompositionGuard = !lazy && type !== 'range';
  const event = lazy ? 'change' : type === 'range' ? RANGE_TOKEN : 'input';

  let valueExpression = '$event.target.value';
  if (trim) {
    valueExpression = `$event.target.value.trim()`;
  }
  if (number) {
    valueExpression = `_n(${valueExpression})`;
  }

  let code = genAssignmentCode(value, valueExpression);
  if (needCompositionGuard) {
    code = `if($event.target.composing)return;${code}`;
  }

  addProp(el, 'value', `(${value})`);
  addHandler(el, event, code, null, true);
  if (trim || number) {
    addHandler(el, 'blur', '$forceUpdate()');
  }
}

这个方法的核心是给el.propsel.events添加了 2 个属性,在后续处理propsevents的时候一并进行处理。

genAssignmentCode核心逻辑是根据v-model绑定的value值的不同写法,去决定是直接赋值,还是使用$set去实现数据更新。用一个例子来说明这个产出的代码

<input v-model="test" />

在经过codegen处理后的代码大概如下:

function render() {
  with (this) {
    return _c('input', {
      //...
      domProps: {
        value: test,
      },
      on: {
        input: function ($event) {
          if ($event.target.composing) return;
          test = $event.target.value;
        },
      },
    });
  }
}

这里能看出,v-model在普通元素上其实就是添加了对应的属性和事件来实现所谓的双向绑定效果。

接着看下在v-mode组件上的处理

function genComponentModel(el, value, modifiers) {
  const { number, trim } = modifiers || {};

  const baseValueExpression = '$$v';
  let valueExpression = baseValueExpression;
  if (trim) {
    valueExpression =
      `(typeof ${baseValueExpression} === 'string'` +
      `? ${baseValueExpression}.trim()` +
      `: ${baseValueExpression})`;
  }
  if (number) {
    valueExpression = `_n(${valueExpression})`;
  }
  const assignment = genAssignmentCode(value, valueExpression);

  el.model = {
    value: `(${value})`,
    expression: JSON.stringify(value),
    callback: `function (${baseValueExpression}) {${assignment}}`,
  };
}

这里比较简单,就是对el.model进行了一个初始化

# 挂载阶段

普通元素其实就是一个事件的挂载,不再做阐述。

组件元素,在元素组件的vnode创建阶段,会有这样一段代码在src/core/vdom/create-component.js

function createComponent(){
    //...
    if (isDef(data.model)) {
        transformModel(Ctor.options, data)
    }
    //
}
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
  }
}

可以看到,核心是调用了transformModel方法来处理组件的v-model指令,将el.model中的callback添加到当前vnodeon属性中,就是组件的事件对象中,通过组件事件来实现v-model的实现。

# 总结

  • v-model本质上是事件的语法糖
  • 普通元素的实现,是在codegen阶段,通过内置的指令解析方法,将v-model对应的代码生成对应的原生事件代码
  • 组件的事件,codegen阶段,通过内置的指令方法将解析出model对象字符串拼接到render函数中。在组件的vnode生成阶段,解析model对象,并将对应的事件添加到vnode的事件对象中去