v-model源码解析(超详细)

抛出问题

咱们先来看一下下面这段代码html

<template>
  <div>
    <div class="message">{{ info.message }}</div>
    <div><input v-model="info.message" type="text"></div>
    <button @click="change">click</button>
  </div>
</template>

<script>
  export default {
    data () {
      return {
        info: {}
      }
    },
    methods: {
      change () {
        this.info.message = 'hello world'
      }
    }
  }
</script>

上述代码很简单,就不作过多的解释了。若是这段代码都看不懂,那下面也不必再看下去了vue

问题重现步骤

我如今对上述代码作两种操做:react

  1. 一进页面先在输入框中输入hello vue
  2. 一进页面先点击click按钮进行赋值操做,再在输入框中输入hello vue

上述两种状况分别会出现什么现象呢?express

第一种操做,当咱们在输入框中输入hello vue的时候,class为message的div中会联动出现hello vue,也就是说info中的message属性是响应式的segmentfault

第二种操做,当咱们先进行赋值操做,以后不管在输入框中输入什么内容,class为message的div中都不会联动出现任何值,也就是说info中的message属性非响应式的数组

问题引起的猜测

查阅vue官方文档咱们得知vue在初始化的时候会对data中全部已经定义的对象及其子属性进行遍历,给他们添加gettersetter,使得他们变成响应式的(关于响应式这块以后会单开文章进行解析),可是vue不能检测对象属性的添加或删除。可是,可使用 Vue.set(object, propertyName, value)方法向嵌套对象添加响应式属性app

基于上述描述,咱们先看第一种操做。直接在输入框中输入hello vue,class为message的div中会联动出现hello vue。可是咱们看data中只定义了info对象,其中并无定义message属性,message属于新增属性。根据vue官方文档中说的,vue不能检测对象属性的添加或删除,因此我猜想vue底层在解析v-model指令的时候,每当触发表单元素的监听事件(例如input事件),就会有Vue.set()操做,从而触发setterdom

带着这个猜想,咱们来看第二种操做。一进页面先点击click按钮,对info.message进行赋值,message属于新增属性,根据官方文档中说的,此时message并非响应式的,没问题。可是咱们接着在input输入框中输入值,class为message的div中没有联动出现任何值,根据咱们对于第一种状况的猜想,当输入框监听到input事件的时候,会对info中的message进行Vue.set()操做,因此理论上就算一开始click中是对新增属性message直接赋值的,致使该属性并不是响应式的,在通过输入框input事件中的Vue.set()操做以后,应该会变成响应式的,而如今呈现出来的状况并非这样的啊,这是为何呢?编辑器

聪明的大家应该已经猜到在Vue.set()底层源码中,应该是会判断message属性是否一开始就在info中,若是存在就只是进行单纯的赋值,不存在的话在进行响应式操做,绑定gettersetteride

可是光猜想确定是不够的,咱们要用事实说话,作到有理有据。接下来咱们就去看下vue源码中v-model这块,看看是否是如咱们猜测的同样

探索真相-源码分析

v-model指令使用分为两种状况:一种是在表单元素上使用,另一种是在组件上使用。咱们今天分析的是第一种状况,也就是在表单元素上使用

v-model实现机制

咱们先简单说下v-model的机制:v-model会把它关联的响应式数据(如info.message),动态地绑定到表单元素的value属性上,而后监听表单元素的input事件:当v-model绑定的响应数据发生变化时,表单元素的value值也会同步变化;当表单元素接受用户的输入时,input事件会触发,input的回调逻辑会把表单元素value最新值同步赋值给v-model绑定的响应式数据。

v-model实现原理

我用来分析的源码是在vue官网安装模块里面下载的开发版本(2.6.10),便于调试

编译

咱们今天讲的内容其实就是把模版编译成render函数的一个流程,这里不会对每步流程都展开讲解,我能够给出一个步骤实现的流程,你们有兴趣的话能够根据这个流程来阅读代码,提升效率
$mount()->compileToFunctions()->compile()->baseCompile()
真正的编译过程都是在这个baseCompile()里面执行,执行步骤能够分为三个过程

  1. 解析模版字符串生成AST
const ast = parse(template.trim(), options)
  1. 优化语法树
optimize(ast, options)
  1. 生成代码
const code = generate(ast, options)

而后咱们看下generate里面的代码,这也是咱们今天讲的重点

function generate (
    ast,
    options
  ) {
    var state = new CodegenState(options);
    var code = ast ? genElement(ast, state) : '_c("div")';
    return {
      render: ("with(this){return " + code + "}"),
      staticRenderFns: state.staticRenderFns
    }
  }

generate() 首先经过 genElement()->genData$2()->genDirectives() 生成code,再把codewith(this){return ${code}}} 包裹起来,最终的到render函数。
接下来咱们从genDirectives()开始讲解

genDirectives

在模板的编译阶段,v-model跟其余指令同样,会被解析到 el.directives中,以后会经过genDirectives方法处理这些指令,咱们这里从genDirectives()重点开始讲,至于怎么到这步,若是你们感兴趣的话,能够从generate()开始看

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) {
            // compile-time directive that manipulates AST.
            // returns true if it also needs a runtime counterpart.
            needRuntime = !!gen(el, dir, state.warn);
          }
          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.isDynamicArg ? dir.arg : ("\"" + (dir.arg) + "\""))) : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
          }
        }
        if (hasRuntime) {
          return res.slice(0, -1) + ']'
        }
    }

我对上面这个代码打个断点,结合咱们上面的代码例子,这样子看的更清楚,以下图:
getDirectives.png
咱们能够看到传进来的elAst语法树,el.directivesel上的指令,在咱们这里就是el-model的相关参数,而后赋值给变量dirs

往下看代码,for循环中有段代码:

var gen = 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.dirctives是什么呢?打个断点看一下,以下图:
genDirectives_state.png
咱们能够看到state.directives里面包含了不少指令方法,model就在其中,

var gen = state.directives[dir.name];

其实就是等价于

var gen = state.directives[model];

因此代码中的变量gen获得的是model()

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
  }

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

genDefaultModel

function genDefaultModel (el,value,modifiers) {
        var type = el.attrsMap.type;
        {
          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()');
        }
  }

咱们对genDefaultModel()中的代码进行分块解析,首先看下面这段代码:

是否同时具备指令v-modelv-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]
      );
    }

这块代码其实就是解释表单元素是否同时有指令v-modelv-bind

var ref = modifiers || {};
    var lazy = ref.lazy;
    var number = ref.number;
    var trim = ref.trim;
修饰符

这段代码就是获取修饰符lazy, number及trim

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

这里的needCompositionGuard后面再说有什么用,如今只用知道默认是true就好了

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 + ")";
    }

上面这段代码中,event = ‘input’,定义变量valueExpression,修饰符trimnumber在咱们这个demo中默认都没有,因此跳过往下看

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

这里涉及到一个函数genAssignmentCode,上源码:

function genAssignmentCode (
    value,
    assignment
  ) {
    var res = parseModel(value);
    if (res.key === null) {
      return (value + "=" + assignment)
    } else {
      return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")")
    }
  }

这段代码是生成v-model绑定的value的值,看到这段代码,咱们就知道离真相不远了,由于咱们看到了$set()。如今咱们经过断点具体分析下,以下图:
getAssignmentCode.png
经过断点咱们能够很清楚的看到咱们先执行parseModel('info.message')获取到一个对象res,因为咱们的demo中绑定的值是路径形式的对象,即info.message,因此此时res经过parseModel解析出来就是{exp: "info", key: "message"}。那下面的判断就进入else,即:

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

回到上面的getDefaultModel()中

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

此时code获取到genAssignmentCode()返回的字符串值"$set(info, "message", $event.target.value)"

$event.target.composing

上面我说的到变量needCompositionGuard = true,通过拼接,最终code = “if($event.target.composing)return;$set(info, "message", $event.target.value)”

这里的$event.target.composing有什么用呢?其实就是用于判断这次input事件是不是IME构成触发的,若是是IME构成,直接return。IME 是输入法编辑器(Input Method Editor) 的英文缩写,IME构成指咱们在输入文字时,处于未确认状态的文字。如图:
composing.png
带下划线的ceshi就属于IME构成,它会一样会触发input事件,但不会触发v-model更新数据。

继续往下看

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

先说下addProp(el, 'value', ("(" + value + ")"))

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

照常打个断点看下:,以下图
addProp.png

能够看到此方法的功能为给el添加props,首先判断el上有没有props,若是没有的话建立props并赋值为一个空数组,随后拼接对象并推到props中,代码在此demo中至关于push{name: "value", value: "(info.message)"}

若是一直往下追,能够看到这个方法实际上是在input输入框上绑定了value,对照咱们的demo来看,就是将<input v-model="info.message" type="text">变成<input v-bind:value="info.message" type="text">

addHandler

一样的,addHandler()至关于在input上绑定了input事件,最终咱们demo的模版就会被编译成

<input v-bind:value="info.message" v-on:input="info.message=$event.target.value">
render

后续再根据一些指令拼接,咱们最终的到的render以下:

with(this) {
    return _c('div', {
        attrs: {
            "id": "app-2"
        }
    }, [_c('div', [_v(_s(info.message))]), _v(" "), _c('div', [_c('input', {
        directives: [{
            name: "model",
            rawName: "v-model",
            value: (info.message),
            expression: "info.message"
        }],
        attrs: {
            "type": "text"
        },
        domProps: {
            "value": (info.message)
        },
        on: {
            "input": function ($event) {
                if ($event.target.composing) return;
                $set(info, "message", $event.target.value)
            }
        }
    })]), _v(" "), _c('button', {
        on: {
            "click": change
        }
    }, [_v("click")])])
}

最后经过createFunction()render代码串经过new Function的方式转换成可执行的函数,赋值给 vm.options.render,这样当组件经过vm._render的时候,就会执行这个render函数

至此,针对表单元素上的v-model指令从开始编译到最终生成render()并执行的过程就讲解完了,咱们验证了在编译阶段,v-model会在监听到input事件时对咱们绑定的value进行Vue.$set()操做

还记得咱们上面说的对demo第二种操做状况么?先进行click操做赋值,那v-model中的Vue.$set()操做彷佛没有做用了。咱们当时猜想的是Vue.$set()底层源码中有应该是会判断message属性是否一开始就在info中,若是存在就只是进行单纯的赋值,不存在的话在进行响应式操做,绑定gettersetter

如今咱们就去Vue.$set()中看一下

set

先上代码:

/**
   * Set a property on an object. Adds the new property and
   * triggers change notification if the property doesn't
   * already exist.
   */
  function set (target, key, val) {
    if (isUndef(target) || isPrimitive(target)
    ) {
      warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
    }
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      target.length = Math.max(target.length, key);
      target.splice(key, 1, val);
      return val
    }
    if (key in target && !(key in Object.prototype)) {
      target[key] = val;
      return val
    }
    var ob = (target).__ob__;
    if (target._isVue || (ob && ob.vmCount)) {
      warn(
        'Avoid adding reactive properties to a Vue instance or its root $data ' +
        'at runtime - declare it upfront in the data option.'
      );
      return val
    }
    if (!ob) {
      target[key] = val;
      return val
    }
    defineReactive$$1(ob.value, key, val);
    ob.dep.notify();
    return val
  }

看到这句代码了么?这就是证据,验证咱们猜测的证据

if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val
}
验证猜测

当咱们首先点击click的时候,执行this.info.message = 'hello world',此时info对象中新增了一个message属性。当咱们在input框中输入值并触发Vue.$set()时,key in targettrue,而且message又不是Object原型上的属性,因此!(key in Object.prototype)也为true,此时message属性并非响应式属性,没有绑定setter,因此仅仅进行了单纯的赋值操做。

而当咱们一进页面首次input中执行输入操做时,根据上面咱们的分析input框监听到了input事件,先执行了Vue.$set()操做,由于时首次,因此info中尚未message属性,因此上面的key in targetfalse,跳过了赋值操做,到了下面的

defineReactive$$1(ob.value, key, val);
ob.dep.notify();

这个defineReactive的做用就是为message绑定了getter()setter(),以后再对message的赋值操做都会直接进入自身绑定的setter中进行响应式操做

一个意外的发现

我忽然奇想把vue的版本换到了2.3.0,发现v-model不能对demo中的message属性实现响应化,跑去看了下vue更新日志,发如今2.5.0版本中,有这么一句话
now creates non-existent properties as reactive (non-recursive) e1da0d5, closes #5932 (See reasoning behind this change)
上面这句话的意思是从2.5.0版本开始支持将不存在的属性响应化,非递归的。
由于message属性一开始在info中并无定义,在2.3.0中,还不支持将不存在的属性响应化的操做,因此对demo无效

总结

到这里,咱们这篇文章就结束了 里面有一些细节若是你们有兴趣的话能够本身再去深究一下。有时候很小的一个问题,背后牵扯到的知识点也是不少的,尽可能把每一个不懂背后的逻辑搞清楚,才能尽快的成为你想成为的人

参考资料

https://segmentfault.com/a/11...
https://blog.csdn.net/fabulou...

相关文章
相关标签/搜索