响应式更新
能自动追踪依赖的技术被称为细粒度更新,是许多前端框架建立状态变化到视图变化的底层原理。
基础实现
ts
interface Effect {
execute: () => void;
deps: Set<Set<Effect>>;
}
type Subs = Set<Effect>;
interface StackImp<T = any> {
push(...elements: T[]): void; // 添加一个(或几个)新元素到栈顶;
pop(): T | undefined; // 移除栈顶元素,同时返回被移除的元素;
peek(): T | undefined; // 返回栈顶的元素,不对栈做任何修改;
isEmpty(): boolean; // 如果栈里没有任何元素就返回true,否则返回false;
clear(): void; // 移除栈里所有元素;
size(): number; // 返回栈里的元素个数。
}
class Stack<T = any> implements StackImp<T> {
#count: number = 0;
#items: { [key: number]: T } = {};
push(...elements: T[]): void {
for (let index = 0; index < elements.length; index++, this.#count++) {
const element = elements[index];
this.#items[this.#count] = element;
}
}
pop(): T | undefined {
if (this.isEmpty()) {
return undefined;
}
this.#count--;
const result = this.#items[this.#count];
delete this.#items[this.#count];
return result;
}
peek(): T | undefined {
if (this.isEmpty()) {
return undefined;
}
return this.#items[this.#count - 1];
}
isEmpty(): boolean {
return this.#count === 0;
}
clear(): void {
this.#items = {};
this.#count = 0;
}
size(): number {
return this.#count;
}
}
// 保存副作用函数和依赖栈
const effectsStack = new Stack<Effect>();
// 建立订阅联系
const subscribe = (effect: Effect, subs: Subs) => {
subs.add(effect);
effect.deps.add(subs);
};
// 清除订阅联系
const cleanup = (effect: Effect) => {
// 从该effect订阅的所有state对应的subs中移除该effect
for (const subs of effect.deps) {
subs.delete(effect);
}
// 将该effect依赖的所有state对应的subs移除
effect.deps.clear();
};
// 创建响应式数据
export const useState = <T extends any>(
value: T
): [() => T, (newValue: T) => void] => {
// 保存订阅该state变化的effect
const subs: Subs = new Set<Effect>();
// 自动追踪依赖
const getter = (): T => {
// 获取当前上下文的effect
const effect = effectsStack.peek();
if (effect) {
// 建立订阅发布关系
subscribe(effect, subs);
}
return value;
};
// 触发依赖
const setter = (newValue: T) => {
value = newValue;
// 通知所有订阅该state变化的副作用函数执行
for (const effect of [...subs]) {
effect.execute();
}
};
return [getter, setter];
};
// 创建副作用函数和依赖
export const useEffect = (callback: () => void) => {
const execute = () => {
// 重置依赖
cleanup(effect);
// 将当前effect推入栈顶
effectsStack.push(effect);
try {
// 执行回调
callback();
} catch (error) {
console.log(error);
} finally {
// 当前effect出栈
effectsStack.pop();
}
};
const effect: Effect = {
execute,
deps: new Set(),
};
// 立刻执行一次,建立订阅发布关系
execute();
};
测试用例
ts
import { describe, test, expect } from "@jest/globals";
import { useState, useEffect } from "./reactive";
describe("reactive", () => {
beforeEach(() => {
// 重置所有可能的全局状态(如果有)
jest.clearAllMocks();
});
test("useState 应该正确返回 getter 和 setter 并维护状态", () => {
const [getCount, setCount] = useState(0);
// 初始值检查
expect(getCount()).toBe(0);
// 设值后检查
setCount(1);
expect(getCount()).toBe(1);
// 多次设值验证
setCount(100);
expect(getCount()).toBe(100);
});
test("useEffect 应该在依赖变化时执行", () => {
const [getCount, setCount] = useState(0);
const callback = jest.fn();
// 创建副作用,依赖 count
useEffect(() => {
callback(getCount());
});
// 初始执行一次
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(0);
// 第一次更新
setCount(1);
expect(callback).toHaveBeenCalledTimes(2);
expect(callback).toHaveBeenCalledWith(1);
// 第二次更新
setCount(2);
expect(callback).toHaveBeenCalledTimes(3);
expect(callback).toHaveBeenCalledWith(2);
});
test("useEffect 应该只依赖相关状态变化", () => {
const [getA, setA] = useState("a");
const [getB, setB] = useState("b");
const callbackA = jest.fn();
const callbackB = jest.fn();
// 副作用A依赖a
useEffect(() => {
callbackA(getA());
});
// 副作用B依赖b
useEffect(() => {
callbackB(getB());
});
// 初始执行
expect(callbackA).toHaveBeenCalledWith("a");
expect(callbackB).toHaveBeenCalledWith("b");
expect(callbackA).toHaveBeenCalledTimes(1);
expect(callbackB).toHaveBeenCalledTimes(1);
// 更新a,只触发A
setA("a1");
expect(callbackA).toHaveBeenCalledTimes(2);
expect(callbackA).toHaveBeenCalledWith("a1");
expect(callbackB).toHaveBeenCalledTimes(1);
// 更新b,只触发B
setB("b1");
expect(callbackB).toHaveBeenCalledTimes(2);
expect(callbackB).toHaveBeenCalledWith("b1");
expect(callbackA).toHaveBeenCalledTimes(2);
});
test("useEffect 应该清理旧依赖", () => {
const [getFlag, setFlag] = useState(true);
const [getA, setA] = useState(0);
const [getB, setB] = useState(0);
const callback = jest.fn();
// 条件依赖:flag为true时依赖a,否则依赖b
useEffect(() => {
if (getFlag()) {
callback(getA());
} else {
callback(getB());
}
});
// 初始状态:依赖a
expect(callback).toHaveBeenCalledWith(0);
callback.mockClear();
// 更新a应该触发
setA(1);
expect(callback).toHaveBeenCalledWith(1);
callback.mockClear();
// 更新b不应该触发
setB(1);
expect(callback).not.toHaveBeenCalled();
callback.mockClear();
// 切换flag,此时应该依赖b
setFlag(false);
expect(callback).toHaveBeenCalledWith(1); // 触发一次新的依赖收集
callback.mockClear();
// 现在更新a不应该触发
setA(2);
expect(callback).not.toHaveBeenCalled();
callback.mockClear();
// 更新b应该触发
setB(2);
expect(callback).toHaveBeenCalledWith(2);
});
test("多个副作用应该独立工作", () => {
const [getCount, setCount] = useState(0);
const effect1 = jest.fn();
const effect2 = jest.fn();
useEffect(() => effect1(getCount()));
useEffect(() => effect2(getCount() * 2));
// 初始执行
expect(effect1).toHaveBeenCalledWith(0);
expect(effect2).toHaveBeenCalledWith(0);
// 更新后两个副作用都应执行
setCount(3);
expect(effect1).toHaveBeenCalledWith(3);
expect(effect2).toHaveBeenCalledWith(6);
expect(effect1).toHaveBeenCalledTimes(2);
expect(effect2).toHaveBeenCalledTimes(2);
});
});