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.currentNodelif (node.tagType === ElementTypes.ELEMENT) {const dir = findDir(node, 'model')if (dir && dir.exp) {// 根據節點類型調用不同的處理函數const { tag } = nodeif (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-modelprocessComponent(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-modelconst { value, modifiers } = bindingel.value = value == null ? '' : value// 添加事件監聽el._assign = getModelAssigner(vnode)const lazy = modifiers ? modifiers.lazy : falseconst event = lazy ? 'change' : 'input'el.addEventListener(event, e => {// 觸發更新el._assign(el.value)})
}export function vModelCheckbox(el: any, binding: any, vnode: VNode) {// 處理復選框的 v-modelconst { value, oldValue } = bindingel._assign = getModelAssigner(vnode)// 處理數組類型的值(多選)if (isArray(value)) {const isChecked = el._modelValue? looseIndexOf(value, el._modelValue) > -1: falseif (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
}