- state.js: 新增 shallowEqual、update()、setIn()、batch() 方法 - input-area.js: messages.push() 全部改为 state.update(),确保引用变化触发通知 - model-bar.js: 直接属性赋值改为 state.update(),修复 model 切换不触发变更检测
206 lines
5.6 KiB
JavaScript
206 lines
5.6 KiB
JavaScript
/**
|
|
* State - 轻量级响应式状态管理
|
|
*
|
|
* 优化点:
|
|
* 1. 结构化浅比较:对象按 key 数量 + 逐 key 严格比较,数组按长度 + 逐元素
|
|
* 2. update():函数式更新,自动比较
|
|
* 3. setIn():路径式嵌套更新,沿途浅拷贝确保引用变化
|
|
* 4. batch():批量更新,合并通知
|
|
*/
|
|
|
|
// ── 结构化浅比较 ──
|
|
function shallowEqual(a, b) {
|
|
if (a === b) return true;
|
|
if (a == null || b == null) return a === b;
|
|
if (typeof a !== 'object' || typeof b !== 'object') return false;
|
|
|
|
// 数组:长度 + 逐元素
|
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
if (a.length !== b.length) return false;
|
|
for (let i = 0; i < a.length; i++) {
|
|
if (a[i] !== b[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// 混合类型
|
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
|
|
// 对象:key 数量 + 逐 key
|
|
const keysA = Object.keys(a);
|
|
const keysB = Object.keys(b);
|
|
if (keysA.length !== keysB.length) return false;
|
|
for (const key of keysA) {
|
|
if (!Object.prototype.hasOwnProperty.call(b, key) || a[key] !== b[key]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
class AppState {
|
|
constructor() {
|
|
this._state = {};
|
|
this._listeners = new Map();
|
|
this._batching = false;
|
|
this._batchQueue = null;
|
|
}
|
|
|
|
/**
|
|
* 设置状态值,使用结构化浅比较判断变更
|
|
*/
|
|
set(key, value) {
|
|
const old = this._state[key];
|
|
this._state[key] = value;
|
|
|
|
if (!shallowEqual(old, value)) {
|
|
if (this._batching) {
|
|
this._batchQueue.add(key);
|
|
} else {
|
|
this._notify(key, value, old);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 函数式更新:基于当前值计算新值,自动比较
|
|
*
|
|
* state.update('currentSession', session => ({
|
|
* ...session,
|
|
* messages: [...session.messages, newMsg],
|
|
* updatedAt: Date.now()
|
|
* }));
|
|
*/
|
|
update(key, updater) {
|
|
const old = this._state[key];
|
|
const value = updater(old);
|
|
this._state[key] = value;
|
|
|
|
if (!shallowEqual(old, value)) {
|
|
if (this._batching) {
|
|
this._batchQueue.add(key);
|
|
} else {
|
|
this._notify(key, value, old);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 路径式嵌套更新:按 path 更新嵌套属性,沿途浅拷贝确保引用变化
|
|
*
|
|
* state.setIn('currentSession', ['messages', 0, 'content'], '新内容');
|
|
*/
|
|
setIn(key, path, value) {
|
|
if (!path || path.length === 0) {
|
|
this.set(key, value);
|
|
return;
|
|
}
|
|
|
|
const old = this._state[key];
|
|
if (old == null || typeof old !== 'object') {
|
|
console.warn(`[State] setIn 失败: ${key} 不是对象`);
|
|
return;
|
|
}
|
|
|
|
const newRoot = Array.isArray(old) ? [...old] : { ...old };
|
|
let current = newRoot;
|
|
|
|
for (let i = 0; i < path.length - 1; i++) {
|
|
const segment = path[i];
|
|
const next = current[segment];
|
|
if (next == null || typeof next !== 'object') {
|
|
const isIndex = typeof path[i + 1] === 'number';
|
|
current[segment] = isIndex ? [] : {};
|
|
} else {
|
|
current[segment] = Array.isArray(next) ? [...next] : { ...next };
|
|
}
|
|
current = current[segment];
|
|
}
|
|
|
|
current[path[path.length - 1]] = value;
|
|
|
|
this._state[key] = newRoot;
|
|
if (!shallowEqual(old, newRoot)) {
|
|
if (this._batching) {
|
|
this._batchQueue.add(key);
|
|
} else {
|
|
this._notify(key, newRoot, old);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 批量更新:合并多次 set 为一次通知
|
|
*
|
|
* state.batch(() => {
|
|
* state.set('isStreaming', false);
|
|
* state.update('currentSession', s => ({ ...s, updatedAt: Date.now() }));
|
|
* });
|
|
*/
|
|
batch(fn) {
|
|
this._batching = true;
|
|
this._batchQueue = new Set();
|
|
try {
|
|
fn();
|
|
} finally {
|
|
const keys = this._batchQueue;
|
|
this._batching = false;
|
|
this._batchQueue = null;
|
|
for (const k of keys) {
|
|
this._notify(k, this._state[k], undefined);
|
|
}
|
|
}
|
|
}
|
|
|
|
_notify(key, value, old) {
|
|
if (!this._listeners.has(key)) return;
|
|
for (const cb of this._listeners.get(key)) {
|
|
try { cb(value, old); } catch (e) {
|
|
console.error(`[State] 监听器错误 (${key}):`, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取状态值
|
|
*/
|
|
get(key, defaultValue = undefined) {
|
|
return key in this._state ? this._state[key] : defaultValue;
|
|
}
|
|
|
|
/**
|
|
* 订阅状态变更
|
|
* @returns {function} 取消订阅函数
|
|
*/
|
|
on(key, callback) {
|
|
if (!this._listeners.has(key)) {
|
|
this._listeners.set(key, new Set());
|
|
}
|
|
this._listeners.get(key).add(callback);
|
|
return () => this._listeners.get(key)?.delete(callback);
|
|
}
|
|
|
|
/**
|
|
* 批量设置初始状态
|
|
*/
|
|
init(initial) {
|
|
Object.assign(this._state, initial);
|
|
}
|
|
}
|
|
|
|
// 单例
|
|
export const state = new AppState();
|
|
|
|
export const KEYS = {
|
|
DB: 'db',
|
|
API: 'api',
|
|
CURRENT_SESSION: 'currentSession',
|
|
IS_STREAMING: 'isStreaming',
|
|
IS_HISTORY_VIEW: 'isHistoryView',
|
|
PENDING_IMAGES: 'pendingImages',
|
|
ABORT_CONTROLLER: 'abortController',
|
|
SYSTEM_PROMPT: 'systemPrompt',
|
|
SYSTEM_PROMPT_ENABLED: 'systemPromptEnabled',
|
|
NUM_CTX: 'numCtx',
|
|
HISTORY_PAGE: 'historyPage',
|
|
HISTORY_SEARCH: 'historySearchQuery',
|
|
};
|