最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue3疑问系列(5) — v-model(vModelCheckbox)指令是如何工作的?

    正文概述 掘金(徐志伟酱)   2021-03-02   692

    前言

    尝试编写 vModelCheckbox 指令对象

      思考:如果你看懂官网的的例子和我写的那4个小例子,在介绍vModelCheckbox实现之前,思考下,如果让你来实现一个vModelCheckbox对象,你会怎么写?
    

    例子展示

    这个例子就是通过实现一个自定义的vModelCheckbox指令对象来达到双向绑定 可以狠狠的点我去看看

    const vModelCheckbox = (() => {
          const listener = (type = 'on') => {
              return (el, evt, handler, useCapture = false) => {
                  if (el && evt && handler) {
                      el[type == 'on' ? 'addEventListener' : 'removeEventListener'](evt, handler, useCapture)
                  }
              }
          }
          const on = listener('on')
          const off = listener('off')
    
          const hasOwnProperty = Object.prototype.hasOwnProperty
    
          const hasCustomValue = (vnode) => {
              const props = vnode.props || {}
              return hasOwnProperty.call(props, 'true-value') && hasOwnProperty.call(props, 'false-value')
          }
    
          const getCheckboxValue = (el, vnode) => {
              const props = vnode.props || {}
              if (hasCustomValue(vnode)) {
                  const trueValue = props['true-value'], falseValue = props['false-value']
                  return el.checked ? trueValue : falseValue
              } else {
                  return el.checked
              }
          }
    
          const setMapValue = (el, vnode) => {
              const props = vnode['props'] || {}
              el._mapData = new Map([[
                  props['true-value'] || true, true
              ], [
                  props['false-value'] || false, false
              ]])
          }
    
          const getMapValue = (el, key) => {
              return el._mapData.get(key)
          }
    
          return {
              created(el, binding, vnode, preVnode) {
                  const { value } = binding
    
                  on(el, 'change', el._handleEvt = (evt) => {
                      const fn = vnode['props'] && vnode['props']['onUpdate:modelValue']
    
                      const domValue = vnode['props']['value']
                      const checked = evt.target.checked
    
                      if (Array.isArray(value)) {
                          if (checked && !value.includes(domValue)) {
                              value.push(domValue)
                              fn && fn(value)
                          } else if (!checked && value.includes(domValue)) {
                              const i = value.findIndex(m => m === domValue)
                              if (i !== -1) {
                                  value.splice(i, 1)
                                  fn && fn(value)
                              }
                          }
                      } else if (value instanceof Set) {
                          if (checked) {
                              fn && fn(value.add(domValue))
                          } else if (!checked && value.delete(domValue)) {
                              fn && fn(value)
                          }
                      } else {
                          fn && fn(getCheckboxValue(el, vnode))
                      }
                  })
              },
              mounted(el, binding, vnode, preVnode) {
                  setMapValue(el, vnode)
              },
              beforeUpdate(el, binding, vnode, preVnode) {
                  setMapValue(el, vnode)
                  const { value, oldValue } = binding
                  const props = vnode['props']
                  const fn = props && props['onUpdate:modelValue']
    
                  if (Array.isArray(value)) {
                      el.checked = value.includes(props['value'])
                  } else if (value instanceof Set) {
                      el.checked = value.has(props['value'])
                  } else if (value !== oldValue) {
                      el.checked = getMapValue(el, value)
                  }
              },
              beforeUnmount(el) {
                  el._mapData = null
                  off(el, 'change', el._handleEvt)
              }
          }
      })()
    
      Vue.vModelCheckbox = vModelCheckbox
    
    1. 上面的代码不考虑代码写的咋样, 功能起码完成了。注意,引用类型的数据,回显时必须使用同一个引用数据,因为我内部使用了[].includes和set.has方法来判断的.

    2. 比起Vue3 v-molde="数组", 当数组的值是引用类型的数据时,Vue3 回显是不需要同一个引用对象,就能进行回显,但是使用 Vue3 v-molde="set实例",如果set实例的成员是引用数据时,回显时set实例的成员必须是同一个引用类型数据。这样Vue3 v-molde="set实例"和Vue3 v-molde="数组"的回显的使用姿势不保持一致了.

    3. 上面代码实现的思路

    • 在created钩子中注册change事件,每次复选框选中或者不选中时,触发该事件,v-model="绑定值", 根据绑定值的类型求出value值然后赋值给绑定值
    • 在beforeUpdate钩子中, 根据绑定值的类型,和vnode[props][value]的值求出是否被选中,然后赋值给el.checked
    • 这样就达到了双向绑定

    小栗子

    小栗子就不贴代码了,但是想要理解源码,最好要看下,不然下面的实现,可能会看不懂.
    关于本次源码讲解的的4个小例子

    使用姿势都会了,那接下来看看尤大是如何实现的(看看那个使用姿势的问题到底是哪行代码引起的)

    vModelCheckbox内部实现

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

    export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
      created(el, _, vnode) {
        el._assign = getModelAssigner(vnode) // 拿到 onUpdate:modelValue 函数
        addEventListener(el, 'change', () => {
          const modelValue = (el as any)._modelValue // 获取绑定的值eg: v-model="arr" 这里的 modelValue就是 arr
          const elementValue = getValue(el) // 获取el的的value eg: <input v-model="arr" :value="{ name: 'xzw' }" /> 这里的elementValue就是 { name: 'xzw' }
          const checked = el.checked // input checkbox 的选中状态
          const assign = el._assign
          if (isArray(modelValue)) { // v-model="arr"
            const index = looseIndexOf(modelValue, elementValue) // 使用比较宽松的方式找出elementValue在modelValue的索引(这样,不是同一个引用对象,也能进行回显)
            const found = index !== -1
            if (checked && !found) { // 选中且没有找到
              assign(modelValue.concat(elementValue)) // 合并elementValue到modelValue上,然后给arr赋值
            } else if (!checked && found) { // 未选中且找到了
              const filtered = [...modelValue]
              filtered.splice(index, 1) // 删除已存在且未选中的
              assign(filtered) // 给arr赋值
            }
          } else if (isSet(modelValue)) { // v-model="set实例"
            const cloned = new Set(modelValue)
            if (checked) { // 选中则添加(set实例不会重复添加)
              cloned.add(elementValue)
            } else { // 否则未选中则删除
              cloned.delete(elementValue)
            }
            assign(cloned) // 给 set实例赋值
          } else { // 处理原始值类型
            assign(getCheckboxValue(el, checked)) // getCheckboxValue获取真实的value值后给绑定值赋值
          }
        })
      },
      // set initial checked on mount to wait for true-value/false-value
      mounted: setChecked, // 回显
      beforeUpdate(el, binding, vnode) {
        el._assign = getModelAssigner(vnode) // 获取onUpdate:modelValue函数(为了每次更新都使用最新的绑定函数)
        setChecked(el, binding, vnode) // 回显
      }
    }
    
    function setChecked(
      el: HTMLInputElement,
      { value, oldValue }: DirectiveBinding,
      vnode: VNode
    ) {
      // store the v-model value on the element so it can be accessed by the
      // change listener.
      ;(el as any)._modelValue = value // 给el添加_modelValue属性 : eg: v-model="arr" 这里的 value 就是 arr
      if (isArray(value)) { // 数组
        el.checked = looseIndexOf(value, vnode.props!.value) > -1 // eg: <input v-model="arr" :value="{ name: 'xzw' }" /> 这里的vnode.props!.value就是 { name: 'xzw' }, 根据vnode.props!.value的值是否在value中,来进行回显
      } else if (isSet(value)) { // set实例
        el.checked = value.has(vnode.props!.value) // 根据set.has来判断(如果set的成员是引用对象,回显时必须同同一个引用对象才能回显,这就和上面数组的实现有差异了。)
      } else if (value !== oldValue) { // 原始值类型
        el.checked = looseEqual(value, getCheckboxValue(el, true)) // 根据a,b的判断是否相等再赋值el.checked
      }
    }
    
    // retrieve raw value for true-value and false-value set via :true-value or :false-value bindings
    function getCheckboxValue(
      el: HTMLInputElement & { _trueValue?: any; _falseValue?: any },
      checked: boolean
    ) {
      const key = checked ? '_trueValue' : '_falseValue'
      return key in el ? el[key] : checked
    }
    
    // retrieve raw value set via :value bindings
    function getValue(el: HTMLOptionElement | HTMLInputElement) {
      return '_value' in el ? (el as any)._value : el.value
    }
    
    1. 上面的实现不难理解,和 '尝试编写 vModelCheckbox 指令对象' 的思路差不多。

    2. 注解都写在代码中了,所以不重复解释了,这里主要说下他和 '尝试编写 vModelCheckbox 指令对象' 实现的差异:

    • getCheckboxValue 方法中的 _trueValue 和 _falseValue属性,这2个属性何时给el添加上去的?
      我们知道安装元素vnode的时候,会把vnode上的属性,class,style和事件都添加到创建好的el上.
      [后面会单独写一篇关于vnode如何变成el,属性和事件如何添加上去的,这里不做过多介绍]

    patchProp 源码地址

      export const patchProp: DOMRendererOptions['patchProp'] = (
        el,
        key,
        prevValue,
        nextValue,
        isSVG = false,
        prevChildren,
        parentComponent,
        parentSuspense,
        unmountChildren
      ) => {
        switch (key) {
          // special
          case 'class':
            patchClass(el, nextValue, isSVG)
            break
          case 'style':
            patchStyle(el, prevValue, nextValue)
            break
          default:
            if (isOn(key)) {
              // ignore v-model listeners
              if (!isModelListener(key)) {
                patchEvent(el, key, prevValue, nextValue, parentComponent)
              }
            } else if (shouldSetAsProp(el, key, nextValue, isSVG)) {
              patchDOMProp(
                el,
                key,
                nextValue,
                prevChildren,
                parentComponent,
                parentSuspense,
                unmountChildren
              )
            } else {
              // special case for <input v-model type="checkbox"> with
              // :true-value & :false-value
              // store value as dom properties since non-string values will be
              // stringified.
              if (key === 'true-value') {
                ;(el as any)._trueValue = nextValue
              } else if (key === 'false-value') {
                ;(el as any)._falseValue = nextValue
              }
              patchAttr(el, key, nextValue, isSVG)
            }
            break
        }
    }
    

    原来如此,难怪能拿到我们绑定的引用数据,而在'尝试编写 vModelCheckbox 指令对象'中,我是通过vnode['props']['value']来获取的,似乎也没有问题.

    • getValue方法中的 _value属性又是何时添加上去的呢?

    在上面的 patchProp 方法内部会调用 patchDOMProp 方法

    patchDOMProp 源码

      export function patchDOMProp(
        el: any,
        key: string,
        value: any,
        // the following args are passed only due to potential innerHTML/textContent
        // overriding existing VNodes, in which case the old tree must be properly
        // unmounted.
        prevChildren: any,
        parentComponent: any,
        parentSuspense: any,
        unmountChildren: any
      ) {
        // ...
    
        if (key === 'value' && el.tagName !== 'PROGRESS') {
          // store value as _value as well since
          // non-string values will be stringified.
          el._value = value
          const newValue = value == null ? '' : value
          if (el.value !== newValue) {
            el.value = newValue
          }
          return
        }
    
        // ...
    }
    

    原来还是在patchProp时添加的,这样就很方便拿到原始值或者true-value|false-value的值,而不必像'尝试编写 vModelCheckbox 指令对象'实现中通过setMapValue设值和通过getMapValue来取值.

    1. 在beforeUpdate钩子中,他的实现采用宽松的比较方式去比较的,有点类似鸭式辨型的思想,像鸭子一样嘎嘎叫且用2条腿行走的动物就认为它是鸭子.
      而在'尝试编写 vModelCheckbox 指令对象'采用 [].includes 和 set.has 方法来判断的

      宽松的比较代码,比较简单,就不黏贴了,可以点我去查看

    总结

    使用: v-model作用在 <input type="checkbox" v-mode="绑定值" /> 绑定值可以是原始值变量或者数组或者set实例

    实现:

    • 在created钩子中注册change事件,事件触发后根据绑定值的类型求出value值然后赋值给绑定值
    • 在beforeUpdate钩子中根据绑定值的类型和vnode[props][value]的值求出是否被选中然后赋值给el.checked
    • 从而达到双向绑定

    下篇: Vue3疑问系列(5) — v-model(vModelRadio)指令是如何工作的?


    起源地下载网 » Vue3疑问系列(5) — v-model(vModelCheckbox)指令是如何工作的?

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元