【学习】响应系统

响应式更新

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

基础实现

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);
	});
});
相关推荐
buibui3 小时前
Vue3组件库搭建5-发布
前端
mingupup3 小时前
WPF依赖属性学习
学习·wpf
Sherry0073 小时前
【译】CSS 高度之谜:破解百分比高度的秘密
前端·css·面试
我命由我123453 小时前
Photoshop - Photoshop 分享作品和设计
学习·ui·adobe·媒体·设计·photoshop·美工
叫我詹躲躲3 小时前
Web Animation性能优化:从EffectTiming到动画合成
前端·javascript
_AaronWong3 小时前
基于 CropperJS 的图片编辑器实现
前端·vue.js·图片资源
叫我詹躲躲3 小时前
3 分钟掌握前端 IndexedDB 高效用法,告别本地存储焦虑
前端·indexeddb
默默地离开3 小时前
React Native 入门实战:样式、状态管理与网络请求全解析 (二)
前端·react native
Q_Q5110082853 小时前
python+springboot+vue的旅游门票信息系统web
前端·spring boot·python·django·flask·node.js·php