diff --git a/libs/horizon/src/horizonx/CommonUtils.js b/libs/horizon/src/horizonx/CommonUtils.js deleted file mode 100644 index 13f2c73f..00000000 --- a/libs/horizon/src/horizonx/CommonUtils.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2020 Huawei Technologies Co.,Ltd. - * - * openGauss is licensed under Mulan PSL v2. - * You can use this software according to the terms and conditions of the Mulan PSL v2. - * You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, - * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * See the Mulan PSL v2 for more details. - */ - -export function isObject(obj) { - const type = typeof obj; - return obj != null && (type === 'object' || type === 'function'); -} - -export function isSet(obj) { - return obj != null && (Object.prototype.toString.call(obj) === '[object Set]' || obj.constructor === Set); -} - -export function isWeakSet(obj) { - return obj != null && (Object.prototype.toString.call(obj) === '[object WeakSet]' || obj.constructor === WeakSet); -} - -export function isMap(obj) { - return obj != null && (Object.prototype.toString.call(obj) === '[object Map]' || obj.constructor === Map); -} - -export function isWeakMap(obj) { - return obj != null && (Object.prototype.toString.call(obj) === '[object WeakMap]' || obj.constructor === WeakMap); -} - -export function isArray(obj) { - return Object.prototype.toString.call(obj) === '[object Array]'; -} - -export function isCollection(obj) { - return isSet(obj) || isWeakSet(obj) || isMap(obj) || isWeakMap(obj); -} - -export function isString(obj) { - return typeof obj === 'string'; -} - -export function isValidIntegerKey(key) { - return isString(key) && key !== 'NaN' && key[0] !== '-' && String(parseInt(key, 10)) === key; -} - -export const noop = () => {}; - -export function isSame(x, y) { - if (!(typeof Object.is === 'function')) { - if (x === y) { - // +0 != -0 - return x !== 0 || 1 / x === 1 / y; - } else { - // NaN == NaN - return x !== x && y !== y; - } - } else { - return Object.is(x, y); - } -} diff --git a/libs/horizon/src/horizonx/CommonUtils.ts b/libs/horizon/src/horizonx/CommonUtils.ts new file mode 100644 index 00000000..e799c963 --- /dev/null +++ b/libs/horizon/src/horizonx/CommonUtils.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2020 Huawei Technologies Co.,Ltd. + * + * openGauss is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +export function isObject(obj: any): boolean { + const type = typeof obj; + return (obj !== null || obj !== undefined) && (type === 'object' || type === 'function'); +} + +export function isSet(obj: any): boolean { + return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object Set]' || obj.constructor === Set); +} + +export function isWeakSet(obj: any): boolean { + return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object WeakSet]' || obj.constructor === WeakSet); +} + +export function isMap(obj: any): boolean { + return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object Map]' || obj.constructor === Map); +} + +export function isWeakMap(obj: any): boolean { + return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object WeakMap]' || obj.constructor === WeakMap); +} + +export function isArray(obj: any): boolean { + return Object.prototype.toString.call(obj) === '[object Array]'; +} + +export function isCollection(obj: any): boolean { + return isSet(obj) || isWeakSet(obj) || isMap(obj) || isWeakMap(obj); +} + +export function isString(obj: any): boolean { + return typeof obj === 'string'; +} + +// key是有效的正整数字的字符串 +export function isValidIntegerKey(key: any): boolean { + return isString(key) && key !== 'NaN' && key[0] !== '-' && String(parseInt(key, 10)) === key; +} + +export function isPromise(obj: any): boolean { + return isObject(obj) && typeof obj.then === 'function'; +} + +export function isSame(x, y) { + if (!(typeof Object.is === 'function')) { + if (x === y) { + // +0 != -0 + return x !== 0 || 1 / x === 1 / y; + } else { + // NaN == NaN + return x !== x && y !== y; + } + } else { + return Object.is(x, y); + } +} diff --git a/libs/horizon/src/horizonx/Constants.ts b/libs/horizon/src/horizonx/Constants.ts index 0eb3a9a7..6d65ab52 100644 --- a/libs/horizon/src/horizonx/Constants.ts +++ b/libs/horizon/src/horizonx/Constants.ts @@ -13,8 +13,4 @@ * See the Mulan PSL v2 for more details. */ -// The two constants must be the same as those in horizon. -export const FunctionComponent = 'FunctionComponent'; -export const ClassComponent = 'ClassComponent'; - export const OBSERVER_KEY = '_horizonObserver'; diff --git a/libs/horizon/src/horizonx/adapters/redux.ts b/libs/horizon/src/horizonx/adapters/redux.ts index 359981ef..d84acbf0 100644 --- a/libs/horizon/src/horizonx/adapters/redux.ts +++ b/libs/horizon/src/horizonx/adapters/redux.ts @@ -15,8 +15,6 @@ import { createStore as createStoreX } from '../store/StoreHandler'; -import { ReduxStoreHandler } from '../store/StoreHandler'; - export { thunk } from './reduxThunk'; export { @@ -29,6 +27,14 @@ export { createDispatchHook, } from './reduxReact'; +export type ReduxStoreHandler = { + reducer: (state: any, action: { type: string }) => any; + dispatch: (action: { type: string }) => void; + getState: () => any; + subscribe: (listener: () => void) => () => void; + replaceReducer: (reducer: (state: any, action: { type: string }) => any) => void; +}; + export type ReduxAction = { type: string; [key: string]: any; diff --git a/libs/horizon/src/horizonx/adapters/reduxReact.ts b/libs/horizon/src/horizonx/adapters/reduxReact.ts index 615c5d9b..c804c469 100644 --- a/libs/horizon/src/horizonx/adapters/reduxReact.ts +++ b/libs/horizon/src/horizonx/adapters/reduxReact.ts @@ -17,9 +17,7 @@ import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/HookExternal'; import { createContext } from '../../renderer/components/context/CreateContext'; import { createElement } from '../../external/JSXElement'; -import { BoundActionCreator } from './redux'; -import { ReduxAction } from './redux'; -import { ReduxStoreHandler } from '../store/StoreHandler'; +import type { ReduxStoreHandler, ReduxAction, BoundActionCreator } from './redux'; const DefaultContext = createContext(null); type Context = typeof DefaultContext; diff --git a/libs/horizon/src/horizonx/adapters/reduxThunk.ts b/libs/horizon/src/horizonx/adapters/reduxThunk.ts index 70dfe622..36e25af0 100644 --- a/libs/horizon/src/horizonx/adapters/reduxThunk.ts +++ b/libs/horizon/src/horizonx/adapters/reduxThunk.ts @@ -13,8 +13,7 @@ * See the Mulan PSL v2 for more details. */ -import { ReduxAction, ReduxMiddleware } from './redux'; -import { ReduxStoreHandler } from '../store/StoreHandler'; +import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from './redux'; function createThunkMiddleware(extraArgument?: any): ReduxMiddleware { return (store: ReduxStoreHandler) => diff --git a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts index 063fba9f..1f2bf922 100644 --- a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts +++ b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts @@ -17,7 +17,6 @@ import { IObserver } from './Observer'; /** * 一个对象(对象、数组、集合)对应一个Observer - * */ export class HooklessObserver implements IObserver { listeners: (() => void)[] = []; diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index 950c16c6..73d4a2cc 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -13,14 +13,10 @@ * See the Mulan PSL v2 for more details. */ -/** - * 一个对象(对象、数组、集合)对应一个Observer - */ - -//@ts-ignore import { launchUpdateFromVNode } from '../../renderer/TreeBuilder'; import { getProcessingVNode } from '../../renderer/GlobalVar'; import { VNode } from '../../renderer/vnode/VNode'; + export interface IObserver { useProp: (key: string) => void; @@ -39,6 +35,9 @@ export interface IObserver { clearByVNode: (vNode: any) => void; } +/** + * 一个对象(对象、数组、集合)对应一个Observer + */ export class Observer implements IObserver { vNodeKeys = new WeakMap(); @@ -48,16 +47,18 @@ export class Observer implements IObserver { watchers = {} as { [key: string]: ((key: string, oldValue: any, newValue: any) => void)[] }; + // 对象的属性被使用时调用 useProp(key: string | symbol): void { const processingVNode = getProcessingVNode(); if (processingVNode === null || !processingVNode.observers) { + // 异常场景 return; } // vNode -> Observers processingVNode.observers.add(this); - // key -> vNodes + // key -> vNodes,记录这个prop被哪些VNode使用了 let vNodes = this.keyVNodes.get(key); if (!vNodes) { vNodes = new Set(); @@ -65,7 +66,7 @@ export class Observer implements IObserver { } vNodes.add(processingVNode); - // vNode -> keys + // vNode -> keys,记录这个VNode使用了哪些props let keys = this.vNodeKeys.get(processingVNode); if (!keys) { keys = new Set(); @@ -74,6 +75,32 @@ export class Observer implements IObserver { keys.add(key); } + // 对象的属性被赋值时调用 + setProp(key: string | symbol): void { + const vNodes = this.keyVNodes.get(key); + vNodes?.forEach((vNode: VNode) => { + if (vNode.isStoreChange) { + // VNode已经被触发过,不再重复触发 + return; + } + vNode.isStoreChange = true; + + // 触发vNode更新 + this.triggerUpdate(vNode); + }); + + this.triggerChangeListeners(); + } + + triggerUpdate(vNode: VNode): void { + if (!vNode) { + return; + } + + // 触发VNode更新 + launchUpdateFromVNode(vNode); + } + addListener(listener: () => void): void { this.listeners.push(listener); } @@ -82,32 +109,11 @@ export class Observer implements IObserver { this.listeners = this.listeners.filter(item => item != listener); } - setProp(key: string | symbol): void { - const vNodes = this.keyVNodes.get(key); - vNodes?.forEach((vNode: VNode) => { - if (vNode.isStoreChange) { - // update already triggered - return; - } - vNode.isStoreChange = true; - - // 触发vNode更新 - this.triggerUpdate(vNode); - }); - this.triggerChangeListeners(); - } - triggerChangeListeners(): void { this.listeners.forEach(listener => listener()); } - triggerUpdate(vNode: VNode): void { - if (!vNode) { - return; - } - launchUpdateFromVNode(vNode); - } - + // 触发所有使用的props的VNode更新 allChange(): void { const keyIt = this.keyVNodes.keys(); let keyItem = keyIt.next(); @@ -117,6 +123,7 @@ export class Observer implements IObserver { } } + // 删除keyVNodes中保存的这个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 0004aae9..9dc22b16 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -22,16 +22,18 @@ import { createCollectionProxy } from './handlers/CollectionProxyHandler'; import { IObserver } from '../types'; import { OBSERVER_KEY } from '../Constants'; +// 保存rawObj -> Proxy const proxyMap = new WeakMap(); export const hookObserverMap = new WeakMap(); -export function createProxy(rawObj: any, hookObserver = true): any { +export function createProxy(rawObj: any, isHookObserver = true): any { // 不是对象(是原始数据类型)不用代理 if (!isObject(rawObj)) { return rawObj; } + // 已代理过 const existProxy = proxyMap.get(rawObj); if (existProxy) { return existProxy; @@ -45,15 +47,15 @@ export function createProxy(rawObj: any, hookObserver = true): any { // 创建Observer let observer: IObserver = getObserver(rawObj); if (!observer) { - observer = hookObserver ? new Observer() : new HooklessObserver(); + observer = isHookObserver ? new Observer() : new HooklessObserver(); rawObj[OBSERVER_KEY] = observer; } - hookObserverMap.set(rawObj, hookObserver); + hookObserverMap.set(rawObj, isHookObserver); // 创建Proxy let proxyObj; - if (!hookObserver) { + if (!isHookObserver) { proxyObj = createObjectProxy(rawObj, true); } else if (isArray(rawObj)) { // 数组 diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index 745e35f3..8279512f 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -44,6 +44,7 @@ function get(rawObj: any[], key: string, receiver: any) { if (isValidIntegerKey(key) || key === 'length') { return objectGet(rawObj, key, receiver); } + return Reflect.get(rawObj, key, receiver); } @@ -58,16 +59,19 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const observer = getObserver(rawObj); if (!isSame(newValue, oldValue)) { + // 值不一样,触发监听器 if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { cb(key, oldValue, newValue); }); } + // 触发属性变化 observer.setProp(key); } if (oldLength !== newLength) { + // 触发数组的大小变化 observer.setProp('length'); } diff --git a/libs/horizon/src/horizonx/proxy/readonlyProxy.ts b/libs/horizon/src/horizonx/proxy/readonlyProxy.ts index f2853fed..24575067 100644 --- a/libs/horizon/src/horizonx/proxy/readonlyProxy.ts +++ b/libs/horizon/src/horizonx/proxy/readonlyProxy.ts @@ -15,10 +15,11 @@ import { isObject } from '../CommonUtils'; -export function readonlyProxy(target: T): ProxyHandler { - return new Proxy(target, { - get(target, property, receiver) { - const result = Reflect.get(target, property, receiver); +export function readonlyProxy(rawObj: T): ProxyHandler { + return new Proxy(rawObj, { + get(rawObj, property, receiver) { + const result = Reflect.get(rawObj, property, receiver); + try { if (isObject(result)) { return readonlyProxy(result); diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index 6c1cac0a..30ba7a89 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -21,35 +21,33 @@ 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'; const storeMap = new Map>(); -function isPromise(obj: any): boolean { - return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; -} - type StoreConfig, C extends UserComputedValues> = { + id?: string; state?: S; actions?: A; - id?: string; computed?: C; + options?: any; }; -export type ReduxStoreHandler = { - reducer: (state: any, action: { type: string }) => any; - dispatch: (action: { type: string }) => void; - getState: () => any; - subscribe: (listener: () => void) => () => void; - replaceReducer: (reducer: (state: any, action: { type: string }) => any) => void; -}; +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> = { - $subscribe: (listener: () => void) => void; - $unsubscribe: (listener: () => void) => void; $s: S; - $queue: QueuedStoreActions; $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> = { @@ -63,41 +61,35 @@ type RemoveFirstFromTuple = T['length'] extends 0 ? I : []; -type UserActions = { [K: string]: ActionFunction }; type UserComputedValues = { [K: string]: ComputedFunction }; -type ActionFunction = (this: StoreHandler, state: S, ...args: any[]) => any; type ComputedFunction = (state: S) => any; -type Action, S extends object> = ( - this: StoreHandler, - ...args: RemoveFirstFromTuple> -) => ReturnType; + type AsyncAction, S extends object> = ( this: StoreHandler, ...args: RemoveFirstFromTuple> ) => Promise>; -type StoreActions> = { [K in keyof A]: Action }; type QueuedStoreActions> = { [K in keyof A]: AsyncAction }; type ComputedValues> = { [K in keyof C]: ReturnType }; export function createStore, C extends UserComputedValues>( - config: StoreConfig + storeConfig: StoreConfig ): () => StoreHandler { - // create a local shalow copy to ensure consistency (if user would change the config object after store creation) - config = { - id: config.id, - options: config.options, - state: config.state, - actions: config.actions ? { ...config.actions } : undefined, - computed: config.computed ? { ...config.computed } : undefined, - }; - // 校验 - if (Object.prototype.toString.call(config) !== '[object Object]') { + if (Object.prototype.toString.call(storeConfig) !== '[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); proxyObj.$pending = false; @@ -114,14 +106,14 @@ export function createStore, C extend const $a: Partial> = {}; const $queue: Partial> = {}; const $c: Partial> = {}; - const handler = { + const storeHandler = { + $s: proxyObj, + $a: $a as StoreActions, + $c: $c as ComputedValues, + $queue: $queue as QueuedStoreActions, + $config: config, $subscribe, $unsubscribe, - $a: $a as StoreActions, - $s: proxyObj, - $c: $c as ComputedValues, - $config: config, - $queue: $queue as QueuedStoreActions, } as unknown as StoreHandler; function tryNextAction() { @@ -132,7 +124,7 @@ export function createStore, C extend const nextAction = plannedActions.shift()!; const result = config.actions - ? config.actions[nextAction.action].bind(handler, proxyObj)(...nextAction.payload) + ? config.actions[nextAction.action].bind(storeHandler, proxyObj)(...nextAction.payload) : undefined; if (isPromise(result)) { @@ -153,7 +145,7 @@ export function createStore, C extend return new Promise(resolve => { if (!proxyObj.$pending) { proxyObj.$pending = true; - const result = config.actions![action].bind(handler, proxyObj)(...payload); + const result = config.actions![action].bind(storeHandler, proxyObj)(...payload); if (isPromise(result)) { result.then(value => { @@ -174,45 +166,50 @@ export function createStore, C extend }); }; + // 让store.$a[action]可以访问到action方法 ($a as any)[action] = function Wrapped(...payload) { - return config.actions![action].bind(handler, proxyObj)(...payload); + return config.actions![action].bind(storeHandler, proxyObj)(...payload); }; - // direct store access - Object.defineProperty(handler, action, { + // 让store[action]可以访问到action方法 + Object.defineProperty(storeHandler, action, { writable: false, value: (...payload) => { - return config.actions![action].bind(handler, proxyObj)(...payload); + return config.actions![action].bind(storeHandler, proxyObj)(...payload); }, }); }); } if (config.computed) { - Object.keys(config.computed).forEach(key => { - ($c as any)[key] = config.computed![key].bind(handler, readonlyProxy(proxyObj)); + Object.keys(config.computed).forEach(computeKey => { + // 让store.$c[computeKey]可以访问到computed方法 + ($c as any)[computeKey] = config.computed![computeKey].bind(storeHandler, readonlyProxy(proxyObj)); - // direct store access - Object.defineProperty(handler, key, { - get: $c[key] as () => any, + // 让store[computeKey]可以访问到computed的值 + Object.defineProperty(storeHandler, computeKey, { + get: $c[computeKey] as () => any, }); }); } - // direct state access + // 让store[key]可以访问到state的值 if (config.state) { Object.keys(config.state).forEach(key => { - Object.defineProperty(handler, key, { - get: () => proxyObj[key], + Object.defineProperty(storeHandler, key, { + get: () => { + // 从Proxy对象获取值,会触发代理 + return proxyObj[key]; + }, }); }); } if (config.id) { - storeMap.set(config.id, handler); + storeMap.set(config.id, storeHandler); } - return createStoreHook(handler); + return createStoreHook(storeHandler); } export function clearVNodeObservers(vNode) { @@ -238,8 +235,7 @@ function hookStore() { if (processingVNode.tag === FunctionComponent) { // from FunctionComponent - const vNodeRef = useRef(null) as unknown as { current: VNode }; - vNodeRef.current = processingVNode; + const vNodeRef = useRef(processingVNode); useEffect(() => { return () => { diff --git a/libs/horizon/src/renderer/hooks/HookType.ts b/libs/horizon/src/renderer/hooks/HookType.ts index 9377c456..fddf5223 100644 --- a/libs/horizon/src/renderer/hooks/HookType.ts +++ b/libs/horizon/src/renderer/hooks/HookType.ts @@ -54,7 +54,7 @@ export type CallBack = { }; export type Ref = { - current: V | null; + current: V; }; export type Trigger = (A) => void;