在前两篇文章【重学 Vue:深入本质,脱离八股文,彻底搞定面试】:(四)数据拦截的本质和 《【重学 Vue:深入本质,脱离八股文,彻底搞定面试】:(五)响应式数据的本质》 中,我们深入理解了 JS 中的两种数据拦截方式、ref 和 reactive 实现响应式的策略,明确了 什么操作会触发数据拦截。
但我们仍然留下了一个关键问题没有深入探讨:Vue 是如何知道某个函数依赖了哪些响应式数据?又是如何追踪并在数据变化时通知这些函数重新执行的?
这,正是 Vue 响应式系统的“核心秘密”:依赖收集 + 派发更新。
本篇文章将带你深入这个核心机制,并用多个示例拆解,演示多种典型的依赖收集和派发更新场景,带你彻底摆脱”黑箱式使用“,玩转”响应式陷阱“。
Vue 的响应式系统本质上是一种“函数与数据之间的映射关系机制”,数据变了,对应的函数重新运行。为了实现这一点,就需要两个核心机制:
依赖关系的核心是:
函数运行时读取了响应式数据 ⇒ 函数依赖该响应式数据。
特别说明:
被监控的函数:
effect
(底层 API)watchEffect
watch
响应式数据:
ref
reactive
computed
props
这些数据具有 可追踪变化 的能力,即当数据发生变换会通知一些函数重新执行。
var a;
function foo() {
console.log(a); // ❌ 无依赖,a 不是响应式数据
}
import { ref } from "vue";
var a = ref(1);
function foo() {
console.log(a); // ❌ 无依赖,虽然是 ref,但未访问 .value,读取未被拦截
// console.log(a.value); // ✅ 有依赖,foo 依赖 a.value
}
import { ref } from "vue";
var a = ref({ b: 1 });
const k = a.value;
const n = k.b;
function foo() {
a;
a.value; // ✅ 有依赖
a.value.b; // ✅ 有依赖
k.b; // ✅ 有依赖
n;
}
import { ref } from "vue";
var a = ref({ b: 1 });
const k = a.value;
const n = k.b;
function foo() {
function fn2(){
a;
a.value.b; // ✅ 有依赖
n;
}
fn2();
}
import { ref } from "vue";
var a = ref({ b: 1 });
const k = a.value;
async function foo() {
a;
a.value; // ✅ 有依赖
await 1;
k.b; // ❌ 无依赖
}
await
后的代码不再参与依赖收集。
_ 详见:_Vue 响应式为什么异步读取不建立依赖?
demo 共用部分:
import { ref, watchEffect } from "vue";
const state = ref({ a: 1 });
const k = state.value;
const n = k.a;
watchEffect(() => {
console.log("运行");
state; // ❌ 无依赖
state.value; // ✅ 依赖 value
state.value.a; // ✅ 依赖 value,value.a
n; // ❌ 无依赖
});
setTimeout(() => {
state.value = { a: 3 }; // 会重新运行
// state.value; // 不会重新运行
// state.value.a = 1; // 不会重新运行,值没变
// k.a = 2; // 会重新运行
// n++; // 不会重新运行
// state = 100; // 不会重新运行,已经不是 ref 对象了
}, 500);
watchEffect(() => {
console.log("运行");
state; // ❌ 无依赖
state.value; // ✅ 依赖 value
n;
});
setTimeout(() => {
state.value.a = 100; // 不会重新运行
}, 500);
watchEffect(() => {
console.log("运行");
state;
state.value.a; // ✅ 依赖 value,value.a
n;
});
setTimeout(() => {
state.value.a = 1; // 不会重新运行,值没有改变
}, 500);
setTimeout(() => {
state.value.a = 2; // 会重新运行
}, 1000);
setTimeout(() => {
k.a = 3; // 会重新运行
}, 1500);
watchEffect(() => {
console.log("运行");
state.value.a; // ✅ 依赖 value,value.a
});
setTimeout(() => {
state.value = { a: 1 }; // 会重新运行
}, 500);
setTimeout(() => {
state.value.a = 2; // 会重新运行
}, 1000);
setTimeout(() => {
k.a = 3; // 不会重新运行,因为前面修改了 state.value,不再是同一个代理对象
}, 1000);
watchEffect(() => {
console.log("运行");
state.value.a; // ✅ 依赖 value,value.a
k.a; // 返回的 proxy 对象的 a 成员
});
setTimeout(() => {
state.value = { a: 1 }; // 会重新运行
}, 500);
setTimeout(() => {
k.a = 3; // 会重新运行
}, 1000);
watchEffect(() => {
console.log("运行");
state.value.a = 2; // ✅ 仅依赖 value,拦截value 的 get, a 的 set
});
setTimeout(() => {
state.value.a = 100; // 不会重新运行
// state.value = {}; // 会重新运行
}, 500);
下一篇:【重学 Vue:深入本质,脱离八股文,彻底搞定面试】:(七)响应式和组件渲染
敬请期待~
如果你觉得这篇文章对你有帮助,欢迎关注 + 点赞 + 收藏,我会持续输出「Vue 技术本质」系列内容。