Skip to content
On this page

Vue3 的数据双向绑定的原理

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

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

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

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

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

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

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

案例分析

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

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

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

代码分析

我们来看看渲染函数

js
import {
  vModelText as _vModelText,
  createElementVNode as _createElementVNode,
  withDirectives as _withDirectives,
  toDisplayString as _toDisplayString,
  createTextVNode as _createTextVNode,
  Fragment as _Fragment,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock
} from 'vue'

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock(
      _Fragment,
      null,
      [
        _withDirectives(
          _createElementVNode(
            'input',
            {
              'onUpdate:modelValue': ($event) => (_ctx.value = $event)
            },
            null,
            8 /* PROPS */,
            ['onUpdate:modelValue']
          ),
          [[_vModelText, _ctx.value]]
        ),
        _createElementVNode(
          'div',
          null,
          _toDisplayString(_ctx.value),
          1 /* TEXT */
        ),
        _createTextVNode(' setup() { let value = ref(""); return { value }; }')
      ],
      64 /* STABLE_FRAGMENT */
    )
  )
}

// Check the console for the AST

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

vModelText指令

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

    const castToNumber = numbr || (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
    }
  }
}
  • created钩子函数中,如果有lazy修饰符,input表单监听change事件,否则监听input事件;
  • beforeUpdate钩子函数中,要重新获取onUpdate:modelValue函数,因为重新渲染函数可能更改了这个函数,并且重新给input赋值;
  • 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的新值放在vNodelText指令的beforeUpdate中执行?

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

  1. 在更新 DOM 前更新input的新值,如果只修改了input值,就省去了patchProp的部分操作,提高了patch性能。

  2. 指令的beforeUpdate是 DOM 更新前,而updated钩子函数是在 DOM 更新后异步执行的,如果业务复杂同步任务太多的情况下可能出现更新延迟或者卡顿的现象。

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

js
<Son v-model="modalValue" />

其实等同于

js
<Son :modalValue="modalValue" @update:modalValue="modalUpdate=$event.target.value"/>

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

如果希望自定义 model 参数

js
<Son v-model:visible="visible"/>
setup(props, ctx){
    ctx.emit("update:visible", false)
}