vue源码分析-v-model的本质

本文深入探讨了Vue中v-model指令的工作原理,揭示了它如何实现双向数据绑定。通过模板编译、AST树解析、render函数生成、patch真实节点以及语法糖背后的操作,展示了v-model在表单绑定中的作用。v-model实际上是事件监听和数据更新的语法糖,它优化了在输入法组合文字过程中的数据更新,提高了用户体验。同时,v-model在组件间的使用本质上是子父组件通信的一种表达形式。
摘要由CSDN通过智能技术生成

双向数据绑定这个概念或者大家并不陌生,视图影响数据,数据同样影响视图,两者间有双向依赖的关系。在响应式系统构建的上,中,下篇我已经对数据影响视图的原理详细阐述清楚了。而如何完成视图影响数据这一关联?这就是本节讨论的重点:指令v-model

由于v-model和前面介绍的插槽,事件一致,都属于vue提供的指令,所以我们对v-model的分析方式和以往大同小异。分析会围绕模板的编译,render函数的生成,到最后真实节点的挂载顺序执行。最终我们依然会得到一个结论,v-model无论什么使用场景,本质上都是一个语法糖

11.1 表单绑定

11.1.1 基础使用

v-model和表单脱离不了关系,之所以视图能影响数据,本质上这个视图需要可交互的,因此表单是实现这一交互的前提。表单的使用以<input > <textarea> <select>为核心,更细的划分结合v-model的使用如下:

// 普通输入框
<input type="text" v-model="value1">

// 多行文本框
<textarea v-model="value2" cols="30" rows="10"></textarea>

// 单选框
<div class="group">
  <input type="radio" value="one" v-model="value3"> one
  <input type="radio" value="two" v-model="value3"> two
</div> 

// 原生单选框的写法 注:原生单选框的写法需要通过name绑定一组单选,两个radio的name属性相同,才能表现为互斥
<div class="group">
  <input type="radio" name="number" value="one">one
  <input type="radio" name="number" value="two">two
</div>


// 多选框  (原始值: value4: [])
<div class="group">
  <input type="checkbox" value="jack" v-model="value4">jack
  <input type="checkbox" value="lili" v-model="value4">lili
</div>

// 下拉选项
<select name="" id="" v-model="value5">
  <option value="apple">apple</option>
  <option value="banana">banana</option>
  <option value="bear">bear</option>
</select>


接下来的分析,我们以普通输入框为例

<div id="app">
  <input type="text" v-model="value1">
</div>

new Vue({
   
  el: '#app',
  data() {
   
    return {
   
      value1: ''
    }
  }
})

进入正文前先回顾一下模板到真实节点的过程。

    1. 模板解析成AST树;
    1. AST树生成可执行的render函数;
    1. render函数转换为Vnode对象;
    1. 根据Vnode对象生成真实的Dom节点。

接下来,我们先看看模板解析为AST树的过程。

11.1.2 AST树的解析

模板的编译阶段,会调用var ast = parse(template.trim(), options)生成AST树,parse函数的其他细节这里不展开分析,前面的文章或多或少都涉及过,我们还是把关注点放在模板属性上的解析,也就是processAttrs函数上。

使用过vue写模板的都知道,vue模板属性由两部分组成,一部分是指令,另一部分是普通html标签属性。z这也是属性处理的两大分支。而在指令的细分领域,又将v-on,v-bind做特殊的处理,其他的普通分支会执行addDirective过程。

// 处理模板属性
function processAttrs(el) {
   
  var list = el.attrsList;
  var i, l, name, rawName, value, modifiers, syncGen, isDynamic;
  for (i = 0, l = list.length; i < l; i++) {
   
    name = rawName = list[i].name; // v-on:click
    value = list[i].value; // doThis
    if (dirRE.test(name)) {
    // 1.针对指令的属性处理
      ···
      if (bindRE.test(name)) {
    // v-bind分支
        ···
      } else if(onRE.test(name)) {
    // v-on分支
        ···
      } else {
    // 除了v-bind,v-on之外的普通指令
        ···
        // 普通指令会在AST树上添加directives属性
        addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]);
        if (name === 'model') {
   
          checkForAliasModel(el, value);
        }
      }
    } else {
   
      // 2. 普通html标签属性
    }

  }
}

在深入剖析Vue源码 - 揭秘Vue的事件机制这一节,我们介绍了AST产生阶段对事件指令v-on的处理是为AST树添加events属性。类似的,普通指令会在AST树上添加directives属性,具体看addDirective函数。

// 添加directives属性
function addDirective (el,name,rawName,value,arg,isDynamicArg,modifiers,range) {
   
    (el.directives || (el.directives = [])).push(rangeSetItem({
   
      name: name,
      rawName: rawName,
      value: value,
      arg: arg,
      isDynamicArg: isDynamicArg,
      modifiers: modifiers
    }, range));
    el.plain = false;
  }

最终AST树多了一个属性对象,其中modifiers代表模板中添加的修饰符,如:.lazy, .number, .trim

// AST
{
   
  directives: {
   
    {
   
      rawName: 'v-model',
      value: 'value',
      name: 'v-model',
      modifiers: undefined
    }
  }
}

11.1.3 render函数生成

render函数生成阶段,也就是前面分析了数次的generate逻辑,其中genData会对模板的诸多属性进行处理,最终返回拼接好的字符串模板,而对指令的处理会进入genDirectives流程。

function genData(el, state) {
   
  var data = '{';
  // 指令的处理
  var dirs = genDirectives(el, state);
  ··· // 其他属性,指令的处理
  // 针对组件的v-model处理,放到后面分析
  if (el.model) {
   
    data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
  }
  return data
}

genDirectives逻辑并不复杂,他会拿到之前AST树中保留的directives对象,并遍历解析指令对象,最终以'directives:['包裹的字符串返回。

参考Vue3源码视频讲解:进入学习

// directives render字符串的生成
  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;
      // 对指令ast树的重新处理
      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) + ']'
    }
  }

这里有一句关键的代码var gen = state.directives[dir.name],为了了解其来龙去脉,我们回到Vue源码中的编译流程,在以往的文章中,我们完整的介绍过template模板的编译流程,这一部分的设计是非常复杂且巧妙的,其中大量运用了偏函数的思想,即分离了不同平台不同的编译过程,也为同一个平台每次提供相同的配置选项进行了合并处理,并很好的将配置进行了缓存。其中针对浏览器端有三个重要的指令选项。

var directive$1 = {
   
  model: model,
  text: text,
  html, html
}
var baseOptions = {
   
  ···
  // 指令选项
  directives: directives$1,
};
// 编译时传入选项配置
createCompiler(baseOptions)

而这个state.directives['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;
    {
   
      // 这里遇到type是file的html,如果还使用双向绑定会报出警告。
      // 因为File inputs是只读的
      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']
       
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值