[vue2]熬夜编写为了让你们通俗易懂的去深入理解v-model原理

416 阅读3分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

更多文章

[vue2]熬夜编写为了让你们通俗易懂的去深入理解vue-router并手写一个

[vue2]熬夜编写为了让你们通俗易懂的去深入理解vuex并手写一个

[vue2]熬夜编写为了让你们通俗易懂的去深入理解nextTick原理

[vue2]熬夜编写为了让你们通俗易懂的去深入理解双向绑定以及解决监听Array数组变化问题

熬夜不易,点个赞再走吧

概念

value+input方法的语法糖
可绑定:input,checkbox,select,textarea,radio

源码

因为根据可绑定的类型不同,会有不同的处理方法,因此需要默认一项

此默认绑定input,v-model绑定值为value,默认在注释中逐行解释

引入:在模板的编译阶段, v-model跟其他指令一样,会被解析到 el.directives 中,之后会通过genDirectives方法处理这些指令

genDirectives
src/compiler/codegen/index.js

function genDirectives (el, state) {
    var dirs = el.directives;
    if (!dirs) { return }
    var res = 'directives:[';
    var hasRuntime = false;
    var i, l, dir, needRuntime;
    for (i = 0, l = dirs.length; i < l; i++) {
      dir = dirs[i];
      needRuntime = true;
      var gen = state.directives[dir.name];
      if (gen) {
        needRuntime = !!gen(el, dir, state.warn);
      }
}

指令参数,directives中有个model,然后赋值给dirs

var dirs = el.directives;

dirs是个数组,genDirectives方法中遍历处理这些指令

for (i = 0, l = dirs.length; i < l; i++) {
var gen = state.directives[dir.name];

这里等于var gen = state.directives[model];

因此gen = model()

然后执行model()

if (gen) {
needRuntime = !!gen(el, dir, state.warn);
}

执行model()

model

那我们再来看看model这个方法里面具体做了些什么事情,先上model的代码

  function model (el,dir,_warn) {
    warn$1 = _warn;
    var value = dir.value;
    var modifiers = dir.modifiers;
    var tag = el.tag;
    var type = el.attrsMap.type;

    {
      // inputs with type="file" are read only and setting the input's
      // value will throw an error.
      if (tag === 'input' && type === 'file') {
        warn$1(
          "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +
          "File inputs are read only. Use a v-on:change listener instead.",
          el.rawAttrsMap['v-model']
        );
      }
    }

    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
    } else {
      warn$1(
        "<" + (el.tag) + " v-model=\"" + value + "\">: " +
        "v-model is not supported on this element type. " +
        'If you are working with contenteditable, it\'s recommended to ' +
        'wrap a library dedicated for that purpose inside a custom component.',
        el.rawAttrsMap['v-model']
      );
    }

    // ensure runtime directive metadata
    return true
  }

value:model值
tag:model,在input中使用就是input
type: 绑定类型

var value = dir.value;
var modifiers = dir.modifiers;
var tag = el.tag; 
var type = el.attrsMap.type;

判断绑定的什么,通过对应的类型判断进入不同的执行方法\

其中根据判断分别进入以下分支

  1. 组件 — 执行genComponentModel方法
  2. select — 执行genSelect方法
  3. checkbox — input中使用且是checkbox类型执行genCheckboxModel方法
  4. radio — input中使用且是radio类型执行genRadioModel方法,
  5. textarea — 执行genDefaultModel方法
  6. input — 执行genDefaultModel方法
  7. neither — 都不是则抛出异常
if (el.component) {
  genComponentModel(el, value, modifiers);
  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);
  return false
} else {
  warn$1(
    "<" + (el.tag) + " v-model=\"" + value + "\">: " +
    "v-model is not supported on this element type. " +
    'If you are working with contenteditable, it\'s recommended to ' +
    'wrap a library dedicated for that purpose inside a custom component.',
    el.rawAttrsMap['v-model']
  );
}

model方法根据传入的参数对tag的类型进行判断,调用不同的处理逻辑,本demo中tag的类型为input,所以会执行genDefaultModel方法

进入到genDefaultModel执行方法

genDefaultModel

input --- 执行genDefaultModel方法

function genDefaultModel (el,value,modifiers) { }

是否同时具有指令v-model和v-bind

{
  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]
    );
  }
}

vue表单中有多种可选修饰符
接下来开始获取修饰符lazy, number及trim

  1. .lazy 取代input监听change事件
  2. .number 输入字符串转为数字
  3. .trim 输入首尾空格过滤
var ref = modifiers || {};
var lazy = ref.lazy;
var number = ref.number;
var trim = ref.trim;
var needCompositionGuard = !lazy && type !== 'range';

.lazy 取代input监听change事件

var event = lazy
  ? 'change'
  : type === 'range'
    ? RANGE_TOKEN
    : 'input';

var valueExpression = '$event.target.value';

.trim 输入首尾空格过滤

if (trim) { 
    valueExpression = "$event.target.value.trim()";
}

.number 输入字符串转为数字

if (number) { 
    valueExpression = "_n(" + valueExpression + ")"; 
}

接下来处理input

获取code

var code = genAssignmentCode(value, valueExpression);
if (needCompositionGuard) {
  code = "if($event.target.composing)return;" + code;
}

$event.target.composing记录我们在输入文字时,处于未确认状态的文字
if($event.target.composing)return;不记录用户未确定的输入
它会同样会触发input事件,但不会触发v-model更新数据

其中
code = "if($event.target.composing)return;" + code;
等于
code = "if($event.target.composing)return;" + $event.target.value\

value=$event.target.value在genAssignmentCode方法中返回
那现在进入genAssignmentCode

genAssignmentCode
生成model中绑定的value值,返回code

function genAssignmentCode ( value, assignment ) {
    var res = parseModel(value);
}

res.key === null时,返回value=$event.target.value

if (res.key === null) { 
    return (value + "=" + assignment) 
}

但我们不走这里,当obj.value时会进入这里
返回$set(obj, "value", $event.target.value)”

else {
  return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")")
}

接下来进入$set

function set (target, key, val) {}
  1. value属性是否一开始就在obj中
  2. 如果存在就只是进行单纯的赋值
  3. 不存在的话在进行响应式操作
function set (target, key, val) {
    if (key in target && !(key in Object.prototype)) {
        target[key] = val;
        return val
    }
}
function set (target, key, val) {
    if (key in target && !(key in Object.prototype)) {
        target[key] = val;
        return val
    }
    // defineReactive为value绑定getter()和setter()
    defineReactive$$1(ob.value, key, val);
    ob.dep.notify();
    return val
}

addProp(el, 'value', ("(" + value + ")")); 这一行有提到动态绑定value,那么进入addProp里看一下

addProp
首先判断el上有没有props
如果没有的话创建props并赋值为一个空数组
然后拼接对象并加入到props中

function addProp (el, name, value, range, dynamic) {
  (el.props || (el.props = [])).push(rangeSetItem({ name: name, value: value, dynamic: dynamic }, range));
            el.plain = false;
}

addProp的作用是让input动态绑定value 让原本的<input v-model="value">变成<input v-bind:value="value">
然后继续执行addHandler

addHandler的作用是让input动态绑定input 让原本的<input v-bind:value="value">变成<input :value="value" @input="value=$event.target.value">

最后根据render()生成最终渲染,over