Vue3 的数据双向绑定是通过响应式系统来实现的。相比于 Vue2,Vue3 在响应式系统上做了很多改进,主要使用了 Proxy 对象来替代原来的 Object.defineProperty。本文将介绍 Vue3 数据双向绑定的主要特点和实现方式。
1. 响应式系统
1.1. Proxy对象
Vue3 使用 JavaScript 的 Proxy 对象来实现响应式数据。Proxy 可以监听对象的所有操作,包括读取、写入、删除属性等,从而实现更加灵活和高效的响应式数据。
1.2. reactive函数
Vue3 提供了一个 reactive 函数来创建响应式对象,通过 reactive 函数包装的对象会变成响应式数据,Vue 会自动跟踪这些数据的变化。
import { reactive } from 'vue';
const state = reactive({
message: 'Hello Vue3'
});
1.3. ref函数
对于基本数据类型,如字符串、数字等,Vue3 提供了 ref 函数来创建响应式数据,使用 ref 包装的值可以在模板中进行双向绑定。
import { ref } from 'vue';
const count = ref(0);
2. 双向绑定
Vue3 中的双向绑定主要通过 v-model 指令来实现,适用于表单元素,如输入框、复选框等。以下是一个简单的示例:
<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 的使用更加灵活,可以支持自定义组件的双向绑定:
<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 组件中:
<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 响应式系统的核心代码片段:
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 函数来实现这两个功能。
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 来实现响应式。
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. 编译时实现
// 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. 运行时实现
// 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
}