美文网首页
Vue.js 3.x 双向绑定原理

Vue.js 3.x 双向绑定原理

作者: AshengTan | 来源:发表于2022-04-19 08:52 被阅读0次

    什么是双向绑定?

    废话不多说,我们先来看一个 v-model 基本的示例:

    <input type="text" v-model="search">
    

    首先,我们要明白一点的是:v-model 的本质是指令。因此,它跟我们一般的自定义指令是一样的,需要实现 Vue.js 生命周期的钩子函数。

    其次,v-model 实现了双向绑定,也就是:数据到 DOM 的单向流动DOM 到数据的单向流动

    明白了上面这两点,再来看代码就清晰多了。

    // packages/runtime-dom/src/directives/vModel.ts
    
    export const vModelText: ModelDirective<
      HTMLInputElement | HTMLTextAreaElement
    > = {
      created() {},
      mounted() {},
      beforeUpdate() {}
    }
    

    打开 v-model 的源码我们可以看到,它实现了对应的 Vue.js 生命周期钩子函数,实际上它就是一个内置的自定义指令。

    那么,v-model 如何实现双向绑定的呢?具体来说,数据到 DOM 的单向流动以及DOM 到数据的单向流动是如何实现的。

    数据到 DOM 的单向流动

    // packages/runtime-dom/src/directives/vModel.ts
    
    export const vModelText: ModelDirective<
      HTMLInputElement | HTMLTextAreaElement
    > = {
      // set value on mounted so it's after min/max for type="range"
      mounted(el, { value }) {
        el.value = value == null ? '' : value
      }
    }
    

    数据到 DOM 的单向流动实现非常简单,一行代码就搞定了,就是把 v-model 绑定的值赋值给 el.value

    DOM 到数据的单向流动

    // packages/runtime-dom/src/directives/vModel.ts
    
    export const vModelText: ModelDirective<
      HTMLInputElement | HTMLTextAreaElement
    > = {
      created(el, { modifiers: { lazy, trim, number } }, vnode) {
        el._assign = getModelAssigner(vnode)
        
        // see: https://github.com/vuejs/core/issues/3813
        const castToNumber = number || (vnode.props && vnode.props.type === 'number')
        
        // 实现 lazy 功能
        addEventListener(el, lazy ? 'change' : 'input', e => {
          // `composing=true` 时不把 DOM 的值赋值给数据
          if ((e.target as any).composing) return
          
          let domValue: string | number = el.value
          if (trim) {
            domValue = domValue.trim()
          } else if (castToNumber) {
            domValue = toNumber(domValue)
          }
          
          // DOM 的值改变时,同时改变对应的数据(即改变 v-model 上绑定的变量的值)
          el._assign(domValue)
        })
        
        // 实现 trim 功能
        if (trim) {
          addEventListener(el, 'change', () => {
            el.value = el.value.trim()
          })
        }
        
        // 不配置 lazy 时,监听的是 input 的 input 事件,它会在用户实时输入的时候触发。
        // 此外,还会多监听 compositionstart 和 compositionend 事件。
        if (!lazy) {
            // 这是因为,用户使用拼音输入法开始输入汉字时,这个事件会被触发,
            // 此时,设置 `composing=true`,在 input 事件回调里可以进行判断,避免将 DOM 的值赋值给数据,
          // 因为此时并未输入完成。
          addEventListener(el, 'compositionstart', onCompositionStart)
          
          // 当用户从输入法中确定选中了一些数据完成输入后(如中文输入法常见的按空格确认输入的文字),
          // 设置 `composing=false`,在 onCompositionEnd 中手动触发 input 事件,完成数据的赋值。
          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)
        }
      }
    }
    
    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
        target.dispatchEvent(new Event('input'))
      }
    }
    
    const getModelAssigner = (vnode: VNode): AssignerFn => {
      const fn = vnode.props!['onUpdate:modelValue']
      return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
    }
    

    代码有点多,但原理很简单:

    • 通过自定义监听事件 addEventListener 来监听 input 元素的 inputchange 事件
    • 当用户手动输入数据时执行对应的函数,并通过 el.value 获取 input 的新值
    • 调用 el._assignonUpdate:modelValue 属性对应的函数)方法 v-model 绑定的值

    而实现 DOM 到数据的单向流动,关键就在 onUpdate:modelValue。借助 Vue 3 Template Explorer,我们可以查看其编译后生成的 render 函数,可以发现它做所的事情并没有什么神奇的地方,就是帮我们自动更新 v-model 上绑定的变量的值。

    <input type="text" v-model="search">
    
    import { vModelText as _vModelText, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
    
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return _withDirectives((_openBlock(), _createElementBlock("input", {
        type: "text",
        
        // `onUpdate:modelValue` 所做的事,
        // 就是自动帮我们更新 `v-model` 上绑定的变量的值。
        "onUpdate:modelValue": $event => ((_ctx.search) = $event)
        
      }, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [
        [_vModelText, _ctx.search]
      ])
    }
    

    除此之外,还有对 lazy 的处理、trim 的处理、数字的处理、以及解决正在输入时文本被清空的问题。

    关于 onCompositionStartonCompositionEnd 两个方法的作用,详见 text added with IME to input that has v-model is gone when the view is updated #2302

    一句话总结:通过使用 addEventListener 来实现 DOM 到数据的单向流动

    最后是 beforeUpdate 的实现,如果数据的值和 DOM 的值不一致,则将数据更新到 DOM:

    // packages/runtime-dom/src/directives/vModel.ts
    
    beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
        el._assign = getModelAssigner(vnode)
        // avoid clearing unresolved text. #2302
        // 输入某些语言如中文,在没有输入完成时,在更新时会自动将已存在的文本清空,具体可见 issue#2302
        if ((el as any).composing) return
      
        if (document.activeElement === el) {
          if (lazy) {
            return
          }
          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
        }
      }
    

    以上就是 text 类型的 input 元素双向绑定原理,当然 input 元素类型不止这个,还有诸如 radiocheckbox 等类型,大家有兴趣的话可以自己去看,但是原理都是相同的,就是实现两个功能:数据到 DOM 的单向流动DOM 到数据的单向流动

    相关文章

      网友评论

          本文标题:Vue.js 3.x 双向绑定原理

          本文链接:https://www.haomeiwen.com/subject/fhtrertx.html