【学习】响应系统
响应式更新
能自动追踪依赖的技术被称为细粒度更新,是许多前端框架建立状态变化到视图变化的底层原理。
基础实现
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中移除该effectfor (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变化的effectconst subs: Subs = new Set<Effect>();// 自动追踪依赖const getter = (): T => {// 获取当前上下文的effectconst 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();
};
测试用例
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();// 创建副作用,依赖 countuseEffect(() => {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依赖auseEffect(() => {callbackA(getA());});// 副作用B依赖buseEffect(() => {callbackB(getB());});// 初始执行expect(callbackA).toHaveBeenCalledWith("a");expect(callbackB).toHaveBeenCalledWith("b");expect(callbackA).toHaveBeenCalledTimes(1);expect(callbackB).toHaveBeenCalledTimes(1);// 更新a,只触发AsetA("a1");expect(callbackA).toHaveBeenCalledTimes(2);expect(callbackA).toHaveBeenCalledWith("a1");expect(callbackB).toHaveBeenCalledTimes(1);// 更新b,只触发BsetB("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,否则依赖buseEffect(() => {if (getFlag()) {callback(getA());} else {callback(getB());}});// 初始状态:依赖aexpect(callback).toHaveBeenCalledWith(0);callback.mockClear();// 更新a应该触发setA(1);expect(callback).toHaveBeenCalledWith(1);callback.mockClear();// 更新b不应该触发setB(1);expect(callback).not.toHaveBeenCalled();callback.mockClear();// 切换flag,此时应该依赖bsetFlag(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);});
});