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

    正文概述 掘金(徐志伟酱)   2021-02-27   960

    前言

    尝试编写 vModelText 指令对象

      提醒:如果你看懂官网的那2个例子,且对render函数有点了解,那下面这个例子可以先看下.
    

    例子展示

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

    const vModelText = (() => {
    
          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')
    
          return {
              created(el, binding, vnode) {
                  const { number, trim, lazy } = binding.modifiers
    
                  on(el, lazy ? 'change' : 'input', el._handleEvt = (e) => {
                      let target = e.target, domValue = target.value
    
                      if (trim) {
                          domValue = domValue.trim()
                      } else if (number || el.type == 'number') {
                          domValue = isNaN(parseFloat(domValue)) ? domValue : parseFloat(domValue)
                      }
    
                      const fn = vnode['props'] && vnode['props']['onUpdate:modelValue']
                      if (fn) fn(domValue)
                  })
              },
              beforeUpdate(el, binding) {
                  const { number, trim, lazy } = binding.modifiers
                  if (el.value !== binding.value) {
                      el.value = binding.value
                  }
              },
              beforeUnmount(el, { modifiers: { lazy } }) {
                  off(el, lazy ? 'change' : 'input', el._handleEvt)
              }
          }
      })()
    

    例子实现讲解

         <input v-model="value" />
    
    1. 双向绑定的实现原理(下面只是input[type为text和number和range]、textarea的实现原理):
    • 在指令created钩子中, 监听input\color{red}{ input }input元素的input事件\color{red}{ input事件 }input事件;
    • 用户输入时当值发生变化,触发input事件,重新赋值给响应式value\color{red}{value}value变量;
    • 当响应式值value\color{red}{ value }value改变时,调用指令的beforeUpdate钩子,这个钩子函数中把响应式value\color{red}{ value }value的值赋值给input这个dom元素\color{red}{ input这个dom元素 }input这个dom元素;
    1. 上面例子的代码就不细讲(举这个例子就是想让大家养成看源码前,先学会思考,如果是你来实现vModelText,你会怎么想),瞅瞅就好,下面才开始进入正题.

    小栗子

    1. 本次举的小例子, 不是源码中的单侧,而是我编写的小例子,为了让大家更直观的去理解.
        <div id="app"></div>
    
        const { render, defineComponent, h, withDirectives, vModelText } = Vue
    
        const root = document.querySelector('#app')
    
        const component = defineComponent({
            data() {
                return { value: null }
            },
            template: `
                <div>
                    <input v-model="value" />
                    <p>{{ value }}</p>
                </div>
            `
        })
    
        render(h(component), root)
    

    这个例子很简单,就是在input\color{red}{ input }input上使用了vmodel\color{red}{ v-model }v−model然后把value\color{red}{ value }value的值实时的展示在p\color{red}{ p }p元素上.

    function render() {
        const _this = this
        return h('div', [
            withDirectives(h('input', {
                'onUpdate:modelValue'($event) {
                    _this.value = $event
                }
            }), [[vModelText , this.value ]]),
            h('p', this.value)
        ])
    }
    

    上面的模板其实可以写成这样的render函数,可以看到本质还是调用了withDirectives方法.

    function render(_ctx, _cache) {
      with (_ctx) {
        const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, toDisplayString: _toDisplayString, openBlock: _openBlock, createBlock: _createBlock } = _Vue
    
        return (_openBlock(), _createBlock("div", null, [
          _withDirectives(_createVNode("input", {
            "onUpdate:modelValue": $event => (value = $event)
          }, null, 8 /* PROPS */, ["onUpdate:modelValue"]), [
            [_vModelText, value]
          ]),
          _createVNode("p", null, _toDisplayString(value), 1 /* TEXT */)
        ]))
      }
    }
    
    • 这个render函数是运行时根据上面的模板通过Vue.compile编译出来的,可以看到模板编译的render函数和上面写的render函数类似
    • 从我写的render函数或者通过Vue.compile编译出来的render函数来看
       <input v-model="value" />
       被编译成了
       withDirectives(h('input', {
          'onUpdate:modelValue'($event) {
              _this.value = $event
          }
       }), [[vModelText , this.value ]])
    
    • 当input元素触发input事件时,会调用onUpdate:modelValue函数从而对value进行赋值更新
    • 当响应式value更改触发指令beforeUpdate钩子时,会把响应式value的值赋值给input元素
    • 其实就是这么简单,那接下来看看尤大是如何实现的

    vModelText内部实现

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

    
    const getModelAssigner = (vnode: VNode): AssignerFn => {
      const fn = vnode.props!['onUpdate:modelValue']
      return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
    }
    
    function onCompositionStart(e: Event) {
      ;(e.target as any).composing = true
    }
    
    function onCompositionEnd(e: Event) {
      const target = e.target as any
      if (target.composing) {
        target.composing = false
        trigger(target, 'input')
      }
    }
    
    function trigger(el: HTMLElement, type: string) {
      const e = document.createEvent('HTMLEvents')
      e.initEvent(type, true, true)
      el.dispatchEvent(e)
    }
    
    // We are exporting the v-model runtime directly as vnode hooks so that it can
    // be tree-shaken in case v-model is never used.
    export const vModelText: ModelDirective<
      HTMLInputElement | HTMLTextAreaElement
    > = {
      created(el, { modifiers: { lazy, trim, number } }, vnode) {
        el._assign = getModelAssigner(vnode)
        const castToNumber = number || el.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()
          } else if (castToNumber) {
            domValue = toNumber(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: { trim, number } }, vnode) {
        el._assign = getModelAssigner(vnode)
        // avoid clearing unresolved text. #2302
        if ((el as any).composing) return
        if (document.activeElement === el) {
          if (trim && el.value.trim() === value) {
            return
          }
          if ((number || el.type === 'number') && toNumber(el.value) === value) {
            return
          }
        }
        const newValue = value == null ? '' : value
        if (el.value !== newValue) {
          el.value = newValue
        }
      }
    }
    
    1. 从实现来看,比上面尝试写的vModelText的实现,要多好多其他情况的处理

    2. 当执行created钩子时:

      • 拿到需要执行的函数: 通过getModelAssigner方法,从vnode的props上提取 onUpdate:modelValue的函数,对于本次的小栗子其实就是
       "onUpdate:modelValue": $event => (value = $event)
      

      后面的那个函数

      • 根据 lazy 来给 input 元素注册 change 或者 input 事件
      • 根据 trim 来给 input 元素再次注册一个change事件
      • 根据 lazy 来给 input 元素分别注册 compositionstart compositionend change 事件为了修复Safari < 10.2的bug
    3. 当执行mounted钩子时:

      • 为 type="range" 的 input元素 做特殊处理
    4. 当执行beforeUpdate钩子时:

      • 拿到需要执行的函数
      • 避免清除未解析的文本
      • 给input.value赋值
    5. 上面代码小总结:

      • 调用created钩子时注册相关事件,当input值发生改变后,调用onUpdate:modelValue函数给value赋值
      • 当value值改变时,调用beforeUpdate钩子,把value值赋值给input.value
      • 这就达到了双向绑定(话说,我觉得最好加个beforeUnmount钩子,把created钩子中注册的事件移除掉)

    总结

    一个优秀的库真不容易,要处理那么多特殊情况

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


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

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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