数据驱动 DOM,DOM 的变化也会影响数据 input 中 v-model 的原理 input 中的 v-model 如何使用 源码 组件 中 v-model 的原理 组件中的 v-model 如何使用 源码 v-model 的本质 对于 input,v-model="account" 是一个语法糖,通过设置一个 :value="account",并设置一…

数据驱动 DOM,DOM 的变化也会影响数据

input 中 v-model 的原理

  • input 中的 v-model 如何使用
let vue = new Vue({
  el: "#app",
  template: `<div><input v-model="account" placeholder="placeholder">{{ account }}</div>`,
  data() {
    return {
      account: ""
    };
  }
});
  • 源码
// 遍历el.directives,获取每一个指令对应的方法,同时根据指令生成一些data数据
// src/compiler/codegen/index.js
function genDirectives(el: ASTElement, state: CodegenState): string | void {
  const dirs = el.directives;
  if (!dirs) return;
  let res = "directives:[";
  let hasRuntime = false;
  let i, l, dir, needRuntime;
  // 遍历el.directives
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i];
    needRuntime = true;
    // 获取每一个指令对应的方法
    const gen: DirectiveFunction = state.directives[dir.name];
    if (gen) {
      // 根据ast元素节点的不同去执行model函数
      needRuntime = !!gen(el, dir, state.warn);
    }
    // 根据指令生成一些data数据
    if (needRuntime) {
      hasRuntime = true;
      res += `{name:"${dir.name}",rawName:"${dir.rawName}"${dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ""}${dir.arg ? `,arg:"${dir.arg}"` : ""}${dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ""}},`;
    }
  }
  if (hasRuntime) {
    return res.slice(0, -1) + "]";
  }
}
// 遍历el.directives的时候需要的参数
// src/platforms/web/compiler/options.js
export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules,
  directives,
  isPreTag,
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
};
// v-model对应的directive,根据ast元素节点的不同去执行model函数
// src/platforms/web/compiler/directives/model.js
export default function model(el: ASTElement, dir: ASTDirective, _warn: Function): ?boolean {
  warn = _warn;
  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") {
    // input用v-model是执行到这
    genDefaultModel(el, value, modifiers);
  } else if (!config.isReservedTag(tag)) {
    // 根据v-model的修饰符做不同的处理,修饰符:lazy,number,trim
    // 处理完成,生成代码
    // 组件中用的v-model是执行到这块
    genComponentModel(el, value, modifiers);
    // component v-model doesn't need extra runtime
    return false;
  } else if (process.env.NODE_ENV !== "production") {
    // ...
  }
  // ensure runtime directive metadata
  return true;
}
// 根据v-model的修饰符做不同的处理,修饰符:lazy,number,trim
// 处理完成,生成代码
// src/platforms/web/compiler/directives/model.js
function genDefaultModel(el: ASTElement, value: string, modifiers: ?ASTModifiers): ?boolean {
  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})`;
  }
  // 处理完成,生成代码
  // code是account=$event.target.value
  let code = genAssignmentCode(value, valueExpression);
  // 又因为v-model的修饰符没有lazy
  // code是if($event.target.composing)return;account=$event.target.value
  if (needCompositionGuard) {
    code = `if($event.target.composing)return;${code}`;
  }
  // 修改ast元素,给el添加一个prop,prop的值是account,又添加了一个自定义事件,自定义事件是input
  // 最终为:<input v-bind:value="account" v-on:input="account=$event.target.value">
  // 对于input来说,有一个prop是account,当触发@input事件,就重写account,这个account的值就是input的value
  addProp(el, "value", `(${value})`);
  addHandler(el, event, code, null, true);
  if (trim || number) {
    addHandler(el, "blur", "$forceUpdate()");
  }
}
// 对v-model的value进行解析,当前例子中,v-model=account res.key为null,所以${value}=${assignment}
// src/compiler/directives/model.js
export function genAssignmentCode(value: string, assignment: string): string {
  const res = parseModel(value);
  if (res.key === null) {
    return `${value}=${assignment}`;
  } else {
    return `$set(${res.exp}, ${res.key}, ${assignment})`;
  }
}
// 最终生成的render伪代码是
with (this) {
  return _c("div", [
    _c("input", {
      directives: [
        {
          name: "model",
          rawName: "v-model",
          value: account,
          expression: "account"
        }
      ],
      attrs: { placeholder: "placeholder" },
      domProps: { value: account },
      on: {
        input: function($event) {
          if ($event.target.composing) return;
          account = $event.target.value;
        }
      }
    }),
    _s(account)
  ]);
}

组件 中 v-model 的原理

  • 组件中的 v-model 如何使用
let Child = {
  template: `<div><input :value="value" @input="updateValue" placeholder="placeholder"></div>`,
  props: ["value"],
  methods: {
    updateValue(e) {
      this.$emit("input", e.target.value);
    }
  }
};

let vue = new Vue({
  el: "#app",
  template: `<div>'<child v-model="account"></child>{{ account }}</div>`,
  data() {
    return {
      account: ""
    };
  },
  components: {
    Child
  }
});
  • 源码
// 生成伪代码
// src/compiler/directives/model.js
export function genComponentModel(el: ASTElement, value: string, modifiers: ?ASTModifiers): ?boolean {
  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: `"${value}"`,
    callback: `function (${baseValueExpression}) {${assignment}}`
  };
}
// 最终生成的render伪代码是
with (this) {
  return _c(
    "div",
    [
      _c("child", {
        model: {
          value: account,
          callback: function($$v) {
            account = $$v;
          },
          expression: "account"
        }
      }),
       _s(account))
    ],
    1
  );
}
// 创建子组件vnode阶段,会执行createComponent函数,给data.props添加参数data.model.value,给data.on添加data.model.callback
// src/core/vdom/create-component.js
export function createComponent(Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string): VNode | Array<VNode> | void {
  // ...
  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data);
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag);
  // ...
  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on;
  // ...
  const vnode = new VNode(`vue-component-${Ctor.cid}${name ? `-${name}` : ""}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory);

  return vnode;
}
// 最终为
data.props = {
  value: account
};
data.on = {
  input: function($$v) {
    account = $$v;
  }
};
// 可见,子组件传入的value,绑定在父组件的account中,
// 父组件同时又监听自定义事件input变化,子组件input变化会派发事件,父组件account就会重新赋值,子到父
// 如果父组件account变化,传入子组件的value也会变化,value变化,触发子组件的input事件,所以又子组件派发事件,父组件的account又变化,父到子
// 父组件通过prop把数据传入子组件,子组件修改数据,通过$emit通知父组件
let vue = new Vue({
  el: "#app",
  template: `<div><child :value="account" @input="account=arguments[0]"></child> {{ account }} </div>`,
  data() {
    return {
      account: ""
    };
  },
  components: {
    Child
  }
});

v-model 的本质

  1. 对于 input,v-model="account" 是一个语法糖,通过设置一个 :value="account",并设置一个 input 事件,在触发 input 的时候,(:value="account" @input="account=\$event.target.value"),重写 account="$event.target.value",所以 account 变,dom 变,dom 变,account 变。
  2. 对于组件来说,v-model="account" 也是一个语法糖,子组件接收一个 :value="value",并设置一个 input 事件,在触发 input 事件的时候,$emit一个事件,传入$event.target.value,(:value="value" @input="this.$emit("input", e.target.value)"),父组件在监听$emit 的事件的时候,会把传入的值赋给一个 account,account 在父组件定义,account 又作为 v-bind 传入子组件,(:value="account" @input="account=arguments[0]"),所以子变->父变 父变->子变

暂无评论