保存一下
This commit is contained in:
158
src/core/state/Observable.ts
Normal file
158
src/core/state/Observable.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
type Listener<T> = (state: T) => void
|
||||
type KeyListener<T, K extends keyof T> = (changed: Pick<T, K>) => void
|
||||
|
||||
/**
|
||||
* 从给定类型 T 中排除所有函数类型的属性,只保留非函数类型的属性
|
||||
* @template T - 需要处理的原始类型
|
||||
* @returns 一个新的类型,该类型只包含原始类型中非函数类型的属性
|
||||
*/
|
||||
type NonFunctionProperties<T> = {
|
||||
[K in keyof T]: T[K] extends Function ? never : T[K]
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个可观察对象,用于管理状态和事件。
|
||||
* @template T - 需要处理的状态类型
|
||||
* @example
|
||||
* interface AppState {
|
||||
* count: number
|
||||
* isOpen: boolean
|
||||
* title: string
|
||||
* }
|
||||
*
|
||||
* const app = new Observable<AppState>({
|
||||
* count: 0,
|
||||
* isOpen: false,
|
||||
* title: "Demo"
|
||||
* })
|
||||
*
|
||||
* // 全量订阅
|
||||
* app.subscribe(state => console.log("全量:", state))
|
||||
*
|
||||
* // 单字段订阅
|
||||
* app.subscribeKey("count", changes => console.log("count 变化:", changes))
|
||||
*
|
||||
* // 多字段订阅
|
||||
* app.subscribeKey(["count", "isOpen"], changes =>
|
||||
* console.log("count/isOpen 回调:", changes)
|
||||
* )
|
||||
*
|
||||
* // 直接修改属性
|
||||
* app.count = 1
|
||||
* app.isOpen = true
|
||||
* app.title = "New Title"
|
||||
*
|
||||
* // 输出示例:
|
||||
* // 全量: { count: 1, isOpen: true, title: 'New Title' }
|
||||
* // count 变化: { count: 1 }
|
||||
* // count/isOpen 回调: { count: 1, isOpen: true }
|
||||
*/
|
||||
export class Observable<T extends object> {
|
||||
private listeners = new Set<WeakRef<Listener<T>>>()
|
||||
private keyListeners = new Map<keyof T, Set<WeakRef<Function>>>()
|
||||
private registry = new FinalizationRegistry((ref: WeakRef<any>) => {
|
||||
this.listeners.delete(ref)
|
||||
this.keyListeners.forEach(set => set.delete(ref))
|
||||
})
|
||||
|
||||
private pendingKeys = new Set<keyof T>()
|
||||
private notifyScheduled = false
|
||||
|
||||
constructor(initialState: NonFunctionProperties<T>) {
|
||||
Object.assign(this, initialState)
|
||||
|
||||
// Proxy 拦截属性修改
|
||||
return new Proxy(this, {
|
||||
set: (target, prop: string, value) => {
|
||||
const key = prop as keyof T
|
||||
(target as any)[key] = value
|
||||
|
||||
// 每次赋值都加入 pendingKeys
|
||||
this.pendingKeys.add(key)
|
||||
this.scheduleNotify()
|
||||
return true
|
||||
},
|
||||
get: (target, prop: string) => (target as any)[prop]
|
||||
})
|
||||
}
|
||||
|
||||
/** 安排微任务通知 */
|
||||
private scheduleNotify() {
|
||||
if (!this.notifyScheduled) {
|
||||
this.notifyScheduled = true
|
||||
Promise.resolve().then(() => this.flushNotify())
|
||||
}
|
||||
}
|
||||
|
||||
/** 执行通知:全量 + 单/多字段通知 */
|
||||
private flushNotify() {
|
||||
const keys = Array.from(this.pendingKeys)
|
||||
this.pendingKeys.clear()
|
||||
this.notifyScheduled = false
|
||||
|
||||
// 全量通知一次
|
||||
for (const ref of this.listeners) {
|
||||
const fn = ref.deref()
|
||||
if (fn) fn(this as unknown as T)
|
||||
else this.listeners.delete(ref)
|
||||
}
|
||||
|
||||
// 单/多字段通知(合并函数)
|
||||
const fnMap = new Map<Function, (keyof T)[]>()
|
||||
|
||||
for (const key of keys) {
|
||||
const set = this.keyListeners.get(key)
|
||||
if (!set) continue
|
||||
for (const ref of set) {
|
||||
const fn = ref.deref()
|
||||
if (!fn) {
|
||||
set.delete(ref)
|
||||
continue
|
||||
}
|
||||
if (!fnMap.has(fn)) fnMap.set(fn, [])
|
||||
const arr = fnMap.get(fn)!
|
||||
if (!arr.includes(key)) arr.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用每个函数一次,并返回订阅字段的当前值
|
||||
fnMap.forEach((subKeys, fn) => {
|
||||
const result: Partial<T> = {}
|
||||
subKeys.forEach(k => (result[k] = (this as any)[k]))
|
||||
fn(result)
|
||||
})
|
||||
}
|
||||
|
||||
/** 全量订阅 */
|
||||
subscribe(fn: Listener<T>) {
|
||||
const ref = new WeakRef(fn)
|
||||
this.listeners.add(ref)
|
||||
this.registry.register(fn, ref)
|
||||
return () => {
|
||||
this.listeners.delete(ref)
|
||||
this.registry.unregister(fn)
|
||||
}
|
||||
}
|
||||
|
||||
/** 单字段或多字段订阅 */
|
||||
subscribeKey<K extends keyof T>(keys: K | K[], fn: KeyListener<T, K>) {
|
||||
const keyArray = Array.isArray(keys) ? keys : [keys]
|
||||
const refs: WeakRef<Function>[] = []
|
||||
|
||||
for (const key of keyArray) {
|
||||
if (!this.keyListeners.has(key)) this.keyListeners.set(key, new Set())
|
||||
const ref = new WeakRef(fn)
|
||||
this.keyListeners.get(key)!.add(ref)
|
||||
this.registry.register(fn, ref)
|
||||
refs.push(ref)
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (let i = 0; i < keyArray.length; i++) {
|
||||
const set = this.keyListeners.get(keyArray[i])
|
||||
if (set) set.delete(refs[i])
|
||||
}
|
||||
this.registry.unregister(fn)
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/core/state/useObservableReact.ts
Normal file
32
src/core/state/useObservableReact.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// import { useEffect, useState } from "react"
|
||||
// import { Observable } from "./Observable"
|
||||
//
|
||||
// export function useObservable<T extends object, K extends keyof T>(
|
||||
// observable: Observable<T>,
|
||||
// keys?: K | K[]
|
||||
// ): Pick<T, K> | T {
|
||||
// const keyArray = keys ? (Array.isArray(keys) ? keys : [keys]) : null
|
||||
//
|
||||
// const [state, setState] = useState<Partial<T>>(() => {
|
||||
// if (keyArray) {
|
||||
// const init: Partial<T> = {}
|
||||
// keyArray.forEach(k => (init[k] = (observable as any)[k]))
|
||||
// return init
|
||||
// }
|
||||
// return { ...(observable as any) }
|
||||
// })
|
||||
//
|
||||
// useEffect(() => {
|
||||
// const unsubscribe = keyArray
|
||||
// ? observable.subscribeKey(keyArray as K[], changed => {
|
||||
// setState(prev => ({ ...prev, ...changed }))
|
||||
// })
|
||||
// : observable.subscribe(s => setState({ ...s }))
|
||||
//
|
||||
// return () => {
|
||||
// unsubscribe()
|
||||
// }
|
||||
// }, [observable, ...(keyArray || [])])
|
||||
//
|
||||
// return state as any
|
||||
// }
|
||||
38
src/core/state/useObservableVue.ts
Normal file
38
src/core/state/useObservableVue.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Observable } from '@/core/state/Observable.ts'
|
||||
import { onUnmounted, ref, type Ref } from 'vue'
|
||||
|
||||
/**
|
||||
* vue使用自定义的 observable
|
||||
* @param observable
|
||||
* @param keys
|
||||
* @returns
|
||||
* @example
|
||||
* const app = new Observable({ count: 0, isOpen: false, title: "Demo" })
|
||||
* // 多字段订阅
|
||||
* const state = useObservable(app, ["count", "isOpen"])
|
||||
* // state.value.count / state.value.isOpen 响应式
|
||||
*/
|
||||
export function useObservable<T extends object, K extends keyof T>(
|
||||
observable: Observable<T>,
|
||||
keys?: K[] | K
|
||||
): Ref<Pick<T, K> | T> {
|
||||
const state = ref({} as any) // 响应式
|
||||
|
||||
const keyArray = keys ? (Array.isArray(keys) ? keys : [keys]) : null
|
||||
|
||||
// 订阅回调
|
||||
const unsubscribe = keyArray
|
||||
? observable.subscribeKey(keyArray as K[], changed => {
|
||||
// 更新响应式 state
|
||||
Object.assign(state.value, changed)
|
||||
})
|
||||
: observable.subscribe(s => {
|
||||
state.value = { ...s }
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
return state
|
||||
}
|
||||
Reference in New Issue
Block a user