From cc93ced47802f1caef477293398afe73d90e1694 Mon Sep 17 00:00:00 2001 From: * <8> Date: Tue, 1 Nov 2022 20:26:21 +0800 Subject: [PATCH] Match-id-835607f8b2fd6715e190a54cee5627437d7cb51f --- libs/horizon/src/horizonx/adapters/redux.ts | 2 +- .../src/horizonx/adapters/reduxReact.ts | 26 +- .../src/horizonx/proxy/HooklessObserver.ts | 2 +- libs/horizon/src/horizonx/proxy/Observer.ts | 2 +- .../src/horizonx/proxy/ProxyHandler.ts | 2 +- .../src/horizonx/store/StoreHandler.ts | 228 ++++++++---------- libs/horizon/src/horizonx/types.d.ts | 65 +++++ libs/horizon/src/renderer/vnode/VNode.ts | 5 +- .../StoreFunctionality/async.test.tsx | 10 +- 9 files changed, 175 insertions(+), 167 deletions(-) diff --git a/libs/horizon/src/horizonx/adapters/redux.ts b/libs/horizon/src/horizonx/adapters/redux.ts index d84acbf0..a838d45f 100644 --- a/libs/horizon/src/horizonx/adapters/redux.ts +++ b/libs/horizon/src/horizonx/adapters/redux.ts @@ -73,7 +73,7 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?): }, }, options: { - reduxAdapter: true, + isReduxAdapter: true, }, })(); diff --git a/libs/horizon/src/horizonx/adapters/reduxReact.ts b/libs/horizon/src/horizonx/adapters/reduxReact.ts index c804c469..8f2818b3 100644 --- a/libs/horizon/src/horizonx/adapters/reduxReact.ts +++ b/libs/horizon/src/horizonx/adapters/reduxReact.ts @@ -13,7 +13,6 @@ * See the Mulan PSL v2 for more details. */ -// @ts-ignore import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/HookExternal'; import { createContext } from '../../renderer/components/context/CreateContext'; import { createElement } from '../../external/JSXElement'; @@ -78,31 +77,12 @@ export const useStore = () => { return createStoreHook(DefaultContext)(); }; -// function shallowCompare(a,b){ -// return Object.keys(a).length === Object.keys(b).length && -// Object.keys(a).every(key => a[key] === b[key]); -// } - -//TODO: implement options -// context?: Object, -// areStatesEqual?: Function, :) -// areOwnPropsEqual?: Function, -// areStatePropsEqual?: Function, -// areMergedPropsEqual?: Function, -// forwardRef?: boolean, -// const defaultOptions = { -// areStatesEqual: shallowCompare, -// areOwnPropsEqual: shallowCompare, -// areStatePropsEqual: shallowCompare, -// areMergedPropsEqual: shallowCompare -// }; - export function connect( - mapStateToProps?: (state: any, ownProps: { [key: string]: any }) => Object, + mapStateToProps?: (state: any, ownProps: { [key: string]: any }) => object, mapDispatchToProps?: | { [key: string]: (...args: any[]) => ReduxAction } - | ((dispatch: (action: ReduxAction) => any, ownProps?: Object) => Object), - mergeProps?: (stateProps: Object, dispatchProps: Object, ownProps: Object) => Object, + | ((dispatch: (action: ReduxAction) => any, ownProps?: object) => object), + mergeProps?: (stateProps: object, dispatchProps: object, ownProps: object) => object, options?: { areStatesEqual?: (oldState: any, newState: any) => boolean; context?: any; // TODO: type this diff --git a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts index 1f2bf922..ba390a3a 100644 --- a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts +++ b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts @@ -13,7 +13,7 @@ * See the Mulan PSL v2 for more details. */ -import { IObserver } from './Observer'; +import type { IObserver } from './Observer'; /** * 一个对象(对象、数组、集合)对应一个Observer diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index 73d4a2cc..0db792a0 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -123,7 +123,7 @@ export class Observer implements IObserver { } } - // 删除keyVNodes中保存的这个VNode的关系数据 + // 删除Observer中保存的这个VNode的关系数据 clearByVNode(vNode: VNode): void { const keys = this.vNodeKeys.get(vNode); if (keys) { diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index 9dc22b16..9ea2c7af 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -19,7 +19,7 @@ import { HooklessObserver } from './HooklessObserver'; import { isArray, isCollection, isObject } from '../CommonUtils'; import { createArrayProxy } from './handlers/ArrayProxyHandler'; import { createCollectionProxy } from './handlers/CollectionProxyHandler'; -import { IObserver } from '../types'; +import type { IObserver } from '../types'; import { OBSERVER_KEY } from '../Constants'; // 保存rawObj -> Proxy diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index 30ba7a89..aaa8e029 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -13,150 +13,81 @@ * See the Mulan PSL v2 for more details. */ -// @ts-ignore import { useEffect, useRef } from '../../renderer/hooks/HookExternal'; import { getProcessingVNode } from '../../renderer/GlobalVar'; import { createProxy } from '../proxy/ProxyHandler'; import readonlyProxy from '../proxy/readonlyProxy'; import { Observer } from '../proxy/Observer'; import { FunctionComponent, ClassComponent } from '../../renderer/vnode/VNodeTags'; -import { VNode } from '../../renderer/Types'; import { isPromise } from '../CommonUtils'; +import type { + ActionFunction, ComputedValues, + PlannedAction, QueuedStoreActions, + StoreActions, + StoreConfig, + StoreObj, + UserActions, + UserComputedValues +} from '../types'; +import {VNode} from '../../renderer/vnode/VNode'; -const storeMap = new Map>(); +const storeMap = new Map>(); -type StoreConfig, C extends UserComputedValues> = { - id?: string; - state?: S; - actions?: A; - computed?: C; - options?: any; -}; - -type UserActions = { [K: string]: ActionFunction }; -type ActionFunction = (this: StoreHandler, state: S, ...args: any[]) => any; -type StoreActions> = { [K in keyof A]: Action }; -type Action, S extends object> = ( - this: StoreHandler, - ...args: RemoveFirstFromTuple> -) => ReturnType; - -type StoreHandler, C extends UserComputedValues> = { - $s: S; - $a: StoreActions; - $c: UserComputedValues; - $queue: QueuedStoreActions; - $subscribe: (listener: () => void) => void; - $unsubscribe: (listener: () => void) => void; -} & { [K in keyof S]: S[K] } & { [K in keyof A]: Action } & { [K in keyof C]: ReturnType }; - -type PlannedAction> = { - action: string; - payload: any[]; - resolve: ReturnType; -}; -type RemoveFirstFromTuple = T['length'] extends 0 - ? [] - : ((...b: T) => void) extends (a, ...b: infer I) => void - ? I - : []; - -type UserComputedValues = { [K: string]: ComputedFunction }; - -type ComputedFunction = (state: S) => any; - -type AsyncAction, S extends object> = ( - this: StoreHandler, - ...args: RemoveFirstFromTuple> -) => Promise>; - -type QueuedStoreActions> = { [K in keyof A]: AsyncAction }; -type ComputedValues> = { [K in keyof C]: ReturnType }; export function createStore, C extends UserComputedValues>( - storeConfig: StoreConfig -): () => StoreHandler { + config: StoreConfig +): () => StoreObj { // 校验 - if (Object.prototype.toString.call(storeConfig) !== '[object Object]') { + if (Object.prototype.toString.call(config) !== '[object Object]') { throw new Error('store obj must be pure object'); } - // 创建本地浅拷贝以确保一致性(避免用户在创建存储后更改配置对象) - const config = { - id: storeConfig.id, - state: storeConfig.state, - actions: storeConfig.actions ? { ...storeConfig.actions } : undefined, - computed: storeConfig.computed ? { ...storeConfig.computed } : undefined, - options: storeConfig.options - }; - - const proxyObj = createProxy(config.state, !config.options?.reduxAdapter); + const proxyObj = createProxy(config.state, !config.options?.isReduxAdapter); proxyObj.$pending = false; - const $subscribe = listener => { - proxyObj.addListener(listener); - }; - - const $unsubscribe = listener => { - proxyObj.removeListener(listener); - }; - - const plannedActions: PlannedAction>[] = []; const $a: Partial> = {}; const $queue: Partial> = {}; const $c: Partial> = {}; - const storeHandler = { + const storeObj = { $s: proxyObj, $a: $a as StoreActions, $c: $c as ComputedValues, $queue: $queue as QueuedStoreActions, $config: config, - $subscribe, - $unsubscribe, - } as unknown as StoreHandler; + $subscribe: listener => { + proxyObj.addListener(listener); + }, + $unsubscribe: listener => { + proxyObj.removeListener(listener); + }, + } as unknown as StoreObj; - function tryNextAction() { - if (!plannedActions.length) { - proxyObj.$pending = false; - return; - } - - const nextAction = plannedActions.shift()!; - const result = config.actions - ? config.actions[nextAction.action].bind(storeHandler, proxyObj)(...nextAction.payload) - : undefined; - - if (isPromise(result)) { - result.then(value => { - nextAction.resolve(value); - tryNextAction(); - }); - } else { - nextAction.resolve(result); - tryNextAction(); - } - } + const plannedActions: PlannedAction>[] = []; // 包装actions if (config.actions) { Object.keys(config.actions).forEach(action => { + // 让store.$queue[action]可以访问到action方法 + // 要达到的效果:如果通过store.$queue[action1]调用的action1返回promise,会阻塞下一个store.$queue[action2] ($queue as any)[action] = (...payload) => { return new Promise(resolve => { if (!proxyObj.$pending) { proxyObj.$pending = true; - const result = config.actions![action].bind(storeHandler, proxyObj)(...payload); + + const result = config.actions![action].bind(storeObj, proxyObj)(...payload); if (isPromise(result)) { result.then(value => { resolve(value); - tryNextAction(); + tryNextAction(storeObj, proxyObj, config, plannedActions); }); } else { resolve(result); - tryNextAction(); + tryNextAction(storeObj, proxyObj, config, plannedActions); } } else { + // 加入队列 plannedActions.push({ action, payload, @@ -168,14 +99,14 @@ export function createStore, C extend // 让store.$a[action]可以访问到action方法 ($a as any)[action] = function Wrapped(...payload) { - return config.actions![action].bind(storeHandler, proxyObj)(...payload); + return config.actions![action].bind(storeObj, proxyObj)(...payload); }; // 让store[action]可以访问到action方法 - Object.defineProperty(storeHandler, action, { + Object.defineProperty(storeObj, action, { writable: false, value: (...payload) => { - return config.actions![action].bind(storeHandler, proxyObj)(...payload); + return config.actions![action].bind(storeObj, proxyObj)(...payload); }, }); }); @@ -184,10 +115,10 @@ export function createStore, C extend if (config.computed) { Object.keys(config.computed).forEach(computeKey => { // 让store.$c[computeKey]可以访问到computed方法 - ($c as any)[computeKey] = config.computed![computeKey].bind(storeHandler, readonlyProxy(proxyObj)); + ($c as any)[computeKey] = config.computed![computeKey].bind(storeObj, readonlyProxy(proxyObj)); // 让store[computeKey]可以访问到computed的值 - Object.defineProperty(storeHandler, computeKey, { + Object.defineProperty(storeObj, computeKey, { get: $c[computeKey] as () => any, }); }); @@ -196,7 +127,7 @@ export function createStore, C extend // 让store[key]可以访问到state的值 if (config.state) { Object.keys(config.state).forEach(key => { - Object.defineProperty(storeHandler, key, { + Object.defineProperty(storeObj, key, { get: () => { // 从Proxy对象获取值,会触发代理 return proxyObj[key]; @@ -206,14 +137,56 @@ export function createStore, C extend } if (config.id) { - storeMap.set(config.id, storeHandler); + storeMap.set(config.id, storeObj); } - return createStoreHook(storeHandler); + return createGetStore(storeObj); } -export function clearVNodeObservers(vNode) { - if (!vNode.observers) return; +// 通过该方法执行store.$queue中的action +function tryNextAction(storeObj, proxyObj, config, plannedActions) { + if (!plannedActions.length) { + proxyObj.$pending = false; + return; + } + + const nextAction = plannedActions.shift()!; + const result = config.actions + ? config.actions[nextAction.action].bind(storeObj, proxyObj)(...nextAction.payload) + : undefined; + + if (isPromise(result)) { + result.then(value => { + nextAction.resolve(value); + tryNextAction(storeObj, proxyObj, config, plannedActions); + }); + } else { + nextAction.resolve(result); + tryNextAction(storeObj, proxyObj, config, plannedActions); + } +} + +// createStore返回的是一个getStore的函数,这个函数必须要在组件(函数/类组件)里面被执行,因为要注册VNode销毁时的清理动作 +function createGetStore, C extends UserComputedValues>( + storeObj: StoreObj +): () => StoreObj { + const getStore = () => { + if (!storeObj.$config.options?.isReduxAdapter) { + registerDestroyFunction(); + } + + return storeObj; + }; + + return getStore; +} + +// 删除Observers中保存的这个VNode的相关数据 +export function clearVNodeObservers(vNode: VNode) { + if (!vNode.observers) { + return; + } + vNode.observers.forEach(observer => { observer.clearByVNode(vNode); }); @@ -221,10 +194,11 @@ export function clearVNodeObservers(vNode) { vNode.observers.clear(); } -function hookStore() { +// 注册VNode销毁时的清理动作 +function registerDestroyFunction() { const processingVNode = getProcessingVNode(); - // did not execute in a component + // 获取不到当前运行的VNode,说明不在组件中运行,属于非法场景 if (!processingVNode) { return; } @@ -233,8 +207,8 @@ function hookStore() { processingVNode.observers = new Set(); } + // 函数组件 if (processingVNode.tag === FunctionComponent) { - // from FunctionComponent const vNodeRef = useRef(processingVNode); useEffect(() => { @@ -243,10 +217,9 @@ function hookStore() { vNodeRef.current.observers = null; }; }, []); - } else if (processingVNode.tag === ClassComponent) { - // from ClassComponent + } else if (processingVNode.tag === ClassComponent) { // 类组件 if (!processingVNode.classComponentWillUnmount) { - processingVNode.classComponentWillUnmount = function (vNode) { + processingVNode.classComponentWillUnmount = (vNode) => { clearVNodeObservers(vNode); vNode.observers = null; }; @@ -254,28 +227,17 @@ function hookStore() { } } -function createStoreHook, C extends UserComputedValues>( - storeHandler: StoreHandler -): () => StoreHandler { - const storeHook = () => { - if (!storeHandler.$config.options?.suppressHooks) { - hookStore(); - } - - return storeHandler; - }; - - return storeHook; -} - +// 函数组件中使用的hook export function useStore, C extends UserComputedValues>( id: string -): StoreHandler { +): StoreObj { const storeObj = storeMap.get(id); - if (storeObj && !storeObj.$config.options?.suppressHooks) hookStore(); + if (storeObj && !storeObj.$config.options?.isReduxAdapter) { + registerDestroyFunction(); + } - return storeObj as StoreHandler; + return storeObj as StoreObj; } export function clearStore(id: string): void { diff --git a/libs/horizon/src/horizonx/types.d.ts b/libs/horizon/src/horizonx/types.d.ts index e7868c35..f1a88e56 100644 --- a/libs/horizon/src/horizonx/types.d.ts +++ b/libs/horizon/src/horizonx/types.d.ts @@ -30,3 +30,68 @@ export interface IObserver { clearByVNode: (vNode: any) => void; } + +export type StoreConfig, C extends UserComputedValues> = { + id?: string; + state?: S; + actions?: A; + computed?: C; + options?: { + isReduxAdapter?: boolean; + }; +}; + +export type UserActions = { + [K: string]: ActionFunction +}; + +type ActionFunction = (this: StoreObj, state: S, ...args: any[]) => any; + +export type StoreActions> = { + [K in keyof A]: Action +}; + +type Action, S extends object> = ( + this: StoreObj, + ...args: RemoveFirstFromTuple> +) => ReturnType; + +export type StoreObj, C extends UserComputedValues> = { + $s: S; + $a: StoreActions; + $c: UserComputedValues; + $queue: QueuedStoreActions; + $subscribe: (listener: () => void) => void; + $unsubscribe: (listener: () => void) => void; +} & { [K in keyof S]: S[K] } & { [K in keyof A]: Action } & { [K in keyof C]: ReturnType }; + +export type PlannedAction> = { + action: string; + payload: any[]; + resolve: ReturnType; +}; + +type RemoveFirstFromTuple = T['length'] extends 0 + ? [] + : ((...b: T) => void) extends (a, ...b: infer I) => void + ? I + : []; + +export type UserComputedValues = { + [K: string]: ComputedFunction +}; + +type ComputedFunction = (state: S) => any; + +export type AsyncAction, S extends object> = ( + this: StoreObj, + ...args: RemoveFirstFromTuple> +) => Promise>; + +export type QueuedStoreActions> = { + [K in keyof A]: AsyncAction +}; + +export type ComputedValues> = { + [K in keyof C]: ReturnType +}; diff --git a/libs/horizon/src/renderer/vnode/VNode.ts b/libs/horizon/src/renderer/vnode/VNode.ts index 1cea3243..a5ec09bc 100644 --- a/libs/horizon/src/renderer/vnode/VNode.ts +++ b/libs/horizon/src/renderer/vnode/VNode.ts @@ -36,6 +36,7 @@ import type { VNodeTag } from './VNodeTags'; import type { RefType, ContextType, SuspenseState, Source } from '../Types'; import type { Hook } from '../hooks/HookType'; import { InitFlag } from './VNodeFlags'; +import { Observer } from '../../horizonx/proxy/Observer'; export class VNode { tag: VNodeTag; @@ -100,8 +101,8 @@ export class VNode { // 状态管理器HorizonX使用 isStoreChange: boolean; - observers: Set | null = null; // 记录这个函数组件/类组件依赖哪些Observer - classComponentWillUnmount: Function | null; // HorizonX会在classComponentWillUnmount中清除对VNode的引入用 + observers: Set | null = null; // 记录这个函数组件/类组件依赖哪些Observer + classComponentWillUnmount: ((vNode: VNode) => any) | null; // HorizonX会在classComponentWillUnmount中清除对VNode的引入用 src: Source | null; // 节点所在代码位置 constructor(tag: VNodeTag, props: any, key: null | string, realNode) { diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx index 9ad65d8a..b05b8fdd 100644 --- a/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/async.test.tsx @@ -31,7 +31,7 @@ function postpone(timer, func) { } describe('Asynchronous store', () => { - const useAsyncCounter = createStore({ + const getStore = createStore({ state: { counter: 0, check: false, @@ -61,13 +61,13 @@ describe('Asynchronous store', () => { }); beforeEach(() => { - useAsyncCounter().reset(); + getStore().reset(); }); it('should return promise when queued function is called', () => { jest.useFakeTimers(); - const store = useAsyncCounter(); + const store = getStore(); return new Promise(resolve => { store.$queue.increment().then(() => { @@ -82,9 +82,9 @@ describe('Asynchronous store', () => { it('should queue async functions', () => { jest.useFakeTimers(); return new Promise(resolve => { - const store = useAsyncCounter(); + const store = getStore(); - //initial value + // initial value expect(store.value).toBe('false0'); // no blocking action action