【学习】响应系统

响应式更新

能自动追踪依赖的技术被称为细粒度更新,是许多前端框架建立状态变化到视图变化的底层原理。

基础实现

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);
	});
});
相关推荐
学习_学习_再学习19 小时前
ffmpeg学习记录
学习·ffmpeg
十一.36619 小时前
37-38 for循环
前端·javascript·html
波诺波19 小时前
环境管理器
linux·前端·python
San30.19 小时前
深入理解浏览器渲染流程:从HTML/CSS到像素的奇妙旅程
前端·css·html
IT_陈寒20 小时前
5个Python 3.12新特性让你的代码效率提升50%,第3个太实用了!
前端·人工智能·后端
周杰伦_Jay20 小时前
【Python Web开源框架】Django/Flask/FastAPI/Tornado/Pyramid
前端·python·开源
im_AMBER20 小时前
算法笔记 10
笔记·学习·算法·leetcode
艾小码20 小时前
为什么你的JavaScript代码总是出bug?这5个隐藏陷阱太坑了!
前端·javascript
辻戋1 天前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保1 天前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架