Skip to content

✨ 响应式的本质 👌

核心结论速览

  • 依赖收集:收集“被监控的函数”对“响应式数据读取拦截”形成的对应关系。
  • 派发更新:当响应式数据变更时,按依赖关系通知并重新执行对应的函数。

关键角色

  • 数据:refreactivepropscomputed 等产出的“响应式数据”。
  • 函数:必须是“被监控的函数”,例如 effect(内部)、watchEffectwatch、组件渲染函数。

建立依赖的准确条件

  • 仅在“被监控的函数”的“同步执行期间”,出现“读取响应式数据成员且该读取被拦截”的情况下,才建立依赖。
  • 异步边界之后的代码(如 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(内部实现)、watchEffectwatch、组件渲染函数。只有这些函数在“同步执行期间”发生了“读取拦截”,才会被收集依赖;后续数据变更时按依赖关系重新执行。

练习:

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 管对象/数组成员,配合“读拦截”形成稳定依赖。
  • 搭配微任务合并与调度(如渲染队列)以减少重复执行与抖动。

小结

所谓响应式,背后是函数与数据的映射:数据变化会重新执行依赖该数据的被监控函数。

依赖收集:在监控函数的同步运行期间,读取操作被拦截的响应式数据,建立依赖关系。

派发更新:响应式数据变化后,按依赖关系重新执行对应函数。