vue中的v-model

210 阅读4分钟

高频面试题:v-model?

答案:v-model是语法糖。
节点或者组件的渲染都大底会经历通过编译进行render函数的获取、虚拟DOM的获取和视图渲染过程这三个主要流程。

一、input

new Vue({
  el: "#app",
  data() {
    return {
      msg: ""
    };
  },
  template: `<div>
    <input v-model="msg" placeholder="edit me">
    <p>msg is: {{ msg }}</p>
  </div>`
});

1、编译过程

(1)ast的获取

const ast = parse(template.trim(), options)的过程中,会通过正则的方式去将template转换成ast树,整个过程中如果遇到input闭合标签在ast树管理过程中会执行closeElement(element)中的element = processElement(element, options),其中有属性管理的方法processAttrs(element)

function processAttrs (el) { 
    // ...
    addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
    // ...
    addAttr(el, name, JSON.stringify(value), list[i])
    // ...
}
function addDirective (
  el: ASTElement,
  name: string,
  rawName: string,
  value: string,
  arg: ?string,
  isDynamicArg: boolean,
  modifiers: ?ASTModifiers,
  range?: Range
) {
  (el.directives || (el.directives = [])).push(rangeSetItem({
    name,
    rawName,
    value,
    arg,
    isDynamicArg,
    modifiers
  }, range))
  el.plain = false
}
function addAttr (el, name, value, range, dynamic) {
  var attrs = dynamic
    ? (el.dynamicAttrs || (el.dynamicAttrs = []))
    : (el.attrs || (el.attrs = []));
  attrs.push(rangeSetItem({ name: name, value: value, dynamic: dynamic }, range));
  el.plain = false;
}

在当前例子中,如果满足正则条件,会执行addDirective的方式处理其中的v-model="msg",处理结果为:

image.png

如果满足条件,会执行addAttr的方式处理其中的placeholder="edit me",处理结果为:

image.png

从上面通过debugger断点可以看出,当前的el上会多出directivesattrs两个属性。

(2)code码的生成

在生成code码的过程中会不断的调用genElement进行ast树的递归操作,其中会通过data = genData(el, state)的方式构建当前el上的属性:

export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'

  // directives first.
  // directives may mutate the el's other properties before they are generated.
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','

  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
  // module data generation functions
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  // attributes
  if (el.attrs) {
    data += `attrs:${genProps(el.attrs)},`
  }
  // DOM props
  if (el.props) {
    data += `domProps:${genProps(el.props)},`
  }
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true)},`
  }
  // slot target
  // only for non-scoped slots
  if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
  }
  // scoped slots
  if (el.scopedSlots) {
    data += `${genScopedSlots(el, el.scopedSlots, state)},`
  }
  // component v-model
  if (el.model) {
    data += `model:{value:${
      el.model.value
    },callback:${
      el.model.callback
    },expression:${
      el.model.expression
    }},`
  }
  // inline-template
  if (el.inlineTemplate) {
    const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {
      data += `${inlineTemplate},`
    }
  }
  data = data.replace(/,$/, '') + '}'
  // v-bind dynamic argument wrap
  // v-bind with dynamic arguments must be applied using the same v-bind object
  // merge helper so that class/style/mustUseProp attrs are handled correctly.
  if (el.dynamicAttrs) {
    data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
  }
  // v-bind data wrap
  if (el.wrapData) {
    data = el.wrapData(data)
  }
  // v-on data wrap
  if (el.wrapListeners) {
    data = el.wrapListeners(data)
  }
  return data
}

可以看出,会对el上各个节点属性进行处理,当执行到genDirectives的时候当var gen = state.directives[dir.name]存在时会执行gen(el, dir, state.warn),在当前例子中(tag === 'input'成立,进而执行genDefaultModel(el, value, modifiers)

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

  // warn if v-bind:value conflicts with v-model
  // except for inputs with v-bind:type
  if (process.env.NODE_ENV !== 'production') {
    var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'];
    var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];
    if (value$1 && !typeBinding) {
      var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value';
      warn$1(
        binding + "=\"" + value$1 + "\" conflicts with v-model on the same element " +
        'because the latter already expands to a value binding internally',
        el.rawAttrsMap[binding]
      );
    }
  }

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

  var valueExpression = '$event.target.value';
  if (trim) {
    valueExpression = "$event.target.value.trim()";
  }
  if (number) {
    valueExpression = "_n(" + valueExpression + ")";
  }

  var 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()');
  }
}

其中addProp(el, 'value', ("(" + value + ")"))addHandler(el, event, code, null, true)就是将v-model = msg转换成propevents的核心逻辑。
之后通过propevent的处理逻辑,处理el上在的propevents属性。
最后获得包含domPropsondata值为:

"{directives:[{name:"model",rawName:"v-model",value:(msg),expression:"msg"}],attrs:{"placeholder":"edit me"},domProps:{"value":(msg)},on:{"input":function($event){if($event.target.composing)return;msg=$event.target.value}}}"

2、vNode

获取到的vNode结果中,会有属性directivesonattrsdomProps

image.png

3、patch阶段

patch过程中,如果vNode中存在data属性,会执行到处理invokeCreateHooks的逻辑:

function invokeCreateHooks (vnode, insertedVnodeQueue) {
    for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
      cbs.create[i$1](emptyNode, vnode);
    }
    i = vnode.data.hook; // Reuse variable
    if (isDef(i)) {
      if (isDef(i.create)) { i.create(emptyNode, vnode); }
      if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
    }
  }

在执行的过程中,cbs.create中会包含updateAttrsupdateClassupdateAttrsupdateDOMListenersupdateDOMPropsupdateStyleupdateDirectives等钩子函数,当前例子中主要涉及到的方法如下所示:

image.png

执行完以后,如果vnode.data.hook中有insert钩子,会将其推入到insertedVnodeQueue数组中,在所有父子节点完成节点的创建以后,会在patch逻辑的最后执行invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)

function invokeInsertHook (vnode, queue, initial) {
    // delay insert hooks for component root nodes, invoke them after the
    // element is really inserted
    if (isTrue(initial) && isDef(vnode.parent)) {
      vnode.parent.data.pendingInsert = queue;
    } else {
      for (var i = 0; i < queue.length; ++i) {
        queue[i].data.hook.insert(queue[i]);
      }
    }
  }
function inserted (el, binding, vnode, oldVnode) {
    if (vnode.tag === 'select') {
      // #6903
      if (oldVnode.elm && !oldVnode.elm._vOptions) {
        mergeVNodeHook(vnode, 'postpatch', function () {
          directive.componentUpdated(el, binding, vnode);
        });
      } else {
        setSelected(el, binding, vnode.context);
      }
      el._vOptions = [].map.call(el.options, getValue);
    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
      el._vModifiers = binding.modifiers;
      if (!binding.modifiers.lazy) {
        el.addEventListener('compositionstart', onCompositionStart);
        el.addEventListener('compositionend', onCompositionEnd);
        // Safari < 10.2 & UIWebView doesn't fire compositionend when
        // switching focus before confirming composition choice
        // this also fixes the issue where some browsers e.g. iOS Chrome
        // fires "change" instead of "input" on autocomplete.
        el.addEventListener('change', onCompositionEnd);
        /* istanbul ignore if */
        if (isIE9) {
          el.vmodel = true;
        }
      }
    }
},

这里在input输入的时候,处理了compositionstartcompositionend,并且,在iOS Chrome浏览器中通过change替代input方法。

小结

input中的v-model,最终通过target.addEventListener处理成在节点上监听input事件function($event){msg=$event.target.value}}的形式,当input值发生变化时,msg也改变。

二、组件

const childCom = {
  template: `<div>
    <button @click="changeCount">数据修改</button>
  </div>`,
  data() {
    return {
      count: 1
    };
  },
  methods: {
    changeCount() {
      this.count++;
      this.$emit("input", this.count);
    }
  }
};
Vue.component("childCom", childCom);
new Vue({
  el: "#app",
  data() {
    return {
      msg: ""
    };
  },
  template: `<div>
    <childCom v-model="msg"></childCom>
    <p>{{msg}}</p>
  </div>`
});

1、编译过程

(1)ast的获取

和普通inputast获取过程一样,也会通过addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])的方式对其中的v-model属性进行处理,结果为:

image.png

(2)code码的生成

在对eldirectives进行处理的时候,会执行到genComponentModel(el, value, modifiers):

/**
 * Cross-platform code generation for component v-model
 */
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: JSON.stringify(value),
    callback: `function (${baseValueExpression}) {${assignment}}`
  }
}

在这里为el定义model,然后再执行:

// component v-model
  if (el.model) {
    data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
  }

执行完获取的结果为:

'{model:{value:(msg),callback:function ($$v) {msg=$$v},expression:"msg"}}'

2、vNode

在子组件vNode获取过程中,会执行createComponent,如果存在mode,会执行:

// transform component v-model data into props & events
if (isDef(data.model)) {
    transformModel(Ctor.options, data);
}
// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data) {
  var prop = (options.model && options.model.prop) || 'value';
  var event = (options.model && options.model.event) || 'input'
  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value;
  var on = data.on || (data.on = {});
  var existing = on[event];
  var 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;
  }
}

这里为data定义了attrson属性,执行结果如下:

image.png

然后又通过以下方式将data.on赋值给listeners作为组件事件,并将原生事件data.nativeOn赋值给data.on

// extract listeners, since these needs to be treated as child component listeners instead of DOM listeners
var listeners = data.on;
// replace with listeners with .native modifier so it gets processed during parent component patch.
data.on = data.nativeOn;

3、patch

在子组件patch的过程中,会执行到以下逻辑:

// createElm方法中的代码片段
{
    createChildren(vnode, children, insertedVnodeQueue);
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue);
    }
    insert(parentElm, vnode.elm, refElm);
}

// invokeCreateHooks方法
function invokeCreateHooks (vnode, insertedVnodeQueue) {
    for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
      cbs.create[i$1](emptyNode, vnode);
    }
    i = vnode.data.hook; // Reuse variable
    if (isDef(i)) {
      if (isDef(i.create)) { i.create(emptyNode, vnode); }
      if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
    }
}

cbs.create中有处理组件事件的方法updateDOMListeners (oldVnode, vnode)

function updateDOMListeners (oldVnode, vnode) {
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  var on = vnode.data.on || {};
  var oldOn = oldVnode.data.on || {};
  target$1 = vnode.elm;
  normalizeEvents(on);
  updateListeners(on, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context);
  target$1 = undefined;
}
function updateListeners (
  on,
  oldOn,
  add,
  remove$$1,
  createOnceHandler,
  vm
) {
  var name, def$$1, cur, old, event;
  for (name in on) {
    def$$1 = cur = on[name];
    old = oldOn[name];
    event = normalizeEvent(name);
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        "Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
        vm
      );
    } else if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur, vm);
      }
      if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture);
      }
      add(event.name, cur, event.capture, event.passive, event.params);
    } else if (cur !== old) {
      old.fns = cur;
      on[name] = old;
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name);
      remove$$1(event.name, oldOn[name], event.capture);
    }
  }
}
function add (
  name,
  handler,
  capture,
  passive
) {
  // async edge case #6566: inner click event triggers patch, event handler
  // attached to outer element during patch, and triggered again. This
  // happens because browsers fire microtask ticks between event propagation.
  // the solution is simple: we save the timestamp when a handler is attached,
  // and the handler would only fire if the event passed to it was fired
  // AFTER it was attached.
  if (useMicrotaskFix) {
    var attachedTimestamp = currentFlushTimestamp;
    var original = handler;
    handler = original._wrapper = function (e) {
      if (
        // no bubbling, should always fire.
        // this is just a safety net in case event.timeStamp is unreliable in
        // certain weird environments...
        e.target === e.currentTarget ||
        // event is fired after handler attachment
        e.timeStamp >= attachedTimestamp ||
        // bail for environments that have buggy event.timeStamp implementations
        // #9462 iOS 9 bug: event.timeStamp is 0 after history.pushState
        // #9681 QtWebEngine event.timeStamp is negative value
        e.timeStamp <= 0 ||
        // #9448 bail if event is fired in another document in a multi-page
        // electron/nw.js app, since event.timeStamp will be using a different
        // starting reference
        e.target.ownerDocument !== document
      ) {
        return original.apply(this, arguments)
      }
    };
  }
  target.addEventListener(
    name,
    handler,
    supportsPassive
      ? { capture: capture, passive: passive }
      : capture
  );
}

最终可以看出,当前节点会通过原生方法target.addEventListener进行事件的绑定nameclick,其中的handler是经过处理的changeCount事件:

changeCount() {
  this.count++;
  this.$emit("input", this.count);
}

4、callback执行逻辑

(1)在子组件事件实例化过程中initEvents

function initEvents (vm) {
  vm._events = Object.create(null);
  vm._hasHookEvent = false;
  // init parent attached events
  var listeners = vm.$options._parentListeners;
  if (listeners) {
    updateComponentListeners(vm, listeners);
  }
}
function updateComponentListeners (
  vm,
  listeners,
  oldListeners
) {
  target = vm;
  updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);
  target = undefined;
}
function updateListeners (
  on,
  oldOn,
  add,
  remove$$1,
  createOnceHandler,
  vm
) {
  var name, def$$1, cur, old, event;
  for (name in on) {
    def$$1 = cur = on[name];
    old = oldOn[name];
    event = normalizeEvent(name);
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        "Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
        vm
      );
    } else if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur, vm);
      }
      if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture);
      }
      add(event.name, cur, event.capture, event.passive, event.params);
    } else if (cur !== old) {
      old.fns = cur;
      on[name] = old;
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name);
      remove$$1(event.name, oldOn[name], event.capture);
    }
  }
}
function add (event, fn) {
  target.$on(event, fn);
}

add的过程中,会触发$on方法:

Vue.prototype.$on = function (event, fn) {
    var vm = this;
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn);
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn);
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true;
      }
    }
    return vm
  };

vm._events[event]不存在的时候,会将其置为空数组,并且将当前处理后的事件fn推入其中,并且,这里的fn本质上还是执行到了回调函数ƒunction input($$v) {msg=$$v}

(2)$emit

当点击子元素的按钮的时候,会触发this.$emit方法:

 Vue.prototype.$emit = function (event) {
    var vm = this;
    // ...
    var cbs = vm._events[event];
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs;
      var args = toArray(arguments, 1);
      var info = "event handler for \"" + event + "\"";
      for (var i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info);
      }
    }
    return vm
  };
}

首先获取到vm._events获取到处理后的回调函数,当执行到ƒunction input($$v) {msg=$$v}时,父组件中的msg发生改变,进而触发发布者depnotify方法,视图开始重新渲染。

小结

v-model在组件中则通过给点击事件绑定原生事件,当触发到this.$emit的时候,再进行回调函数ƒunction input($$v) {msg=$$v}的执行,进而达到子组件修改父组件中数据msg的目的。