[vue2源码]深度理解Vue中v-model原理

概念:

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

源码:

v-model一种是在表单元素上使用,另外一种是在组件上使用

一、 表单元素中v-model

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

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

genDirectives
src/compiler/codegen/index.js

function genDirectives (el, state) {
		// 指令参数,directives中有个model,然后赋值给dirs
        var dirs = el.directives;
        if (!dirs) { return }
        var res = 'directives:[';
        var hasRuntime = false;
        var i, l, dir, needRuntime;
        // dirs是个数组,genDirectives方法中遍历处理这些指令
        for (i = 0, l = dirs.length; i < l; i++) {
          dir = dirs[i];
          needRuntime = true;
          // 这里等于var gen = state.directives[model];
          var gen = state.directives[dir.name];
          if (gen) {
          	// 执行model()
            needRuntime = !!gen(el, dir, state.warn);
          }
    }

执行model()

  • 组件 — 执行genComponentModel方法
  • select — 执行genSelect方法
  • checkbox — input中使用且是checkbox类型执行genCheckboxModel方法
  • radio — input中使用且是radio类型执行genRadioModel方法,
  • textarea — 执行genDefaultModel方法
  • input — 执行genDefaultModel方法
  • neither — 都不是则抛出异常

model

  function model (el,dir,_warn) {
  	// value:model值
  	// tag:model,在input中使用就是input
  	// type: 绑定类型
    var value = dir.value;
    var modifiers = dir.modifiers;
    var tag = el.tag; 
    var type = el.attrsMap.type;

    // 判断绑定的什么,通过对应的类型判断进入不同的执行方法
    // 这里会进入到genDefaultModel执行方法中
    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']
      );
    }

    // ensure runtime directive metadata
    return true
  }

进入到genDefaultModel执行方法

genDefaultModel

	// input --- 执行genDefaultModel方法
    function genDefaultModel (el,value,modifiers) {
        var type = el.attrsMap.type;
        {
          // 是否同时具有指令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]
            );
          }
        }
        // 获取修饰符lazy, number及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 + ")";
        }
		// 接下来
  }

接下来

		//获取code
        var code = genAssignmentCode(value, valueExpression);
        if (needCompositionGuard) {
          // code = if($event.target.composing)return; value=$event.target.value
          // if($event.target.composing)return;不记录用户未确定的输入
          // 用户未确定的输入
          // value=$event.target.value在genAssignmentCode方法中返回
          code = "if($event.target.composing)return;" + code;
        }
		// 绑定value
        addProp(el, 'value', ("(" + value + ")"));
        // 给el添加input事件处理
        addHandler(el, event, code, null, true);
        if (trim || number) {
          addHandler(el, 'blur', '$forceUpdate()');
        }

if($ event.target.composing)return;不记录用户未确定的输入

value=$event.target.value在genAssignmentCode方法中返回

用户未确定的输入:
用户未确定输入
aaaaa就是已确定输入,bbbbb就是未确定输入

genAssignmentCode

  // 生成model中绑定的value值,返回code
  function genAssignmentCode (
    value,
    assignment
  ) {
    var res = parseModel(value);
    if (res.key === null) {
      // 返回value=$event.target.value
      return (value + "=" + assignment)
    } else {
      // 但我们不走这里,当obj.value时会进入这里
      // $set(obj, "value", $event.target.value)”
      return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")")
    }
  }

$set

// value属性是否一开始就在obj中
// 如果存在就只是进行单纯的赋值
// 不存在的话在进行响应式操作
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

function addProp (el, name, value, range, dynamic) {
	  // 首先判断el上有没有props
	  // 如果没有的话创建props并赋值为一个空数组
	  // 然后拼接对象并加入到props中
      (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">

二、 组件中v-model

环境:

当有一个自定义的组件时,子组件通过this.$emit(‘input’,value)来对父组件传值,父组件接受到之后让e.target.value赋值给input中的value从而实现组件内部暴露出组件的值到 v-model所绑定的值中去

浅析:

原生控件绑定事件,捕捉到原生组件的值,利用 $emit方法,触发input方法,组件监听到 input事件然后把值传入到value中

源码:

自定义model时,只需要在组件中传入model的prop和event属性
createComponent
src/core/vdom/create-component.js

// 如果当前数据有model属性,就会使用transformModel转换model
if (isDef(data.model)) {
   transformModel(Ctor.options, data)
}

transformModel
src/core/vdom/create-component.js

function transformModel (options, data: any) {
  // 如果没有prop属性默认就是value
  const prop = (options.model && options.model.prop) || 'value'
  // 如果没有event属性,默认就是input
  const event = (options.model && options.model.event) || 'input'
  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value// data.attrs.value="xxx"
  const on = data.on || (data.on = {})
  const existing = on[event] // 给on绑定input的事件,对应的函数就是callback
  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
  }
}

自定义model:


const VueTemplateCompiler = require('vue-template-compiler');
const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></elcheckbox>');
// 解析后
with(this) {
    return _c('el-checkbox', {
        model: { // v-model解析后
            value: (check), 
            callback: function ($$v) {
                check = $$v
            }, 
            expression: "check"
        }
    })
}

传入prop和event后对应的转换

Vue.component('el-checkbox', {
    template:`<input type="checkbox" :checked="check" @change="$emit('change',$event.target.checked)">`, 
    model:{
        prop:'check', // 更改默认的value的名字
        event:'change' // 更改默认的方法名
    },
    props: {
        check: Boolean
    }
})
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
v-modelVue.js 常用的一个指令,它可以在表单元素上创建双向数据绑定。在 Vue.js ,指令是一种特殊的属性,用于添加特定的行为。v-model 指令实际上是一个语法糖,它将表单元素的 value 属性绑定到 Vue 实例的数据对象的一个属性上,并且在用户输入时自动更新这个属性的值。 下面是一个简单的示例: ```html <input type="text" v-model="message"> ``` 上述代码,v-model 指令将输入框的值与 Vue 实例的 message 属性进行双向绑定。当用户在输入框输入内容时,message 属性的值会自动更新,反之亦然。 接下来,我们来深入分析 v-model源码实现。 首先,我们需要了解 v-model 在编译阶段是如何被解析的。在 Vue.js ,编译阶段会将模板代码解析为抽象语法树(AST)。当编译器遇到 v-model 指令时,它会将它解析为一个对象: ```javascript { name: 'model', rawName: 'v-model', value: 'message', expression: '"message"', arg: null, modifiers: {} } ``` 在这个对象,name 属性表示指令的类型,value 属性表示指令绑定的值,expression 属性表示值的表达式,arg 属性表示指令的参数,modifiers 属性表示指令的修饰符。 接下来,编译器会根据指令的类型和绑定的值生成对应的代码,这些代码会在运行时执行,从而实现指令的功能。对于 v-model 指令,编译器会生成以下代码: ```javascript { key: "value", expression: `message`, arg: null, mode: 'twoWay', directive: 'model', modifiers: {} } ``` 这段代码,key 属性表示绑定的属性名,expression 属性表示绑定的属性值,mode 属性表示数据绑定的模式,directive 属性表示指令的类型,modifiers 属性表示指令的修饰符。 在运行时,Vue.js 会在组件渲染过程对这些指令进行解析和处理。对于 v-model 指令,Vue.js 会根据指令的模式(单向绑定或双向绑定)生成对应的数据绑定代码。对于单向绑定,Vue.js 会在组件初始化时将属性值赋给表单元素的 value 属性。对于双向绑定,Vue.js 会在表单元素的 input 和 change 事件更新属性值。 下面是一段简化版的 v-model 指令处理代码: ```javascript function bindModel(el, binding, vnode) { const value = binding.value const mode = binding.mode || 'oneWay' const key = binding.key || 'value' if (mode === 'oneWay') { el.value = value } else if (mode === 'twoWay') { el.value = value el.addEventListener('input', () => { vnode.context[key] = el.value }) } } Vue.directive('model', { bind: bindModel, update: bindModel }) ``` 在这段代码,bindModel 函数用于处理 v-model 指令。它首先获取指令的 value、mode 和 key 属性,然后根据 mode 属性判断数据绑定的模式。如果是单向绑定,直接将属性值赋给表单元素的 value 属性;如果是双向绑定,同时绑定 input 事件,当用户输入时更新属性值。最后,Vue.js 会将这个指令注册到指令系统,以便在组件渲染时自动调用。 总的来说,v-model 指令的实现原理其实很简单,就是通过数据绑定来实现表单元素和数据对象之间的双向绑定。但是,在实际开发,还有很多细节需要注意,比如不同表单元素的处理方式、不同数据类型的转换等等。因此,如果想要深入理解 v-model 指令的实现原理,需要对 Vue.js 的组件渲染机制有比较深入的了解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值