关于vue.js:vue源码分析vmodel的本质

双向数据绑定这个概念或者大家并不生疏,视图影响数据,数据同样影响视图,两者间有双向依赖的关系。在响应式零碎构建的上,中,下篇我曾经对数据影响视图的原理具体论述分明了。而如何实现视图影响数据这一关联?这就是本节探讨的重点:指令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']
        );
      }
    }
    //组件上v-model的解决
    if (el.component) {
      genComponentModel(el, value, modifiers);
      // component v-model doesn't need extra runtime
      return false
    } else if (tag === 'select') {
      // select表单
      genSelect(el, value, modifiers);
    } else if (tag === 'input' && type === 'checkbox') {
      // checkbox表单
      genCheckboxModel(el, value, modifiers);
    } else if (tag === 'input' && type === 'radio') {
      // radio表单
      genRadioModel(el, value, modifiers);
    } else if (tag === 'input' || tag === 'textarea') {
      // 一般input,如 text, 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 {
      // 如果不是表单应用v-model,同样会报出正告,双向绑定只针对表单控件。
      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会对表单控件的AST树做进一步的解决,在下面的根底用法中,咱们晓得表单有不同的类型,每种类型对应的事件处理响应机制也不同。因而咱们须要针对不同的表单控件生成不同的render函数,因而须要产生不同的AST属性。model针对不同类型的表单控件有不同的解决分支。咱们重点剖析一般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]
        );
      }
    }
    // modifiers存贮的是v-model的修饰符。
    var ref = modifiers || {};
    // lazy,trim,number是可供v-model应用的修饰符
    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';
    // 过滤用户输出的首尾空白符
    if (trim) {
      valueExpression = "$event.target.value.trim()";
    }
    // 将用户输出转为数值类型
    if (number) {
      valueExpression = "_n(" + valueExpression + ")";
    }
    // genAssignmentCode函数是为了解决v-model的格局,容许应用以下的模式: v-model="a.b" v-model="a[b]"
    var code = genAssignmentCode(value, valueExpression);
    if (needCompositionGuard) {
      //  保障了不会在输入法组合文字过程中失去更新
      code = "if($event.target.composing)return;" + code;
    }
    //  增加value属性
    addProp(el, 'value', ("(" + value + ")"));
    // 绑定事件
    addHandler(el, event, code, null, true);
    if (trim || number) {
      addHandler(el, 'blur', '$forceUpdate()');
    }
  }

function genAssignmentCode (value,assignment) {
  // 解决v-model的格局,v-model="a.b" v-model="a[b]"
  var res = parseModel(value);
  if (res.key === null) {
    // 一般情景
    return (value + "=" + assignment)
  } else {
    // 对象模式
    return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")")
  }
}

genDefaultModel的逻辑有两局部,一部分是针对修饰符产生不同的事件处理字符串,二是为v-model产生的AST树增加属性和事件相干的属性。其中最重要的两行代码是

//  增加value属性
addProp(el, 'value', ("(" + value + ")"));
// 绑定事件属性
addHandler(el, event, code, null, true);

addHandler在之前介绍事件时剖析过,他会为AST树增加事件相干的属性,同样的addProp也会为AST树增加props属性。最终AST树新增了两个属性:

回到genData,通过genDirectives解决后,原先的AST树新增了两个属性,因而在字符串生成阶段同样须要解决propsevents的分支。

function genData$2 (el, state) {
  var data = '{';
  // 曾经剖析过的genDirectives
  var dirs = genDirectives(el, state);
  // 解决props
  if (el.props) {
    data += "domProps:" + (genProps(el.props)) + ",";
  }
  // 处理事件
  if (el.events) {
    data += (genHandlers(el.events, false)) + ",";
  }
}

最终render函数的后果为:

"_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"type":"text"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}})"

<input type="text" v-model="value"> 如果感觉下面的流程剖析啰嗦,能够间接看上面的论断,比照模板和生成的render函数,咱们能够失去:

    1. input标签所有属性,包含指令相干的内容都是以data属性的模式作为参数的整体传入_c(即:createElement)函数。
    1. input type的类型,在data属性中,以attrs键值对存在。
    1. v-model会有对应的directives属性形容指令的相干信息。
    1. 为什么说v-model是一个语法糖,从render函数的最终后果能够看出,它最终以两局部模式存在于input标签中,一个是将value1props的模式存在(domProps)中,另一个是以事件的模式存储input事件,并保留在on属性中。
    1. 重要的一个要害,事件用$event.target.composing属性来保障不会在输入法组合文字过程中更新数据,这点咱们前面会再次提到。
11.1.4 patch实在节点

patch之前还有一个生成vnode的过程,这个过程没有什么特别之处,所有的包含指令,属性会以data属性的模式传递到构造函数Vnode中,最终的Vnode领有directives,domProps,on属性:

有了Vnode之后紧接着会执行patchVnode,patchVnode过程是一个实在节点创立的过程,其中的要害是createElm办法,这个办法咱们在不同的场合也剖析过,后面的源码失去指令相干的信息也会保留在vnodedata属性里,所以对属性的解决也会走invokeCreateHooks逻辑。

function createElm() {
  ···
  // 针对指令的解决
   if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue);
    }
}

invokeCreateHooks会调用定义好的钩子函数,对vnode上定义的属性,指令,事件等进行实在DOM的解决,步骤包含以下(不蕴含全副):

    1. updateDOMProps会利用vnode data上的domProps更新input标签的value值;
    1. updateAttrs会利用vnode data上的attrs属性更新节点的属性值;
    1. updateDomListeners利用vnode data上的on属性增加事件监听。

因而v-model语法糖最终反馈的后果,是通过监听表单控件本身的input事件(其余类型有不同的监听事件类型),去影响本身的value。如果没有v-model的语法糖,咱们能够这样写: <input type="text" :value="message" @input="(e) => { this.message = e.target.value }" >

11.1.5 语法糖的背地

然而v-model仅仅是起到合并语法,创立一个新的语法糖的意义吗? 显然答案是否定的,对于须要应用输入法 (如中文、日文、韩文等) 的语言,你会发现 v-model 不会在输入法组合文字过程中失去更新。这就是v-model的一个重要的特点。它会在事件处理这一层增加新的事件监听compositionstart,compositionend,他们会别离在语言输出的开始和完结时监听到变动,只有借助$event.target.composing,就能够设计出只会在输入法组合文字的完结阶段才更新数据,这有利于进步用户的应用体验。这一部分我想借助脱离框架的表单来帮忙了解。

脱离框架的一个视图响应数据的实现(成果相似于v-model):

// html
<input type="text" id="inputValue">
<span id="showValue"></span>

// js

<script>
    let input = document.getElementById('inputValue');
    let show = document.getElementById('showValue');
    input.value = 123;
    show.innerText = input.value

    function onCompositionStart(e) {
      e.target.composing = true;
    }

    function onCompositionEnd(e) {
      if (!e.target.composing) {
        return
      }
      e.target.composing = false;
      show.innerText = e.target.value
    }
    function onInputChange(e) {
      // e.target.composing示意是否还在输出中
      if(e.target.composing)return;
      show.innerText = e.target.value
    }
    input.addEventListener('input', onInputChange)
    input.addEventListener('compositionstart', onCompositionStart)// 组合输出开始
    input.addEventListener('compositionend', onCompositionEnd) // 组合输出完结
</script>

11.2 组件应用v-model

最初咱们简略说说在父组件中应用v-model,能够先看论断,组件上应用v-model实质上是子父组件通信的语法糖。先看一个简略的应用例子。

 var child = {
    template: '<div><input type="text" :value="value" @input="emitEvent">{{value}}</div>',    methods: {      emitEvent(e) {        this.$emit('input', e.target.value)      }    },    props: ['value']  } new Vue({   data() {     return {       message: 'test'     }   },   components: {     child   },   template: '<div id="app"><child v-model="message"></child></div>',   el: '#app'
 })

父组件上应用v-model, 子组件默认会利用名为 valueprop 和名为 input 的事件,当然像select表单会以其余默认事件的模式存在。剖析源码的过程也大抵相似,这里只列举几个特地的中央。

AST生成阶段和一般表单控件的区别在于,当遇到child时,因为不是一般的html标签,会执行getComponentModel的过程,而getComponentModel的后果是在AST树上增加model的属性。

function model() {
  if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers);
  }
}

function genComponentModel (el,value,modifiers) {
    var ref = modifiers || {};
    var number = ref.number;
    var trim = ref.trim;

    var baseValueExpression = '?v';
    var valueExpression = baseValueExpression;
    if (trim) {
      valueExpression =
        "(typeof " + baseValueExpression + " === 'string'" +        "? " + baseValueExpression + ".trim()" +        ": " + baseValueExpression + ")";    }    if (number) {      valueExpression = "_n(" + valueExpression + ")";    }    var assignment = genAssignmentCode(value, valueExpression);    // 在ast树上增加model属性,其中有value,expression,callback属性    el.model = {      value: ("(" + value + ")"),      expression: JSON.stringify(value),      callback: ("function (" + baseValueExpression + ") {" + assignment + "}")    };  }

最终AST树的后果:

{
  model: {
    callback: "function ($$v) {message=$$v}"
    expression: ""message""
    value: "(message)"
  }
}

通过对AST树的解决后,回到genData$2的流程,因为有了model属性,父组件拼接的字符串会做进一步解决。

function genData$2 (el, state) { 
  var data = '{';
  var dirs = genDirectives(el, state);
  ···
  // v-model组件的render函数解决
  if (el.model) {
    data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
  }
  ···
  return data
}

因而,父组件最终的render函数体现为: "_c('child',{model:{value:(message),callback:function (?v) {message=?v},expression:"message"}})"

子组件的创立阶段照例会执行createComponent,其中针对model的逻辑须要特地阐明。

function createComponent() {
  // transform component v-model data into props & events
  if (isDef(data.model)) {
    // 解决父组件的v-model指令对象
    transformModel(Ctor.options, data);
  }
}
function transformModel (options, data) {
  // prop默认取的是value,除非配置上有model的选项
  var prop = (options.model && options.model.prop) || 'value';

  // event默认取的是input,除非配置上有model的选项
  var event = (options.model && options.model.event) || 'input'
  // vnode上新增props的属性,值为value
  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value;

  // vnode上新增on属性,标记事件
  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;
  }
}

transformModel的逻辑能够看出,子组件vnode会为data.props 增加 data.model.value,并且给data.on 增加data.model.callback。因而父组件v-model语法糖实质上能够批改为 '<child :value="message" @input="function(e){message = e}"></child>'

显然,这种写法就是事件通信的写法,这个过程又回到对事件指令的剖析过程了。因而咱们能够很显著的意识到,组件应用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']
        );
      }
    }
    //组件上v-model的解决
    if (el.component) {
      genComponentModel(el, value, modifiers);
      // component v-model doesn't need extra runtime
      return false
    } else if (tag === 'select') {
      // select表单
      genSelect(el, value, modifiers);
    } else if (tag === 'input' && type === 'checkbox') {
      // checkbox表单
      genCheckboxModel(el, value, modifiers);
    } else if (tag === 'input' && type === 'radio') {
      // radio表单
      genRadioModel(el, value, modifiers);
    } else if (tag === 'input' || tag === 'textarea') {
      // 一般input,如 text, 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 {
      // 如果不是表单应用v-model,同样会报出正告,双向绑定只针对表单控件。
      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会对表单控件的AST树做进一步的解决,在下面的根底用法中,咱们晓得表单有不同的类型,每种类型对应的事件处理响应机制也不同。因而咱们须要针对不同的表单控件生成不同的render函数,因而须要产生不同的AST属性。model针对不同类型的表单控件有不同的解决分支。咱们重点剖析一般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]
        );
      }
    }
    // modifiers存贮的是v-model的修饰符。
    var ref = modifiers || {};
    // lazy,trim,number是可供v-model应用的修饰符
    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';
    // 过滤用户输出的首尾空白符
    if (trim) {
      valueExpression = "$event.target.value.trim()";
    }
    // 将用户输出转为数值类型
    if (number) {
      valueExpression = "_n(" + valueExpression + ")";
    }
    // genAssignmentCode函数是为了解决v-model的格局,容许应用以下的模式: v-model="a.b" v-model="a[b]"
    var code = genAssignmentCode(value, valueExpression);
    if (needCompositionGuard) {
      //  保障了不会在输入法组合文字过程中失去更新
      code = "if($event.target.composing)return;" + code;
    }
    //  增加value属性
    addProp(el, 'value', ("(" + value + ")"));
    // 绑定事件
    addHandler(el, event, code, null, true);
    if (trim || number) {
      addHandler(el, 'blur', '$forceUpdate()');
    }
  }

function genAssignmentCode (value,assignment) {
  // 解决v-model的格局,v-model="a.b" v-model="a[b]"
  var res = parseModel(value);
  if (res.key === null) {
    // 一般情景
    return (value + "=" + assignment)
  } else {
    // 对象模式
    return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")")
  }
}

genDefaultModel的逻辑有两局部,一部分是针对修饰符产生不同的事件处理字符串,二是为v-model产生的AST树增加属性和事件相干的属性。其中最重要的两行代码是

//  增加value属性
addProp(el, 'value', ("(" + value + ")"));
// 绑定事件属性
addHandler(el, event, code, null, true);

addHandler在之前介绍事件时剖析过,他会为AST树增加事件相干的属性,同样的addProp也会为AST树增加props属性。最终AST树新增了两个属性:

回到genData,通过genDirectives解决后,原先的AST树新增了两个属性,因而在字符串生成阶段同样须要解决propsevents的分支。

function genData$2 (el, state) {
  var data = '{';
  // 曾经剖析过的genDirectives
  var dirs = genDirectives(el, state);
  // 解决props
  if (el.props) {
    data += "domProps:" + (genProps(el.props)) + ",";
  }
  // 处理事件
  if (el.events) {
    data += (genHandlers(el.events, false)) + ",";
  }
}

最终render函数的后果为:

"_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"type":"text"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}})"

<input type="text" v-model="value"> 如果感觉下面的流程剖析啰嗦,能够间接看上面的论断,比照模板和生成的render函数,咱们能够失去:

    1. input标签所有属性,包含指令相干的内容都是以data属性的模式作为参数的整体传入_c(即:createElement)函数。
    1. input type的类型,在data属性中,以attrs键值对存在。
    1. v-model会有对应的directives属性形容指令的相干信息。
    1. 为什么说v-model是一个语法糖,从render函数的最终后果能够看出,它最终以两局部模式存在于input标签中,一个是将value1props的模式存在(domProps)中,另一个是以事件的模式存储input事件,并保留在on属性中。
    1. 重要的一个要害,事件用$event.target.composing属性来保障不会在输入法组合文字过程中更新数据,这点咱们前面会再次提到。
11.1.4 patch实在节点

patch之前还有一个生成vnode的过程,这个过程没有什么特别之处,所有的包含指令,属性会以data属性的模式传递到构造函数Vnode中,最终的Vnode领有directives,domProps,on属性:

有了Vnode之后紧接着会执行patchVnode,patchVnode过程是一个实在节点创立的过程,其中的要害是createElm办法,这个办法咱们在不同的场合也剖析过,后面的源码失去指令相干的信息也会保留在vnodedata属性里,所以对属性的解决也会走invokeCreateHooks逻辑。

function createElm() {
  ···
  // 针对指令的解决
   if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue);
    }
}

invokeCreateHooks会调用定义好的钩子函数,对vnode上定义的属性,指令,事件等进行实在DOM的解决,步骤包含以下(不蕴含全副):

    1. updateDOMProps会利用vnode data上的domProps更新input标签的value值;
    1. updateAttrs会利用vnode data上的attrs属性更新节点的属性值;
    1. updateDomListeners利用vnode data上的on属性增加事件监听。

因而v-model语法糖最终反馈的后果,是通过监听表单控件本身的input事件(其余类型有不同的监听事件类型),去影响本身的value。如果没有v-model的语法糖,咱们能够这样写: <input type="text" :value="message" @input="(e) => { this.message = e.target.value }" >

11.1.5 语法糖的背地

然而v-model仅仅是起到合并语法,创立一个新的语法糖的意义吗? 显然答案是否定的,对于须要应用输入法 (如中文、日文、韩文等) 的语言,你会发现 v-model 不会在输入法组合文字过程中失去更新。这就是v-model的一个重要的特点。它会在事件处理这一层增加新的事件监听compositionstart,compositionend,他们会别离在语言输出的开始和完结时监听到变动,只有借助$event.target.composing,就能够设计出只会在输入法组合文字的完结阶段才更新数据,这有利于进步用户的应用体验。这一部分我想借助脱离框架的表单来帮忙了解。

脱离框架的一个视图响应数据的实现(成果相似于v-model):

// html
<input type="text" id="inputValue">
<span id="showValue"></span>

// js

<script>
    let input = document.getElementById('inputValue');
    let show = document.getElementById('showValue');
    input.value = 123;
    show.innerText = input.value

    function onCompositionStart(e) {
      e.target.composing = true;
    }

    function onCompositionEnd(e) {
      if (!e.target.composing) {
        return
      }
      e.target.composing = false;
      show.innerText = e.target.value
    }
    function onInputChange(e) {
      // e.target.composing示意是否还在输出中
      if(e.target.composing)return;
      show.innerText = e.target.value
    }
    input.addEventListener('input', onInputChange)
    input.addEventListener('compositionstart', onCompositionStart)// 组合输出开始
    input.addEventListener('compositionend', onCompositionEnd) // 组合输出完结
</script>

11.2 组件应用v-model

最初咱们简略说说在父组件中应用v-model,能够先看论断,组件上应用v-model实质上是子父组件通信的语法糖。先看一个简略的应用例子。

 var child = {
    template: '<div><input type="text" :value="value" @input="emitEvent">{{value}}</div>',    methods: {      emitEvent(e) {        this.$emit('input', e.target.value)      }    },    props: ['value']  } new Vue({   data() {     return {       message: 'test'     }   },   components: {     child   },   template: '<div id="app"><child v-model="message"></child></div>',   el: '#app'
 })

父组件上应用v-model, 子组件默认会利用名为 valueprop 和名为 input 的事件,当然像select表单会以其余默认事件的模式存在。剖析源码的过程也大抵相似,这里只列举几个特地的中央。

AST生成阶段和一般表单控件的区别在于,当遇到child时,因为不是一般的html标签,会执行getComponentModel的过程,而getComponentModel的后果是在AST树上增加model的属性。

function model() {
  if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers);
  }
}

function genComponentModel (el,value,modifiers) {
    var ref = modifiers || {};
    var number = ref.number;
    var trim = ref.trim;

    var baseValueExpression = '?v';
    var valueExpression = baseValueExpression;
    if (trim) {
      valueExpression =
        "(typeof " + baseValueExpression + " === 'string'" +        "? " + baseValueExpression + ".trim()" +        ": " + baseValueExpression + ")";    }    if (number) {      valueExpression = "_n(" + valueExpression + ")";    }    var assignment = genAssignmentCode(value, valueExpression);    // 在ast树上增加model属性,其中有value,expression,callback属性    el.model = {      value: ("(" + value + ")"),      expression: JSON.stringify(value),      callback: ("function (" + baseValueExpression + ") {" + assignment + "}")    };  }

最终AST树的后果:

{
  model: {
    callback: "function ($$v) {message=$$v}"
    expression: ""message""
    value: "(message)"
  }
}

通过对AST树的解决后,回到genData$2的流程,因为有了model属性,父组件拼接的字符串会做进一步解决。

function genData$2 (el, state) { 
  var data = '{';
  var dirs = genDirectives(el, state);
  ···
  // v-model组件的render函数解决
  if (el.model) {
    data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
  }
  ···
  return data
}

因而,父组件最终的render函数体现为: "_c('child',{model:{value:(message),callback:function (?v) {message=?v},expression:"message"}})"

子组件的创立阶段照例会执行createComponent,其中针对model的逻辑须要特地阐明。

function createComponent() {
  // transform component v-model data into props & events
  if (isDef(data.model)) {
    // 解决父组件的v-model指令对象
    transformModel(Ctor.options, data);
  }
}
function transformModel (options, data) {
  // prop默认取的是value,除非配置上有model的选项
  var prop = (options.model && options.model.prop) || 'value';

  // event默认取的是input,除非配置上有model的选项
  var event = (options.model && options.model.event) || 'input'
  // vnode上新增props的属性,值为value
  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value;

  // vnode上新增on属性,标记事件
  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;
  }
}

transformModel的逻辑能够看出,子组件vnode会为data.props 增加 data.model.value,并且给data.on 增加data.model.callback。因而父组件v-model语法糖实质上能够批改为 '<child :value="message" @input="function(e){message = e}"></child>'

显然,这种写法就是事件通信的写法,这个过程又回到对事件指令的剖析过程了。因而咱们能够很显著的意识到,组件应用v-model实质上还是一个子父组件通信的语法糖。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理