Vue 3.0双向数据绑定实现原理

Vue3 的数据双向绑定是通过响应式系统来实现的。相比于 Vue2,Vue3 在响应式系统上做了很多改进,主要使用了 Proxy 对象来替代原来的 Object.defineProperty。本文将介绍 Vue3 数据双向绑定的主要特点和实现方式。

1. 响应式系统

1.1. Proxy对象

Vue3 使用 JavaScript 的 Proxy 对象来实现响应式数据。Proxy 可以监听对象的所有操作,包括读取、写入、删除属性等,从而实现更加灵活和高效的响应式数据。

1.2. reactive函数

Vue3 提供了一个 reactive 函数来创建响应式对象,通过 reactive 函数包装的对象会变成响应式数据,Vue 会自动跟踪这些数据的变化。

javascript 复制代码
import { reactive } from 'vue';

const state = reactive({
    message: 'Hello Vue3'
});

1.3. ref函数

对于基本数据类型,如字符串、数字等,Vue3 提供了 ref 函数来创建响应式数据,使用 ref 包装的值可以在模板中进行双向绑定。

javascript 复制代码
import { ref } from 'vue';

const count = ref(0);

2. 双向绑定

Vue3 中的双向绑定主要通过 v-model 指令来实现,适用于表单元素,如输入框、复选框等。以下是一个简单的示例:

html 复制代码
<template>
    <input v-model="message" />
    <p>{{ message }}</p>
</template>

<script>
import { ref } from 'vue';

export default {
    setup() {
        const message = ref('');
        return {
            message
        }
    }
}
</script>

在 Vue3 中,v-model 的使用更加灵活,可以支持自定义组件的双向绑定:

javascript 复制代码
<template>
    <CustomInput v-model:value="message" />
</template>

<script>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';

export default {
    components: {
        CustomInput
    },
    setup() {
        const message = ref('');
        return {
            message
        }
    }
}
</script>

在 CustomInput 组件中:

html 复制代码
<template>
    <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)"/>
</template>

<script>
export default {
    props: {
        modelValue: String
    }
};
</script>

下面,我们来深入了解 Vue3 如何通过源码实现数据的双向绑定。

3. 源码实现

3.1. Proxy实现响应式

Vue3 使用 Proxy 对象来实现响应式数据。Proxy 允许我们定义基本操作的自定义行为,如读、写、删除、枚举等。

以下是 Vue3 响应式系统的核心代码片段:

javascript 复制代码
function reactive(target) {
    return createReactiveObject(target, mutableHandlers);
}

const mutableHandlers = {
    get(target, key, receiver) {
        // 依赖收集
        track(target, key);
        const res = Reflect.get(target, key, receiver);
        // 深度响应式处理
        if (isObject(res)) {
            return reactive(res);
        }
        return res;
    },

    set(target, key, value, receiver) {
        const oldValue = target[key];
        const result = Reflect.set(target, key, value, receiver);
        // 触发更新
        if (oldValue != value) {
            trigger(target, key);
        }
        return result;
    },

    // 其他处理函数 (deleteProperty, has, ownKeys 等)
};

function createReactiveObject(target, handlers) {
    if (!isObject(target)) {
        return target;
    }
    const proxy = new Proxy(target, handlers);
    return proxy;
}

3.2. 依赖心集与触发更新

在响应式系统中,依赖收集和触发更新是两个核心概念。Vue3 使用 track和 trigger 函数来实现这两个功能。

javascript 复制代码
const targetMap = new WeakMap();

function track(target, key) {
    const effect = activeEffect;
    if (effect) {
        let depsMap = targetMap.get(target);
        if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()));
        }
        let dep = depsMap.get(key);
        if (!dep) {
            depsMap.set(key, (dep = new Set()));
        }
        if (!dep.has(effect)) {
            dep.add(effect);
            effect.deps.push(dep);
        }
    }
}

function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    const effects = new Set();
    const add = effectsToAdd => {
        if (effectsToAdd) {
            effectsToAdd.forEach(effect => effects.add(effect));
        }
    };

    add(depsMap.get(key));
    effects.forEach(effect => effect());
}

3.3. ref实现

对于基本数据类型,Vue3 提供了 ref 函数来创建响应式数据。ref 使用一个对象来包装值,并通过 getter和 setter 来实现响应式。

javascript 复制代码
function ref(value) {
    return createRef(value);
}

function createRef(rawValue) {
    if (isRef(rawValue)) {
        return rawValue;
    }
    const r = {
        __v_isRef: true,
        get value() {
            track(r, 'value');
            return rawValue;
        },
        set value(newVal) {
            if (rawValue !== newVal) {
                rawValue = newVal;
                trigger(r, 'value');
            }
        }
    };
    return r;
}

function isRef(r) {
    return r ? r.__v_isRef === true : false;
}

3.4. v-model实现

Vue3 中的 v-model 实现依赖于响应式系统。

3.4.1. 编译时实现

javascript 复制代码
// packages/compiler-core/src/transforms/vModel.ts
export const transformModel: NodeTransform = (node, context) => {
  if (node.type === NodeTypes.ELEMENT) {
    // 对每个元素节点执行此方法
    return () => {
      // 只处理有 v-model 指令的节点
      const node = context.currentNodel
      if (node.tagType === ElementTypes.ELEMENT) {
        const dir = findDir(node, 'model')
        if (dir && dir.exp) {
          // 根据节点类型调用不同的处理函数
          const { tag } = node
          if (tag === 'input') {
            processInput(node, dir, context)
          } else if (tag === 'textarea') {
            processTextArea(node, dir, context)
          } else if (tag === 'select') {
            processSelect(node, dir, context)
          } else if (!context.inSSR) {
            // 组件上的 v-model
            processComponent(node, dir, context)
          }
        }
      }
    }
  }
}


// 处理组件上的v-model
function processComponent(
  node: ElementNode,
  dir: DirectiveNode,
  context: TransformContext
) {
  // 获取 v-model 的参数,支持 v-model:arg 形式
  const { arg, exp } = dir
  
  // 默认参数是 'modelValue'
  const prop = arg ? arg : createSimpleExpression('modelValue', true)
  
  // 默认事件是 'update:modelValue'
  const event = arg
    ? createSimpleExpression(`update:${arg.content}`, true)
    : createSimpleExpression('update:modelValue', true)
  
  // 添加 prop 和 event 到 props 中
  const props = [
    createObjectProperty(prop, dir.exp!),
    createObjectProperty(event, createCompoundExpression([`$event => ((`, exp, `) = $event)`]))
  ]
  
  // 将 v-model 转换为组件的 props 和事件
  node.props.push(
    createObjectProperty(
      createSimpleExpression(`onUpdate:modelValue`, true),
      createCompoundExpression([`$event => (${dir.exp!.content} = $event)`])
    )
  )
}

3.4.2. 运行时实现

javascript 复制代码
// packages/runtime-core/src/helpers/vModel.ts
export function vModelText(el: any, binding: any, vnode: VNode) {
  // 处理文本输入框的 v-model
  const { value, modifiers } = binding
  el.value = value == null ? '' : value
  
  // 添加事件监听
  el._assign = getModelAssigner(vnode)
  const lazy = modifiers ? modifiers.lazy : false
  const event = lazy ? 'change' : 'input'
  
  el.addEventListener(event, e => {
    // 触发更新
    el._assign(el.value)
  })
}

export function vModelCheckbox(el: any, binding: any, vnode: VNode) {
  // 处理复选框的 v-model
  const { value, oldValue } = binding
  el._assign = getModelAssigner(vnode)
  
  // 处理数组类型的值(多选)
  if (isArray(value)) {
    const isChecked = el._modelValue? looseIndexOf(value, el._modelValue) > -1: false
    if (el.checked !== isChecked) {
      el.checked = isChecked
    }
  } else {
    // 处理布尔值
    if (value !== oldValue) {
      el.checked = looseEqual(value, el._trueValue)
    }
  }
}

// 辅肋函数
function getModelAssigner(vnode: VNode): (value: any) => void {
  // 获取模型赋值函数
  const fn = vnode.props!['onUpdate:modelValue']
  return isArray(fn) ? (value: any) => invokeArrayFns(fn, value) : fn
}
相关推荐
志尊宝21 小时前
Vue3 零基础每日笔记(040):Vite 创建 Vue3 + TS 项目——企业标配从这一篇开始
笔记·vue·html·前端开发·软件开发
志尊宝3 天前
Vue3 零基础每日笔记(035):什么是 Composables——mixin 之死与逻辑复用新方案
笔记·vue·html·前端开发·软件开发
志尊宝3 天前
Vue3 零基础每日笔记(030):动态组件与 keep-alive——切换组件与页面缓存
笔记·vue·html·前端开发·软件开发
志尊宝3 天前
Vue3 零基础每日笔记(029):插槽 slot 三连——默认、具名、作用域一次讲透
前端·javascript·vue.js·vue·前端开发
2601_962284178 天前
欧洲转码指南:后端、前端与全栈开发解析
前端开发·devops·后端开发·全栈开发·欧洲转码指南
醉颜凉8 天前
React触摸事件全解析:掌握移动端交互开发的核心秘籍
react·前端开发·触摸事件·移动端交互·react事件
2601_962097568 天前
UI图片转(代码+切图)Skill
前端开发·技能开发·ui图片转代码·设计层级·视觉验收
2601_962284178 天前
从前端到AI全栈:技术演进中的AI赋能革命
需求分析·前端开发·智能测试·ai全栈·技术演进
滨哥GPT11 天前
Codex改完代码为什么保存后页面还是不生效?热更新、文件监听与开发服务器排查
前端开发·热更新·开发调试·codex·hmr
2601_9620779814 天前
阿里二面,前端开发在web3.0中该如何应用,记录面经
区块链·前端开发·web3.0·技术趋势·学习准备