Vue3的双向绑定是如何实现的

2,324 阅读5分钟
Vue 3.0 系列文章

Vue 3.0组件的渲染流程

Vue 3.0组件的更新流程和diff算法详解

揭开Vue3.0 setup函数的神秘面纱

Vue 3.0 Props的初始化和更新流程的细节分析

Vue3.0 响应式实现原理分析

Vue 3.0 计算属性的实现原理分析

Vue3.0 常用响应式API的使用和原理分析(一)

Vue3.0 常用响应式API的使用和原理分析(二)

Vue 3.0 Provide和Inject实现共享数据

Vue 3.0 Teleport的使用和原理分析

Vue3侦听器和异步任务调度, 其中有个神秘角色

Vue3.0 指令

Vue3.0 内置指令的底层细节分析

Vue3.0 的事件绑定的实现逻辑是什么

Vue3.0 的双向绑定是如何实现的

Vue3.0的插槽是如何实现的?

探究Vue3.0的keep-alive和动态组件的实现逻辑

Vuex 4.x

Vue Router 4 的使用,一篇文章给你讲透彻

Vue的双向绑定是指数据变化能引起界面的变化,界面数据的变化也能驱动数据的改变。

这个功能其实和单向数据流规范不一样,所以开始接触Vue的时候非常吸引我的一个功能。我们发现Element UI的表单也有大量使用v-model进行双向绑定。

双向绑定 其实 不是所有的元素/组件都支持的,目前Vue支持 inputselect, checkbox, radio 和组件 利用 v-model 指令进行 双向绑定。

我以前对 双向绑定 这个功能有很大的一个疑惑:就是双向绑定为什么不会造成更新死循环?即 界面变化 -> 数据变化 -> 界面变化 -> 数据变化 -> ...

v-model对表单元素进行双向绑定

由于不同的表单元素使用的内部指令是不一样的,我们就用input作为例子进行分析,其他的表单元素的双向绑定原理非常类似。

这一节涉及到 指令事件处理 相关的知识点,如果不是太清楚的话,建议参阅我前面的两篇相关内容,否则有可能会有一些的疑惑。

案例分析

<input v-model="value" />
<div>{{ value }}</div>

setup() {
  let value = ref("");
  return {
    value
  };
}

简单几行代码就实现了input表单元素和数据value的双向绑定功能。

代码分析

我们来看看渲染函数
const _hoisted_1 = ["onUpdate:modelValue"]

_withDirectives(_createElementVNode("input", {
  "onUpdate:modelValue": $event => (value = $event)
}, null, 8 /* PROPS */, _hoisted_1), [
  [_vModelText, value]
])

我们分析withDirectives函数,看到input生成的VNode 使用了vModelText这个内部指令,且添加了一个名为onUpdate:modelValue的事件处理的pro 函数,onUpdate:modelValue函数用来修改value值;

vModelText 指令
export const vModelText: ModelDirective<
  HTMLInputElement | HTMLTextAreaElement
> = {
  created(el, { modifiers: { lazy, trim, number } }, vnode) {
    // 获取到 vnode.props!['onUpdate:modelValue'] 对应的函数
    el._assign = getModelAssigner(vnode)

    const castToNumber =
      number || (vnode.props && vnode.props.type === 'number')

    // 如果 有lazy修饰符 监听 input 的 change 事件,否则监听 input 的 input 事件
    addEventListener(el, lazy ? 'change' : 'input', e => {
      let domValue: string | number = el.value
      if (trim) {
        // 如果有trim修饰符,则将 input的value进行去空格
        domValue = domValue.trim()
      } else if (castToNumber) {                
        // 如果有number修饰符,或者 input 类型是 number类型,则把 input的value变成number类型
        domValue = toNumber(domValue)
      }
      // 然后进行参数的回调实现 界面 到 数据的更改
      el._assign(domValue)
    })
  },
  beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
    // 更新 'onUpdate:modelValue' 函数,因为有可能不会更新数据,所以
    el._assign = getModelAssigner(vnode)
    // 如果 input的值没变,不进行任何操作
    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
    }
  }
}
  1. created钩子函数中,如果有lazy修饰符,input表单监听change事件,否则监听input事件;
  2. beforeUpdate钩子函数中,要重新获取onUpdate:modelValue函数,因为重新渲染函数可能更改了这个函数,并且重新给input赋值;
  3. input中输入新的内容后,如果有trim修饰符就进行去空格,如果有有number修饰符或者 input类型是number类型需要转换成number,然后通过onUpdate:modelValue对应的函数修改value 值。

总结:

  1. 数据->DOM: 响应式数据value变化触发组件更新,input的内容将发现变化;
  2. DOM->数据: vModelText指令实现了对inputvalue变化的监听,根据vModelText指令的修饰符处理完inputvalue值,然后通过onUpdate:modelValue对应的函数$event => (value = $event),重新完成响应式数据value的修改。响应式数据的修改会触发组件更新。

一些思考

为什么不会出现更新循环呢?

input输入数据 -> 数据处理 -> 调用onUpdate:modelValue对应的$event => (inputValue = $event)方法 -> 响应式数据变化触发组件更新 -> input设置新值input.value = newValue 更新至此终止

双向绑定

为什么更新input的新值放在vModelText指令的beforeUpdate中执行?

指令的更新有两个方法:beforeUpdateupdated。 在beforeUpdate中执行有两个优势:

  1. 在更新DOM前更新input的新值,如果只是修改了input值,就省去了patchProp的部分操作,提高了patch性能;
  2. 指令的beforeUpdate是DOM更新前同步执行的,而updated钩子函数是在DOM更新后异步执行的,如果业务复杂同步任务太多的情况下可能会出现更新延迟或者卡顿的现象。

v-model对组件进行双向绑定

<Son v-model="modelVlue" />

其实等同于:

<Son
  :modelValue="modelVlue"
  @update:modelValue="modelVlue = $event"
></Son>

v-model对组件进行双向绑定 本质上就是一个 语法糖,通过pro给子组件传递数据,子组件通过v-on 进行事件绑定可以进行数据的修改。