Vue3疑问系列(7) — v-model(vModelSelect)指令是如何工作的?

938 阅读3分钟

前言

本文来聊聊 “<select v-model="variable"></select>” 其内部又是如何工作的?

看这篇文章前,一定要把官网中的 表单输入绑定[选择框 (Select)例子看懂] 官网教程

尝试编写 vModelSelect 指令对象

根据前面几篇文章的讲解,其实双向绑定原理的套路都一样,加上上一篇也讲解了looseEqual.ts的实现.
所以这次就不编写 vModelSelect 指令对象了, 因为比较简单.

小栗子

如果你已经看完官网的例子,那我建议你看下我写的7个小例子

  • should work with single select
  • multiple select (model is Array)
  • v-model.number should work with select tag
  • v-model.number should work with select tag
  • multiple select (model is Array, option value is object)
  • multiple select (model is Set)
  • multiple select (model is Set, option value is object)

这7个小例子其实是vue-next单测中的,怕有的同学看不懂,所以改写成大家一眼就能看明白的例子。
你可以狠狠的点我去看看

vModelSelect 内部实现

vModelSelect源码 runtime-dom/src/directives/vModel.ts

export const vModelSelect: ModelDirective<HTMLSelectElement> = {
  created(el, { value, modifiers: { number } }, vnode) { // eg: <select multiple v-model="value"></select>`
    const isSetModel = isSet(value) // value 是不是 set实例
    addEventListener(el, 'change', () => { // el 监听 change 事件
      const selectedVal = Array.prototype.filter
        .call(el.options, (o: HTMLOptionElement) => o.selected) // 拿到选中的 option dom
        .map(
          (o: HTMLOptionElement) =>
            number ? toNumber(getValue(o)) : getValue(o) // 获取到 option dom 上绑定的value值
        )
      el._assign( // 调用 onUpdate:modelValue函数 给 value 赋值
        el.multiple
          ? isSetModel
            ? new Set(selectedVal)
            : selectedVal
          : selectedVal[0]
      )
    })
    el._assign = getModelAssigner(vnode) // 拿到 onUpdate:modelValue函数
  },
  // set value in mounted & updated because <select> relies on its children
  // <option>s.
  mounted(el, { value }) { // 元素安装都是先子后父,所以需要在 mounted钩子中
    setSelected(el, value) // 反选
  },
  beforeUpdate(el, _binding, vnode) {
    el._assign = getModelAssigner(vnode) // 更新前 重新获取 onUpdate:modelValue函数
  },
  updated(el, { value }) {
    setSelected(el, value) // 反选
  }
}

function setSelected(el: HTMLSelectElement, value: any) {
  const isMultiple = el.multiple // select dom 是否含有 multiple 属性
  if (isMultiple && !isArray(value) && !isSet(value)) { // 有的话绑定的值只能是数组或者set实例
    __DEV__ &&
      warn(
        `<select multiple v-model> expects an Array or Set value for its binding, ` +
          `but got ${Object.prototype.toString.call(value).slice(8, -1)}.`
      )
    return
  }
  for (let i = 0, l = el.options.length; i < l; i++) { // 遍历select下的所有option dom
    const option = el.options[i] // 当前的 option dom
    const optionValue = getValue(option) // 拿到 当前的 option dom 上绑定的值
    if (isMultiple) { // 多选
      if (isArray(value)) { // 数组 eg: v-model="value" value是array
        option.selected = looseIndexOf(value, optionValue) > -1  //找出索引判断结果
      } else { // set实例 eg: v-model="value" value是new Set()实例
        option.selected = value.has(optionValue) // 判断当前option的value值是否在value中,进行反选
      }
    } else {
      if (looseEqual(getValue(option), value)) { // 不是多选的话,直接根据比较结果给select.selectedIndex赋值(比如select 下有2个option,当 select.selectedIndex = 1时,第二个option dom就会被选中)
        el.selectedIndex = i
        return
      }
    }
  }
  if (!isMultiple) { // 单选时 且没有选中任何options则把select.selectedIndex置为-1(就是select没选中)
    el.selectedIndex = -1
  }
}

// retrieve raw value set via :value bindings
function getValue(el: HTMLOptionElement | HTMLInputElement) {
  return '_value' in el ? (el as any)._value : el.value
}
  1. 逐行解释都写在代码中,不是考虑到有的同学看不懂,我真不想写注解
    看源码其实只要明白其思路即可,这样自己就能根据思路去实现,哪天源码因修改bug而改变了点,那难道就慌了?

  2. vModelSelec 实现原理

    • 模板编译
       <select v-model="variable"></select>
    
       会编译成如下:
    
       (function anonymous() {
           const _Vue = Vue
    
           return function render(_ctx, _cache) {
               with(_ctx) {
                   const {
                       vModelSelect: _vModelSelect,
                       createVNode: _createVNode,
                       withDirectives: _withDirectives,
                       openBlock: _openBlock,
                       createBlock: _createBlock
                   } = _Vue
    
                   return _withDirectives((_openBlock(), _createBlock("select", {
                       "onUpdate:modelValue": $event => (variable = $event)
                   }, null, 8 /* PROPS */ , ["onUpdate:modelValue"])), [
                       [_vModelSelect, variable]
                   ])
               }
           }
       })
    
      <select multiple v-model="variable"></select>
    
      会编译成如下:
    
      (function anonymous() {
          const _Vue = Vue
    
          return function render(_ctx, _cache) {
              with(_ctx) {
                  const {
                      vModelSelect: _vModelSelect,
                      createVNode: _createVNode,
                      withDirectives: _withDirectives,
                      openBlock: _openBlock,
                      createBlock: _createBlock
                  } = _Vue
    
                  return _withDirectives((_openBlock(), _createBlock("select", {
                      multiple: "",
                      "onUpdate:modelValue": $event => (variable = $event)
                  }, null, 8 /* PROPS */ , ["onUpdate:modelValue"])), [
                      [_vModelSelect, variable]
                  ])
              }
          }
      })
    

    上面的结果有共同的特点,就是指令最终会编译成使用withDirectives调用,其次v-model编译后,还多一个 "onUpdate:modelValue": $event => (variable = $event),其实看到这里,就算不看vModelSelect源码,我自己也能实现vModelSelect指令对象了.

    • 为了更好的理解原理,我们先来约定几个关键词,不然,我解释的时候,可能听不懂我在说啥

        eg: 
        <select v-model="variable">
            <option value="foo">foo</option>
            <option value="bar">bar</option>
        </select>`
      

      a. 绑定值: 响应式变量 variable

      b. onUpdate:modelValue: 模板编译生成的,它是一个函数$event => (variable = $event)

      c. select dom: select 标签元素

      d. option doms: select 标签元素下所有的option标签元素

      e. option dom: select 标签元素下的某个option标签元素

      f. option dom value: 比如<option value="bar">bar</option>他的value就是 bar值

    • 先思考下什么是双向绑定?(下面是我针对<select v-model="variable"></select>使用的解释)

      a. 当用户选择option dom触发change事件时,会把option dom value赋值给绑定值

      b. 绑定值数据发生变化后,会对option doms进行反选

      c. 我就是这么理解的

    • 看看上面尤大写的vModelSelect是不是这个思路:

      a: created钩子中给select dom注册了change事件,当用户选择时会触发回调。回调中会根据option doms遍历筛选出选中option domoption dom value,然后调用onUpdate:modelValue函数给绑定值赋值。

      b. mounted和updated钩子中调用setSelected方法进行反选。setSelected方法中主要是遍历option doms,遍历过程中如果是多选则根据option dom value是否在绑定值中,然后给option dom的selected赋值结果,从而达到多选;如果不是多选,则调用looseEqual(option dom value, 绑定值)得出结果,然后给select dom的selectedIndex赋值索引,从而达到单选。

      c.尤大的实现和上面思考的什么是双向绑定,其实是差不多的。

总结

其实vModelText vModelCheckbox vModelRadio vModelSelect的实现思路都一样,只是实现的时候注册的事件和el 反选的属性不太一样,反选的条件都是调用looseEqual.ts中的宽松比较方法。

下篇: Vue3疑问系列(8) — 组件上使用v-model, 是如何工作的?