Vue.js 源码分析(二十二) 指令篇 v-model指令详解

Vue.js提供了v-model指令用于双向数据绑定,比如在输入框上使用时,输入的内容会事实映射到绑定的数据上,绑定的数据又可以显示在页面里,数据显示的过程是自动完成的。

v-model本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <p>Message is: {{message}}</p>
        <input v-model="message" placeholder="edit me" type="text">
    </div>
    <script>
        Vue.config.productionTip=false;
        Vue.config.devtools=false;
        new Vue({el: ‘#app‘,data(){return { message:‘‘ }}})
    </script>
</body>
</html>

渲染如下:

当我们在输入框输入内容时,Message is:后面会自动显示输入框里的内容,反过来当修改Vue实例的message时,输入框也会自动更新为该内容。

与事件的修饰符类似,v-model也有修饰符,用于控制数据同步的时机,v-model可以添加三个修饰符:lazy、number和trim,具体可以看官网。

我们如果不用v-model,手写一些事件也可以实现例子里的效果,如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <p>Message is: {{message}}</p>
        <input :value="message" @input="message=$event.target.value" placeholder="edit me" type="text">
    </div>
    <script>
        Vue.config.productionTip=false;
        Vue.config.devtools=false;
        new Vue({el: ‘#app‘,data(){return { message:‘‘ }}})
    </script>
</body>
</html>

我们自己手写的和用v-model有一点不同,就是当输入中文时,输入了拼音,但是没有按回车时,p标签也会显示message信息的,而用v-model实现的双向绑定是只有等到回车按下去了才会渲染的,这是因为v-model内部监听了compositionstart和compositionend事件,有兴趣的同学具体可以查看一下这两个事件的用法,网上教程挺多的。

源码分析



Vue是可以自定义指令的,其中v-model和v-show是Vue的内置指令,它的写法和我们的自定义指令是一样的,都保存到Vue.options.directives上,例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
</head>
<body>
    <script>
        console.log(Vue.options.directives)     //打印Vue.options.directives的值
    </script>
</body>
</html>

输出如下:

Vue内部通过extend(Vue.options.directives, platformDirectives); 将v-model和v-show的指令信息保存到Vue.options.directives里面,如下:

var platformDirectives = {                           //第8417行  内置指令 v-module和v-show  platformDirectives的意思是这两个指令和平台无关的,不管任何环境都可以用这两个指令
  model: directive,
  show: show
}

extend(Vue.options.directives, platformDirectives); //第8515行 将两个指令信息保存到Vue.options.directives里面

Vue的源码实现代码比较多,我们一步步来,以上面的第一个例子为例,当Vue将模板解析成AST对象解析到input时会processAttrs()函数,如下:

function processAttrs (el) {            //第9526行 对剩余的属性进行分析
  var list = el.attrsList;
  var i, l, name, rawName, value, modifiers, isProp;
  for (i = 0, l = list.length; i < l; i++) {      //遍历每个属性
    name = rawName = list[i].name;
    value = list[i].value;
    if (dirRE.test(name)) {                         //如果该属性以v-、@或:开头,表示这是Vue内部指令
      // mark element as dynamic
      el.hasBindings = true;
      // modifiers
      modifiers = parseModifiers(name);
      if (modifiers) {
        name = name.replace(modifierRE, ‘‘);
      }
      if (bindRE.test(name)) { // v-bind              //bindRD等于/^:|^v-bind:/ ,即该属性是v-bind指令时
       /*v-bind逻辑*/
      } else if (onRE.test(name)) { // v-on           //onRE等于/^@|^v-on:/,即该属性是v-on指令时
        /*v-on逻辑*/
      } else { // normal directives                   //普通指令
        name = name.replace(dirRE, ‘‘);                   //去掉指令前缀,比如v-model执行后等于model
        // parse arg
        var argMatch = name.match(argRE);                 //argRE等于:(.*)$/,如果name以:开头的话
        var arg = argMatch && argMatch[1];
        if (arg) {
          name = name.slice(0, -(arg.length + 1));
        }
        addDirective(el, name, rawName, value, arg, modifiers);   //执行addDirective给el增加一个directives属性,值是一个数组,例如:[{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}]
        if ("development" !== ‘production‘ && name === ‘model‘) {
          checkForAliasModel(el, value);
        }
      }
    } else {
      /*普通特性的逻辑*/
    }
  }
}

addDirective会给AST对象增加一个directives属性,用于保存对应的指令信息,如下:

function addDirective (     //第6561行 指令相关,给el这个AST对象增加一个directives属性,值为该指令的信息,比如:
  el,
  name,
  rawName,
  value,
  arg,
  modifiers
) {
  (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers });
  el.plain = false;
}

例子里的 <input v-model="message" placeholder="edit me" type="text">对应的AST对象如下:

接下来在generate生成rendre函数的时候,获取data属性时会执行genDirectives()函数,该函数会执行全局的model函数,也就是v-model的初始化函数,如下:

function genDirectives (el, state) {        //第10352行 获取指令
  var dirs = el.directives;                   //获取元素的directives属性,是个数组,例如:[{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}]
  if (!dirs) { return }                       //如果没有directives则直接返回
  var res = ‘directives:[‘;
  var hasRuntime = false;
  var i, l, dir, needRuntime;
  for (i = 0, l = dirs.length; i < l; i++) {        //遍历dirs
    dir = dirs[i];                                  //每一个directive,例如:{name: "model", rawName: "v-model", value: "message", arg: null, modifiers: undefined}
    needRuntime = true;
    var gen = state.directives[dir.name];           //获取对应的指令函数,如果是v-model,则对应model函数,可能为空的,只有内部指令才有
    if (gen) {
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.
      needRuntime = !!gen(el, dir, state.warn);       //执行指令对应的函数,也就是全局的model函数
    }
    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.arg) + "\"") : ‘‘) + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : ‘‘) + "},";
    }
  }
  if (hasRuntime) {
    return res.slice(0, -1) + ‘]‘                 //去掉最后的逗号,并加一个],最后返回
  }
}

model()函数会根据不同的tag(select、input的不同)做不同的处理,如下:

function model (      //第6854行 v-model指令的初始化
  el,
  dir,
  _warn
) {
  warn$1 = _warn;
  var value = dir.value;                                        //值
  var modifiers = dir.modifiers;                                //修饰符
  var tag = el.tag;                                             //标签名,比如:input
  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."
      );
    }
  }

  if (el.component) {
    genComponentModel(el, value, modifiers);
    // component v-model doesn‘t need extra runtime
    return false
  } else if (tag === ‘select‘) {                              //如果typ为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‘) {         //如果是input标签,或者是textarea标签
    genDefaultModel(el, value, modifiers);             //则执行genDefaultModel()函数
  } 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.‘
    );
  }

  // ensure runtime directive metadata
  return true
}

genDefaultModel会在el的value绑定对应的值 2.调用addHandler()添加对应的事件,如下:

function genDefaultModel (          //第6965行  nput标签 和textarea标签 el:AST对象 value:对应值
  el,
  value,
  modifiers
) {
  var type = el.attrsMap.type;                                  //获取type值,比如text,如果未指定则为undefined

  // warn if v-bind:value conflicts with v-model
  // except for inputs with v-bind:type
  {
    var value$1 = el.attrsMap[‘v-bind:value‘] || el.attrsMap[‘:value‘];           //尝试获取动态绑定的value值
    var typeBinding = el.attrsMap[‘v-bind:type‘] || el.attrsMap[‘:type‘];         //尝试获取动态绑定的type值
    if (value$1 && !typeBinding) {                                                //如果动态绑定了value 且没有绑定type,则报错
      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‘
      );
    }
  }

  var ref = modifiers || {};
  var lazy = ref.lazy;                                                        //获取lazy修饰符
  var number = ref.number;                                                    //获取number修饰符
  var trim = ref.trim;                                                        //获取trim修饰符
  var needCompositionGuard = !lazy && type !== ‘range‘;
  var event = lazy                                                            //如果有lazy修饰符则绑定为change事件,否则绑定input事件
    ? ‘change‘
    : type === ‘range‘
      ? RANGE_TOKEN
      : ‘input‘;

  var valueExpression = ‘$event.target.value‘;
  if (trim) {                                                                 //如果有trim修饰符,则在值后面加上trim()
    valueExpression = "$event.target.value.trim()";
  }
  if (number) {                                                               //如果有number修饰符,则加上_n函数,就是全局的toNumber函数
    valueExpression = "_n(" + valueExpression + ")";
  }

  var code = genAssignmentCode(value, valueExpression);                       //返回一个表达式,例如:message=$event.target.value
  if (needCompositionGuard) {                                                 //如果需要composing配合,则在前面加上一段if语句
    code = "if($event.target.composing)return;" + code;
  }

  //双向绑定就是靠着两行代码的
  addProp(el, ‘value‘, ("(" + value + ")"));                                  //添加一个value的prop
  addHandler(el, event, code, null, true);                                    //添加event事件
  if (trim || number) {
    addHandler(el, ‘blur‘, ‘$forceUpdate()‘);
  }
}

渲染完成后对应的render函数如下:

with(this){return _c(‘div‘,{attrs:{"id":"app"}},[_c(‘p‘,[_v("Message is: "+_s(message))]),_v(" "),_c(‘input‘,{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me","type":"text"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}})])}

我们整理一下就看得清楚一点,如下:

with(this) {
    return _c(‘div‘, {
        attrs: {
            "id": "app"
        }
    },
    [_c(‘p‘, [_v("Message is: " + _s(message))]), _v(" "), _c(‘input‘, {
        directives: [{
            name: "model",
            rawName: "v-model",
            value: (message),
            expression: "message"
        }],
        attrs: {
            "placeholder": "edit me",
            "type": "text"
        },
        domProps: {
            "value": (message)
        },
        on: {
            "input": function($event) {
                if ($event.target.composing) return;
                message = $event.target.value
            }
        }
    })])
}

最后等DOM节点渲染成功后就会执行events模块的初始化事件 并且会执行directive模块的inserted钩子函数:

var directive = {
  inserted: function inserted (el, binding, vnode, oldVnode) {      //第7951行
    if (vnode.tag === ‘select‘) {
      // #6903
      if (oldVnode.elm && !oldVnode.elm._vOptions) {
        mergeVNodeHook(vnode, ‘postpatch‘, function () {
          directive.componentUpdated(el, binding, vnode);
        });
      } else {
        setSelected(el, binding, vnode.context);
      }
      el._vOptions = [].map.call(el.options, getValue);
    } else if (vnode.tag === ‘textarea‘ || isTextInputType(el.type)) {      //如果tag是textarea节点,或者type为这些之一:text,number,password,search,email,tel,url
      el._vModifiers = binding.modifiers;                                       //保存修饰符
      if (!binding.modifiers.lazy) {                                            //如果没有lazy修饰符,先后绑定三个事件
        el.addEventListener(‘compositionstart‘, onCompositionStart);
        el.addEventListener(‘compositionend‘, onCompositionEnd);
        // Safari < 10.2 & UIWebView doesn‘t fire compositionend when
        // switching focus before confirming composition choice
        // this also fixes the issue where some browsers e.g. iOS Chrome
        // fires "change" instead of "input" on autocomplete.
        el.addEventListener(‘change‘, onCompositionEnd);
        /* istanbul ignore if */
        if (isIE9) {
          el.vmodel = true;
        }
      }
    }
  },

onCompositionStart和onCompositionEnd分别对应compositionstart和compositionend事件,如下:

function onCompositionStart (e) {       //第8056行
  e.target.composing = true;
}

function onCompositionEnd (e) {
  // prevent triggering an input event for no reason
  if (!e.target.composing) { return }   //如果e.target.composing为false,则直接返回,即保证不会重复触发
  e.target.composing = false;
  trigger(e.target, ‘input‘);               //触发e.target的input事件
}

function trigger (el, type) {           //触发el上的type事件 例如type等于:input
  var e = document.createEvent(‘HTMLEvents‘);   //创建一个HTMLEvents类型
  e.initEvent(type, true, true);                //初始化事件
  el.dispatchEvent(e);                           //向el这个元素派发e这个事件
}

最后执行的el.dispatchEvent(e)就会触发我们生成的render函数上定义的input事件

原文地址:https://www.cnblogs.com/greatdesert/p/11154239.html

时间: 2024-10-11 16:54:05

Vue.js 源码分析(二十二) 指令篇 v-model指令详解的相关文章

Vue.js 源码分析(二十六) 高级应用 作用域插槽 详解

普通的插槽里面的数据是在父组件里定义的,而作用域插槽里的数据是在子组件定义的. 有时候作用域插槽很有用,比如使用Element-ui表格自定义模板时就用到了作用域插槽,Element-ui定义了每个单元格数据的显示格式,我们可以通过作用域插槽自定义数据的显示格式,对于二次开发来说具有很强的扩展性. 作用域插槽使用<template>来定义模板,可以带两个参数,分别是: slot-scope    ;模板里的变量,旧版使用scope属性 slot              ;该作用域插槽的nam

Vue.js 源码分析(二十) 指令篇 v-once指令详解

数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值,例如:<span>Message: {{ msg }}</span>,以后每当msg属性发生了改变,插值处的内容都会自动更新. 可以给DOM节点添加一个v-once指令,这样模板只会在第一次更新时显示数据,此后再次更新该DOM里面引用的数据时,内容不会自动更新了,例如: <!DOCTYPE html> <html lang="en"> <head>

Vue.js 源码分析(二十三) 高级应用 自定义指令详解

除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令. 官网介绍的比较抽象,显得很高大上,我个人对自定义指令的理解是:当自定义指令作用在一些DOM元素或组件上时,该元素在初次渲染.插入到父节点.更新.解绑时可以执行一些特定的操作(钩子函数() 自定义指令有两种注册方式,一种是全局注册,使用Vue.directive(指令名,配置参数)注册,注册之后所有的Vue实例都可以使用,另一种是局部注册,在创建Vue实例时通过directives属性创建局部指令,局

Vue.js 源码分析(十) ref属性详解

用法 ref 被用来给元素或子组件注册引用信息.引用信息将会注册在父组件的 $refs 对象上.如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素:如果用在子组件上,引用就指向组件实例,例如: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <scrip

ABP源码分析三十二:ABP.SignalR

Realtime Realtime是ABP底层模块提供的功能,用于管理在线用户.它是使用SignalR实现给在线用户发送通知的功能的前提 IOnlineClient/OnlineClient: 封装在线用户的信息 OnlineClientManager/IOnlineClientManager: 用于提供基本维护在线用户的方法.其内部维护了一个字典来保存在线的客户信息. SingalR SignalRRealTimeNotifier: 实现了给在线用户发送通知的功能.其从IOnlineClien

three.js 源码注释(十二)Math/Box3.js

商域无疆 (http://blog.csdn.net/omni360/) 本文遵循"署名-非商业用途-保持一致"创作公用协议 转载请保留此句:商域无疆 -  本博客专注于 敏捷开发及移动和物联设备研究:数据可视化.GOLANG.Html5.WEBGL.THREE.JS,否则,出自本博客的文章拒绝转载或再转载,谢谢合作. 俺也是刚开始学,好多地儿肯定不对还请见谅. 以下代码是THREE.JS 源码文件中Math/Box3.js文件的注释. 更多更新在 : https://github.c

Vue.js 源码分析(十七) 指令篇 v-if、v-else-if和v-else 指令详解

v-if 指令用于条件性地渲染一块内容.这块内容只会在指令的表达式返回 truthy 值的时候被渲染. v-else-if,顾名思义,充当 v-if 的“else-if 块”,可以连续使用: 也可以使用 v-else 指令来表示 v-if 的“else 块”: 挺好理解的,就和大多数的语言的if()....else if()...else逻辑语句是一样的,例如: <!DOCTYPE html> <html lang="en"> <head> <

Redis源码分析(十二)--- redis-check-dump本地数据库检测

这个文件我在今天分析学习的时候,一直有种似懂非懂的感觉,代码量700+的代码,最后开放给系统的就是一个process()方法.这里说的说的数据库检测,是针对key的检测,会用到,下面提到的结构体: /* Data type to hold opcode with optional key name an success status */ /* 用于key的检测时使用,后续检测操作都用到了entry结构体 */ typedef struct { //key的名字 char* key; //类型

vue.js源码学习分享(二)

/** * Check if value is primitive//检查该值是否是个原始值 */ function isPrimitive (value) { return typeof value === 'string' || typeof value === 'number' } /** * Create a cached version of a pure function.//创建一个纯粹的函数的缓存版本 */ function cached (fn) { var cache = O