proxy代理实现响应式的底层原理
1.proxy代理:target为被代理的值,handler中的get和set函数是被代理对象被访问和修改时触发的操作,用proxy实现的原理即为用proxy代理要实现响应式的值,然后再proxy的handler中的set收集依赖于这个值更新而触发的副作用函数,然后set再执行这些收集的副作用函数
const proxyMap = new WeakMap();
const handler = {
// 拦截获取属性操作
get(target, prop, receiver) {
const value = target[prop];
// 如果值是对象类型且非 null,递归地为该对象创建 Proxy(深度代理)
if (typeof value === 'object' && value !== null) {
if (!proxyMap.has(value)) {
// 缓存已代理的对象,避免重复代理
proxyMap.set(value, new Proxy(value, handler)); //如果对象的属性还是一个对象,
//设置值为一个新的proxy对象,实现对象的深层拦截
}
return proxyMap.get(value); // 返回已代理的对象
}
return value; // 如果不是对象类型,则直接返回值
},
// 拦截设置属性操作
set(target, prop, value, receiver) {
console.log(`Setting ${prop} to ${value}`);
target[prop] = value;
return true; // 返回 true 表示设置成功
},
}
2.vue3利用proxy实现响应式 effect副作用函数:主要作用即为数据变化时触发数据变化引起的副作用(依赖于这个数据的其他变化)
// targetMap 用来存储响应式对象和它们的属性依赖关系
const targetMap = new WeakMap();
// 用来存储当前正在执行的副作用函数
let activeEffect = null;
function effect(fn) {
activeEffect = fn; // 设置当前的副作用函数
//activeEffect函数的作用很简单,就是要弄清楚当前活跃的effect副作用函数是哪个。
fn(); // 执行副作用函数
activeEffect = null; // 执行完后清除副作用函数
}
function track(target, key) {
if (!activeEffect) return; // 如果没有副作用函数,就不收集依赖
// 从 targetMap 中获取 target 对象的 depsMap
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map())); // 初始化 depsMap
}
// 从 depsMap 中获取该属性的依赖集合
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set())); // 初始化 deps 集合
}
// 将当前的副作用函数添加到依赖集合中
deps.add(activeEffect);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (!deps) return;
// 遍历依赖集合并执行每个副作用函数
deps.forEach(effect => effect());
}
function reactive(target) {
return new Proxy(target, {
get(target, key) {
// 在这里收集依赖
track(target, key);
// 如果属性是对象(可能是嵌套对象),则递归地将其转换为 Proxy
const value = target[key];
if (typeof value === 'object' && value !== null) {
return reactive(value); // 对嵌套对象进行递归代理
}
return value;
},
set(target, key, value) {
const oldValue = target[key];
if (oldValue === value) return true; // 如果值没有变化,直接返回
target[key] = value; // 修改属性值
trigger(target, key); // 触发更新
return true;
}
});
}
// 示例
const state = reactive({ count: 0 });
effect(() => {
console.log(`count is: ${state.count}`);
});
state.count = 1; // 修改 count 会触发 `trigger`,执行副作用函数
相关知识:
- proxy代理的是整个对象,可以拦截整个对象的操作(如果对象的属性还是对象,那么递归的为这个属性创造一个新的proxy对象),不像vue2用object.defineproperty需要给每个对象的属性设置getter和setter,而且proxy支持动态拦截,可以拦截到对象上的新属性,而且支持
watch
function watch(source, cb) {
let getter;
if (typeof source === 'function') {
getter = source; // 如果 source 是一个函数,则直接用它作为 getter
} else {
getter = () => traverse(source); // 如果 source 是一个对象,则对其进行递归遍历
}
let oldValue, newValue;
// 定义 effectFn,封装副作用函数
const effectFn = effect(
() => getter(), // 获取需要监听的数据
{
lazy: true, // 初始化时不执行 effect
scheduler() {
// 当数据变化时触发的调度器(依赖的数据变化,这个数据的proxy代理的trigger就会触发
//相应的effect副作用函数,来通知这个scheduler调度器执行。)
newValue = effectFn(); // 获取新的值
cb(newValue, oldValue); // 执行回调,传入新旧值
oldValue = newValue; // 更新旧值
}
}
);
// 初次执行 effectFn,得到初始值
oldValue = effectFn();
}
computed
function computed(options) {
let getter, setter
if (typeof options === 'function') {
getter = options
setter = () => {
console.warn('Write operation failed: computed value is readonly')
}
} else {
getter = options.get
setter = options.set
}
let value;
let dirty = true;
// effectFn 是由 effect 创建的副作用函数
const effectFn = effect(getter, {
lazy: true, // 延迟执行,只有在计算属性被访问时才计算
scheduler() {
dirty = true; // 当依赖的值发生变化时,副作用函数通知scheduler执行,标记为脏数据,表示需要重新计算
}
});
return {
get value() {
if (dirty) {
value = effectFn(); // 重新计算(effectFn调用处理好的getter)
dirty = false; // 计算完后标记为不脏
}
return value; // 返回缓存值
},
set value(newVal) {
setter(newVal)
}
};
}
v-model
实际上是一个:value 和 @input 的语法糖
<input v-model="message" /> //等价于如下
<input :value="message" @input="message = $event.target.value" />
本质:给input输入框绑定一个value(v-bind绑定,实际上还是利用proxy代理实现响应式,利用虚拟dom和diff算法实现更新),然后监听输入框等dom的input事件,触发input时即修改绑定的value即可。
用于父子组件间通信时
实际上是用props和emit建立一个父子组件之间的双向数据流,即是props和emit封装而来的语法糖, 子组件还是需要用props和emit来接收值和修改值
//父组件
<MyInput v-model="message" /> //等效于如下,也可以自定义如v-model:title,后面子组件修改的事件也得对应改为update:title
<MyComponent :modelValue="message" @update:modelValue="message = $event" />
<!-- 子组件 -->
<template>
<input :value="modelValue" @input="onInput" />
</template>
<script setup>
defineProps({
modelValue: String
});
const emit = defineEmits(['update:modelValue'])
function onInput(e) {
emit('update:modelValue', e.target.value)
}
</script>
Vue 自动将 message 的值通过 props 的方式传入子组件,命名为 modelValue(默认)。子组件通过 props.modelValue 接收该值。 子组件中,当内容改变(例如用户输入),通过 emit('update:modelValue', newValue) 通知父组件更新变量 message。
父组件接收到这个事件后,自动将 message 更新为新的值。
Vue3.3+更简洁的方法——defineModel
<!-- 父组件-->
<template>
<MyModal v-model:visible="show" v-model:title="dialogTitle" />
</template>
<script setup>
import { ref } from 'vue'
import MyModal from './MyModal.vue'
const show = ref(false)
const dialogTitle = ref('欢迎!')
</script>
<!-- 子组件-->
<template>
<div v-if="visible" class="modal">
<h2>{{ title }}</h2>
<button @click="visible = false">关闭</button>
</div>
</template>
<script setup>
const visible = defineModel('visible') // 对应 v-model:visible
const title = defineModel('title') // 对应 v-model:title
//修改时直接title.value = 'aaa' 直接修改就可以
</script>
实际上defineModel就是把原来的v-model在子组件中的操作进行了简化。
// 传统写法
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function updateValue(val) {
emit('update:modelValue', val)
}
defineModel封装原操作,还是利用props和emit进行与父组件的接收和发送,用computed方法来处理值,get获取值,set修改值。
//const visible = defineModel('visible') 等价于进行了如下操作
defineProps({ visible: Boolean })
defineEmits(['update:visible'])
const visible = computed({
get: () => props.visible,
set: val => emit('update:visible', val)
})