vue3源码--v-model之vModelText

430 阅读3分钟

大家好,我是前端小喵

WechatIMG18166.jpg

带着问题看源码

1input在IME(中文,日文和韩文等) 拼字阶段输入阶段,为什么input事件监听不到(原生事件是可以监听到的),是v-model源码做了内部处理吗
2、v-model.lazy,v-model.trim,v-model.number分别是怎么处理的

一、vModelText源码

看到这一坨难免有点懵,其实可以拆几个模块去理解这段代码

    export const vModelText: ModelDirective<
      HTMLInputElement | HTMLTextAreaElement
    > = {
      created(el, { modifiers: { lazy, trim, number } }, vnode) {
        el._assign = getModelAssigner(vnode)
        const castToNumber =
          number || (vnode.props && vnode.props.type === 'number')
        addEventListener(el, lazy ? 'change' : 'input', e => {
          if ((e.target as any).composing) return
          let domValue: string | number = el.value
          if (trim) {
            domValue = domValue.trim()
          }
          if (castToNumber) {
            domValue = looseToNumber(domValue)
          }
          el._assign(domValue)
        })
        if (trim) {
          addEventListener(el, 'change', () => {
            el.value = el.value.trim()
          })
        }
        if (!lazy) {
          addEventListener(el, 'compositionstart', onCompositionStart)
          addEventListener(el, '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.
          addEventListener(el, 'change', onCompositionEnd)
        }
      },
      // set value on mounted so it's after min/max for type="range"
      mounted(el, { value }) {
        el.value = value == null ? '' : value
      },
      beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
        el._assign = getModelAssigner(vnode)
        // avoid clearing unresolved text. #2302
        if ((el as any).composing) return
        if (document.activeElement === el && el.type !== 'range') {
          if (lazy) {
            return
          }
          if (trim && el.value.trim() === value) {
            return
          }
          if (
            (number || el.type === 'number') &&
            looseToNumber(el.value) === value
          ) {
            return
          }
        }
        const newValue = value == null ? '' : value
        if (el.value !== newValue) {
          el.value = newValue
        }
      }
    }
    

二、代码分析

2.1、created:对数据变化做事件监听

  1、保存获取更新数值方法(保证传入更新值,无论是一个v-model绑定还是多个v-model绑定,都会更新所有v-model绑定的值)
     el._assign = getModelAssigner(vnode)
     
     // 可以跳过:getModelAssigner内部逻辑
       var getModelAssigner = (vnode) => {
       // 更新v-model值方法
        const fn = vnode.props["onUpdate:modelValue"] || false;
        // 默认返回更新数值方法fn  
        // 多个v-model绑定的时候,fn为数组,返回函数
           入参:当前值  遍历fn去更新所有数值
        return isArray(fn) ? (value) => invokeArrayFns(fn, value) : fn;
      };
      
      var invokeArrayFns = (fns, arg) => {
        for (let i = 0; i < fns.length; i++) {
          fns[i](arg);
        }
      };
  2、保存变量castToNumber是否要做number处理
      // 如果v-model修饰符为numer v-model.number 或者 虚拟dom vnode的props.type 为 'number'(例如 input 的type为number情况)
      const castToNumber = number || vnode.props && vnode.props.type === "number";
  3、监听数据变化
      // 根据是否有修饰符 lazy 决定监听change还是input
      addEventListener(el, lazy ? "change" : "input", (e) => {
        //  IME(中文,日文和韩文等) 拼字阶段加锁(下面的 !lazy处理的compositionstart、compositionend)
        //  在!lazy的compositionstart、compositionend之间 不断触发input事件,被锁住 return出去不做更新值处理
        if (e.target.composing) return;
        let domValue = el.value;
        // trim处理
        if (trim) {
          domValue = domValue.trim();
        }
        // number处理:
        // looseToNumber工具函数 数字格式化处理
        if (castToNumber) {
          domValue = looseToNumber(domValue);
        }
         // 一开始保存的更新值方法 直接更新数值
        el._assign(domValue);
      });
      
        // trim 就监听change 对最后值做trim处理
      if (trim) {
        addEventListener(el, "change", () => {
          el.value = el.value.trim();
        });
      }
      
        // !lazy的时候 监听(compositionstart、compositionend、change事件)
      if (!lazy) {
        // compositionstart比input更先执行 只执行一次 IME拼字开始时 加锁
        addEventListener(el, "compositionstart", onCompositionStart);
         // compositionend比input更先执行 只执行一次 IME拼字结束 解锁 触发input事件
        addEventListener(el, "compositionend", onCompositionEnd);
          // 监听change 调用 解锁 触发input事件
        addEventListener(el, "change", onCompositionEnd);
      }
      
      
      
      // 可以跳过:looseToNumber函数、onCompositionStart、onCompositionEnd
          // parseFloat处理,值为NaN直接返回val 否则返回parseFloat处理的值
          var looseToNumber = (val) => {
            const n = parseFloat(val);
            return isNaN(n) ? val : n;
          };
          // onCompositionStart 加锁
          function onCompositionStart(e) {
            e.target.composing = true;
          }
          // onCompositionEnd 解锁 触发input事件
          function onCompositionEnd(e) {
            const target = e.target;
            if (target.composing) {
              target.composing = false;
              target.dispatchEvent(new Event("input"));
            }
          }
          

2.2、mounted:赋值到dom的value属性上

  mounted(el, { value }) {
      // 赋值dom  值为null 则赋值dom为 ''
      el.value = value == null ? "" : value;
    },
    

2.3、beforeUpdate: 更新值前的各种校验

beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
        // 更新值方法
      el._assign = getModelAssigner(vnode);
      // IME(中文,日文和韩文等) 拼字阶段 return
      if (el.composing)
        return;
        // 当前获得焦点的元素为当前dom 并且dom type不为range
      if (document.activeElement === el && el.type !== "range") {
         // lazy 监听的change事件 return
        if (lazy) {
          return;
        }
        // trim前后值相等
        if (trim && el.value.trim() === value) {
          return;
        }
        // number处理前后值相等
        if ((number || el.type === "number") && looseToNumber(el.value) === value) {
          return;
        }
      }
      // 值为null 置空字符串
      const newValue = value == null ? "" : value;
      // 前后值不相等就更新
      if (el.value !== newValue) {
        el.value = newValue;
      }
    }
    

三、总结

1、created: 对lazy、非lazy、trim不同情况,做对应的事件监听;非lazy有IME拼字阶段优化、trim、number有相对应的数据处理

2、mounted:赋值到dom的value属性上

3、更新值前的各种校验: 是否在拼字阶段; 当前获得焦点的元素为当前dom 并且dom type不为range前提下,修饰符是否为lazy、trim前后值相等、number处理前后值相等;值为null 置空字符串,值前后是否相等