响应式Reactivity
2026/1/31大约 6 分钟
Vue 响应式的核心是响应式对象的属性与副作用函数(effect)之间的依赖关系。
普通函数在调用时会执行,但是在响应式系统中,我们希望在属性值发生变化时,能够自动调用副作用函数。
- 普通函数
import { ref } from "vue";
const count = ref(0);
function fn() {
console.log(count.value);
}
fn(); // 0
setTimeout(() => {
count.value++; // 修改count的值不会触发fn函数的重新执行
}, 1000);- 副作用函数(effect)
import { ref, effect } from "vue";
const count = ref(0);
function effect() {
console.log(count.value); // 初次执行时输出 0
}
setTimeout(() => {
count.value++; // 修改count的值会触发effect函数的重新执行,输出 1
}, 1000);当在 effect 函数中访问响应式对象的属性时:
- 依赖收集: 当 effect 函数执行时,会读取响应式对象的属性值(调用ref的get方法),并将 effect 函数添加到依赖列表中。
- 触发更新: 当响应式对象的属性值发生变化时(调用ref的set方法),会触发依赖列表中的所有副作用函数重新执行。
最基础实现-ref
- get:当我们读取
.value的时候,触发 get 此时在 get 中会收集依赖,也就是建立响应式数据和 effect 之间的关联关系。 - set:当我们重新给
.value赋值的时候,触发 set,此时在 set 中会找到之前 get 的时候收集的依赖,触发更新。
响应式数据(Ref)
响应式 Ref 是一个包装器对象,它可以让我们追踪简单值的变化。
import { activeSub } from "./effect";
/**
* Ref 类
*/
class RefImpl {
// 存储实际的值
_value;
// 保存和effect之间的关联关系
subs;
constructor(value) {
this._value = value;
}
get value() {
// 触发依赖收集
if (activeSub) {
this.subs = activeSub;
}
return this._value;
}
set value(newValue) {
// 触发更新
this._value = newValue;
// 通知effect重新执行
this.subs?.();
}
}
export function ref(value) {
return new RefImpl(value);
}副作用函数(effect)
副作用是指那些依赖响应式数据的函数,当数据发生变化时,这些函数会自动重新执行。
// 当前正在收集的副作用函数,在模块中导出变量,这个时候当我执行 effect 的时候,我就把当前正在执行的函数,放到 activeSub 中,当然这么做只是为了我们在收集依赖的时候能找到它,如果你还是不理解,那你就把他想象成一个全局变量,这个时候如果执行 effect 那全局变量上就有一个正在执行的函数,就是 activeSub
export let activeSub;
// effect 函数用于注册副作用函数
// 执行传入的函数,并在执行期间自动收集依赖
export function effect(fn) {
// 设置当前活跃的副作用函数,方便在 get 中收集依赖
activeSub = fn;
// 执行副作用函数,此时会触发依赖收集
activeSub();
// 清空当前活跃的副作用函数
activeSub = undefined;
}我们再给 RefImpl 类添加一个标记,来表示这是一个ref。
import { activeSub } from "./effect";
enum ReactiveFlags {
// 属性标记,用于表示对象是不是一个ref
IS_REF = "__v_isRef",
}
/**
* Ref 类
*/
class RefImpl {
// 存储实际的值
_value;
// 保存和effect之间的关联关系
subs;
// ref标记,证明是一个ref
[ReactiveFlags.IS_REF] = true;
constructor(value) {
this._value = value;
}
get value() {
// 触发依赖收集
if (activeSub) {
this.subs = activeSub;
}
return this._value;
}
set value(newValue) {
// 触发更新
this._value = newValue;
// 通知effect重新执行
this.subs?.();
}
}
export function ref(value) {
return new RefImpl(value);
}
/**
* 判断是不是一个ref
*/
export function isRef(value) {
return !!(value && value[ReactiveFlags.IS_REF]);
}此时我们就实现了一个最基础的响应式系统,当我们读取 ref 的 value 属性时,会触发依赖收集,当我们给 ref 的 value 属性赋值时,会触发更新。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script type="module">
import { ref, effect } from '../dist/reactivity.esm.js'
const count = ref(0)
effect(() => {
console.log('count.value=>', count.value)
})
setTimeout(() => {
count.value++
}, 1000)
</script>
</body>
</html>但是这样实现有一个问题,就是我们只能收集到一个副作用函数,当我们有多个副作用函数依赖同一个响应式数据时,就会有问题。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script type="module">
import { ref, effect } from '../dist/reactivity.esm.js'
const count = ref(0)
effect(() => {
console.log('count.value1=>', count.value)
})
effect(() => {
console.log('count.value2=>', count.value)
})
setTimeout(() => {
count.value++
}, 1000)
</script>
</body>
</html>此时我们会发现打印结果如下:
count.value1=> 0
count.value2=> 0
count.value2=> 1后面的effect把前一个给覆盖了,所以只有最后一个effect会触发更新。
链表应用
为了解决这个问题,我们需要用双向链表来优化一下。
import { activeSub } from "./effect";
/**
* 链表节点
*/
interface Link {
// 保存effect
sub: Function;
// 下一个节点
nextSub: Link | undefined;
// 上一个节点
prevSub: Link | undefined;
}
enum ReactiveFlags {
// 属性标记,用于表示对象是不是一个ref
IS_REF = "__v_isRef",
}
/**
* Ref 类
*/
class RefImpl {
// 存储实际的值
_value;
// 订阅者链表的头结点
subs: Link;
// 订阅者链表的尾结点
subsTail: Link;
// ref标记,证明是一个ref
[ReactiveFlags.IS_REF] = true;
constructor(value) {
this._value = value;
}
get value() {
// 触发依赖收集
if (activeSub) {
// 创建一个新的节点
const newLink = {
sub: activeSub,
nextSub: undefined,
prevSub: undefined,
};
// 尾节点有就往尾节点后面添加,如果没有尾节点,那就说明链表为空,就把新节点作为头结点和尾结点
if (this.subsTail) {
this.subsTail.nextSub = newLink;
newLink.prevSub = this.subsTail;
this.subsTail = newLink;
} else {
this.subs = newLink;
this.subsTail = newLink;
}
}
return this._value;
}
set value(newValue) {
// 触发更新
this._value = newValue;
// 通知effect重新执行
let link = this.subs;
let queuedEffect = [];
while (link) {
// 先把当前节点的effect添加到队列中
queuedEffect.push(link.sub);
// 然后把当前节点赋值为下一个节点,继续循环
link = link.nextSub;
}
// 遍历完链表后,执行队列中的effect
queuedEffect.forEach((effect) => effect());
}
}
export function ref(value) {
return new RefImpl(value);
}
/**
* 判断是不是一个ref
*/
export function isRef(value) {
return !!(value && value[ReactiveFlags.IS_REF]);
}整理封装一下代码:
export interface Link {
// 保存当前要关联的 effect
sub: Function
// 链表的下一个节点
nextSub: Link
// 链表的上一个节点
prevSub: Link
}
/**
* 链接链表关系
* @param dep
* @param sub
*/
export function link(dep, sub) {
// 如果 activeSub 有,那就保存起来,等我更新的时候,触发
const newLink = {
sub,
nextSub: undefined,
prevSub: undefined,
}
/**
* 关联链表关系,分两种情况
* 1. 尾节点有,那就往尾节点后面加
* 2. 如果尾节点没有,则表示第一次关联,那就往头节点加,头尾相同
*/
if (dep.subsTail) {
dep.subsTail.nextSub = newLink
newLink.prevSub = dep.subsTail
dep.subsTail = newLink
} else {
dep.subs = newLink
dep.subsTail = newLink
}
}
/**
* 传播更新的函数
* @param subs
*/
export function propagate(subs) {
let link = subs
let queuedEffect = []
while (link) {
queuedEffect.push(link.sub)
link = link.nextSub
}
queuedEffect.forEach(effect => effect())
}import { activeSub } from './effect'
import { Link, link, propagate } from './system'
enum ReactiveFlags {
// 属性标记,用于表示对象是不是一个ref
IS_REF = '__v_isRef',
}
/**
* Ref 类
*/
class RefImpl {
// 存储实际的值
_value
// 订阅者链表的头结点
subs: Link
// 订阅者链表的尾结点
subsTail: Link;
// ref标记,证明是一个ref
[ReactiveFlags.IS_REF] = true
constructor(value) {
this._value = value
}
get value() {
// 触发依赖收集
if (activeSub) {
trackRef(this)
}
return this._value
}
set value(newValue) {
// 触发更新
this._value = newValue
triggerRef(this)
}
}
export function ref(value) {
return new RefImpl(value)
}
/**
* 判断是不是一个ref
*/
export function isRef(value) {
return !!(value && value[ReactiveFlags.IS_REF])
}
/**
* 收集依赖,建立 ref 和 effect 之间的链表关系
* @param dep
*/
export function trackRef(dep) {
if (activeSub) {
link(dep, activeSub)
}
}
/**
* 触发 ref 关联的 effect 重新执行
* @param dep
*/
export function triggerRef(dep) {
if (dep.subs) {
propagate(dep.subs)
}
}