Appearance
✨ 响应式的本质 👌
核心结论速览
- 依赖收集:收集“被监控的函数”对“响应式数据读取拦截”形成的对应关系。
- 派发更新:当响应式数据变更时,按依赖关系通知并重新执行对应的函数。
关键角色
- 数据:
ref、reactive、props、computed等产出的“响应式数据”。 - 函数:必须是“被监控的函数”,例如
effect(内部)、watchEffect、watch、组件渲染函数。
建立依赖的准确条件
- 仅在“被监控的函数”的“同步执行期间”,出现“读取响应式数据成员且该读取被拦截”的情况下,才建立依赖。
- 异步边界之后的代码(如
await之后)不参与当前依赖收集。
依赖的判定与示例
下面通过一组小例子,精确把握“用到”的语义——即“读取拦截是否发生”。
js
// demo1
var a;
function foo() {
console.log(a);
}
// 没有依赖关系,a 不是响应式数据js
// demo2
var a = ref(1);
function foo() {
console.log(a);
}
// 没有依赖关系,虽然用到了响应式数据,但是没有出现读取拦截的情况js
// demo3
var a = ref(1);
function foo() {
console.log(a.value);
}
// 有依赖关系,foo 依赖 value 属性js
// demo4
var a = ref({ b: 1 });
const k = a.value;
const n = k.b;
function foo() {
a;
a.value;
k.b;
n;
}
// 有依赖关系
// foo 依赖 a 的 value 属性
// foo 依赖 k 的 b 属性js
// demo5
var a = ref({ b: 1 });
const k = a.value;
const n = k.b;
function foo() {
a;
k.b;
n;
}
// 有依赖关系
// foo 依赖 k 的 b 属性js
// demo6
var a = ref({ b: 1 });
const k = a.value;
const n = k.b;
function foo() {
a;
a.value.b;
n;
}
// 有依赖关系
// foo 依赖 a 的 value 以及 b 属性js
// demo7
var a = ref({ b: 1 });
const k = a.value;
const n = k.b;
function foo() {
function fn2() {
a;
a.value.b;
n;
}
fn2();
}
// 有依赖关系
// foo 依赖 a 的 value 以及 b 属性总结
只需判断:在“被监控的函数的同步执行期间”,是否出现“读取拦截”。只要有读取拦截,就建立依赖关系。
为什么要区分同步/异步?
在依赖收集中,异步边界后的代码不属于当前收集周期。示例如下:
js
// demo8
var a = ref({ b: 1 });
const k = a.value;
const n = k.b;
async function foo() {
a;
a.value; // 产生依赖,依赖 value 属性
await 1;
k.b; // 没有依赖,因为它是异步后面的代码
n;
}被监控的函数与依赖收集
被监控的函数包括 effect(内部实现)、watchEffect、watch、组件渲染函数。只有这些函数在“同步执行期间”发生了“读取拦截”,才会被收集依赖;后续数据变更时按依赖关系重新执行。
练习:
js
// demo1
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 和 a 属性
n; // 没有依赖关系
});
setTimeout(() => {
state.value = { a: 3 }; // 要重新运行
}, 500);js
// demo2
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 a
n;
});
setTimeout(() => {
// state.value; // 不会重新运行(响应式数据没有发生变化)
state.value.a = 1; // 不会重新运行(响应式数据没有发生变化)
}, 500);js
// demo3
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、a
n;
});
setTimeout(() => {
k.a = 2; // 这里相当于是操作了 proxy 对象的成员 a
// 要重新运行
// 如果将上面的 state.value.a; 这句话注释点,就不会重新运行
}, 500);js
// demo4
import { ref, watchEffect } from "vue";
const state = ref({ a: 1 });
const k = state.value;
let n = k.a;
watchEffect(() => {
console.log("运行");
state;
state.value;
state.value.a;
n;
});
setTimeout(() => {
n++; // 不会重新运行
}, 500);js
// demo5
import { ref, watchEffect } from "vue";
const state = ref({ a: 1 });
const k = state.value;
let n = k.a;
watchEffect(() => {
console.log("运行");
state;
state.value;
state.value.a;
n;
});
setTimeout(() => {
state.value.a = 100; // 要重新运行
}, 500);js
// demo6
import { ref, watchEffect } from "vue";
let state = ref({ a: 1 });
const k = state.value;
let n = k.a;
watchEffect(() => {
console.log("运行");
state;
state.value;
state.value.a;
n;
});
setTimeout(() => {
state = 100; // 不要重新运行
}, 500);js
// demo7
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 会被收集
n;
});
setTimeout(() => {
state.value.a = 100; // 不会重新执行
}, 500);js
// demo8
import { ref, watchEffect } from "vue";
let state = ref({ a: 1 });
const k = state.value;
const n = k.a;
watchEffect(() => {
console.log("运行");
state.value.a; // value、a
});
setTimeout(() => {
state.value = { a: 1 }; // 要重新运行
}, 500);js
// demo9
import { ref, watchEffect } from "vue";
const state = ref({ a: 1 });
const k = state.value;
const n = k.a;
watchEffect(() => {
console.log("运行");
state.value.a = 2; // 注意这里的依赖仅仅只有 value 属性(读取操作)
});
setTimeout(() => {
// state.value.a = 100; // 不会重新运行的
state.value = {}; // 要重新运行
}, 500);js
// demo10
import { ref, watchEffect } from "vue";
let state = ref({ a: 1 });
const k = state.value;
const n = k.a;
watchEffect(() => {
console.log("运行");
state;
state.value.a; // value、a
n;
});
setTimeout(() => {
state.value.a = 2; // 要重新运行
}, 500);
setTimeout(() => {
// k.a = 3; // 要重新运行
k.a = 2; // 因为值没有改变,所以不会重新运行
}, 1000);js
// demo11
import { ref, watchEffect } from "vue";
let state = ref({ a: 1 });
const k = state.value;
const n = k.a;
watchEffect(() => {
console.log("运行");
state.value.a; // value、a
});
setTimeout(() => {
state.value = { a: 1 }; // 要重新运行
}, 500);
setTimeout(() => {
k.a = 3; // 这里不会重新运行,因为前面修改了 state.value,不再是同一个代理对象
}, 1000);js
// demo12
import { ref, watchEffect } from "vue";
let state = ref({ a: 1 });
const k = state.value;
const n = k.a;
watchEffect(() => {
console.log("运行");
state.value.a; // value、a
});
setTimeout(() => {
state.value = { a: 1 }; // 要重新执行
}, 500);
setTimeout(() => {
state.value.a = 2; // 要重新执行
}, 1000);js
// demo13
import { ref, watchEffect } from "vue";
let state = ref({ a: 1 });
const k = state.value;
const n = k.a;
watchEffect(() => {
console.log("运行");
state.value.a; // value、a
});
setTimeout(() => {
state.value = { a: 1 }; // 重新执行
}, 500);
setTimeout(() => {
state.value.a = 1; // 不会重新执行,因为值没有变化
}, 1500);js
// demo14
import { ref, watchEffect } from "vue";
let state = ref({ a: 1 });
const k = state.value;
const n = k.a;
watchEffect(() => {
console.log("运行");
state.value.a; // value、a
k.a; // 返回的 proxy 对象的 a 成员
});
setTimeout(() => {
state.value = { a: 1 }; // 要重新运行
}, 500);
setTimeout(() => {
k.a = 3; // 会重新执行
}, 1000);
setTimeout(() => {
state.value.a = 4; // 会重新执行
}, 1500);常见误区
易错点总结
- 把“非监控的普通函数”产生的读取操作,误以为会被收集依赖。
- 在监控函数中发生异步后继续读取,误以为仍然参与当前依赖收集。
- 写操作替代读操作:在依赖收集中只有“读拦截”才建立依赖,单纯写入不建立依赖(见 demo9 的注记)。
建议实践
- 在监控函数内,尽量将“依赖读取”保持在同步路径上,避免跨异步边界的读导致误判。
- 使用
ref管标量或引用语义,使用reactive管对象/数组成员,配合“读拦截”形成稳定依赖。 - 搭配微任务合并与调度(如渲染队列)以减少重复执行与抖动。
小结
所谓响应式,背后是函数与数据的映射:数据变化会重新执行依赖该数据的被监控函数。
依赖收集:在监控函数的同步运行期间,读取操作被拦截的响应式数据,建立依赖关系。
派发更新:响应式数据变化后,按依赖关系重新执行对应函数。
