diff --git a/.eslintrc.js b/.eslintrc.js index f0692e11..3f20b668 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -45,6 +45,7 @@ module.exports = { }, globals: { isDev: true, + isTest: true, }, overrides: [ { diff --git a/libs/horizon/global.d.ts b/libs/horizon/global.d.ts index 0ebd7135..a3777f70 100644 --- a/libs/horizon/global.d.ts +++ b/libs/horizon/global.d.ts @@ -2,4 +2,5 @@ 区分是否开发者模式 */ declare var isDev: boolean; +declare var isTest: boolean; declare const __VERSION__: string; diff --git a/libs/horizon/src/horizonx/Constants.ts b/libs/horizon/src/horizonx/Constants.ts index 5967d853..334f0ef6 100644 --- a/libs/horizon/src/horizonx/Constants.ts +++ b/libs/horizon/src/horizonx/Constants.ts @@ -1,3 +1,5 @@ // The two constants must be the same as those in horizon. export const FunctionComponent = 'FunctionComponent'; export const ClassComponent = 'ClassComponent'; + +export const OBSERVER_KEY = Symbol('_horizonObserver'); diff --git a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts index 9ea8937f..10fce509 100644 --- a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts +++ b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts @@ -10,7 +10,7 @@ export class HooklessObserver implements IObserver { listeners:(() => void)[] = []; - useProp(key: string): void { + useProp(key: string | symbol): void { } addListener(listener: () => void) { @@ -21,7 +21,7 @@ export class HooklessObserver implements IObserver { this.listeners = this.listeners.filter(item => item != listener); } - setProp(key: string): void { + setProp(key: string | symbol): void { this.triggerChangeListeners(); } diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index be24d1c2..1324c87a 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -16,7 +16,7 @@ export class Observer implements IObserver { listeners:(()=>void)[] = []; - useProp(key: string): void { + useProp(key: string | symbol): void { const processingVNode = getProcessingVNode(); if (processingVNode === null || !processingVNode.observers) { return; @@ -50,7 +50,7 @@ export class Observer implements IObserver { this.listeners = this.listeners.filter(item => item != listener); } - setProp(key: string): void { + setProp(key: string | symbol): void { const vNodes = this.keyVNodes.get(key); vNodes?.forEach((vNode: VNode) => { if (vNode.isStoreChange) { @@ -85,7 +85,7 @@ export class Observer implements IObserver { } } - clearByVNode(vNode: Vnode): void { + clearByVNode(vNode: VNode): void { const keys = this.vNodeKeys.get(vNode); if (keys) { keys.forEach((key: any) => { diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index f2051b9d..a540e199 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -1,12 +1,11 @@ -import {createObjectProxy} from './handlers/ObjectProxyHandler'; -import {Observer} from './Observer'; -import {HooklessObserver} from './HooklessObserver'; -import {isArray, isCollection, isObject} from '../CommonUtils'; -import {createArrayProxy} from './handlers/ArrayProxyHandler'; -import {createCollectionProxy} from './handlers/CollectionProxyHandler'; +import { createObjectProxy } from './handlers/ObjectProxyHandler'; +import { Observer } from './Observer'; +import { HooklessObserver } from './HooklessObserver'; +import { isArray, isCollection, isObject } from '../CommonUtils'; +import { createArrayProxy } from './handlers/ArrayProxyHandler'; +import { createCollectionProxy } from './handlers/CollectionProxyHandler'; import { IObserver } from '../types'; - -const OBSERVER_KEY = Symbol('_horizonObserver'); +import { OBSERVER_KEY } from '../Constants'; const proxyMap = new WeakMap(); @@ -29,7 +28,7 @@ export function createProxy(rawObj: any, hookObserver = true): any { } // 创建Observer - let observer:IObserver = getObserver(rawObj); + let observer: IObserver = getObserver(rawObj); if (!observer) { observer = hookObserver ? new Observer() : new HooklessObserver(); rawObj[OBSERVER_KEY] = observer; @@ -59,4 +58,3 @@ export function createProxy(rawObj: any, hookObserver = true): any { export function getObserver(rawObj: any): Observer { return rawObj[OBSERVER_KEY]; } - diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index 879dc088..a6812bde 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -26,14 +26,14 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const ret = Reflect.set(rawObj, key, newValue, receiver); const newLength = rawObj.length; - const tracker = getObserver(rawObj); + const observer = getObserver(rawObj); if (!isSame(newValue, oldValue)) { - tracker.setProp(key); + observer.setProp(key); } if (oldLength !== newLength) { - tracker.setProp('length'); + observer.setProp('length'); } return ret; diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index ec8214b3..bd044b44 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -40,8 +40,8 @@ function get(rawObj: { size: number }, key: any, receiver: any): any { } function getFun(rawObj: { get: (key: any) => any }, key: any) { - const tracker = getObserver(rawObj); - tracker.useProp(key); + const observer = getObserver(rawObj); + observer.useProp(key); const value = rawObj.get(key); // 对于value也需要进一步代理 @@ -60,14 +60,14 @@ function set( const newValue = value; rawObj.set(key, newValue); const valChange = !isSame(newValue, oldValue); - const tracker = getObserver(rawObj); + const observer = getObserver(rawObj); if (valChange || !rawObj.has(key)) { - tracker.setProp(COLLECTION_CHANGE); + observer.setProp(COLLECTION_CHANGE); } if (valChange) { - tracker.setProp(key); + observer.setProp(key); } return rawObj; @@ -78,17 +78,17 @@ function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) if (!rawObj.has(value)) { rawObj.add(value); - const tracker = getObserver(rawObj); - tracker.setProp(value); - tracker.setProp(COLLECTION_CHANGE); + const observer = getObserver(rawObj); + observer.setProp(value); + observer.setProp(COLLECTION_CHANGE); } return rawObj; } function has(rawObj: { has: (string) => boolean }, key: any): boolean { - const tracker = getObserver(rawObj); - tracker.useProp(key); + const observer = getObserver(rawObj); + observer.useProp(key); return rawObj.has(key); } @@ -98,8 +98,8 @@ function clear(rawObj: { size: number; clear: () => void }) { rawObj.clear(); if (oldSize > 0) { - const tracker = getObserver(rawObj); - tracker.allChange(); + const observer = getObserver(rawObj); + observer.allChange(); } } @@ -107,9 +107,9 @@ function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => v if (rawObj.has(key)) { rawObj.delete(key); - const tracker = getObserver(rawObj); - tracker.setProp(key); - tracker.setProp(COLLECTION_CHANGE); + const observer = getObserver(rawObj); + observer.setProp(key); + observer.setProp(COLLECTION_CHANGE); return true; } @@ -118,8 +118,8 @@ function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => v } function size(rawObj: { size: number }) { - const tracker = getObserver(rawObj); - tracker.useProp(COLLECTION_CHANGE); + const observer = getObserver(rawObj); + observer.useProp(COLLECTION_CHANGE); return rawObj.size; } @@ -148,8 +148,8 @@ function forEach( rawObj: { forEach: (callback: (value: any, key: any) => void) => void }, callback: (valProxy: any, keyProxy: any, rawObj: any) => void ) { - const tracker = getObserver(rawObj); - tracker.useProp(COLLECTION_CHANGE); + const observer = getObserver(rawObj); + observer.useProp(COLLECTION_CHANGE); rawObj.forEach((value, key) => { const valProxy = createProxy(value, hookObserverMap.get(rawObj)); const keyProxy = createProxy(key, hookObserverMap.get(rawObj)); @@ -159,9 +159,9 @@ function forEach( } function wrapIterator(rawObj: Object, rawIt: { next: () => { value: any; done: boolean } }, isPair = false) { - const tracker = getObserver(rawObj); + const observer = getObserver(rawObj); const hookObserver = hookObserverMap.get(rawObj); - tracker.useProp(COLLECTION_CHANGE); + observer.useProp(COLLECTION_CHANGE); return { next() { @@ -170,7 +170,7 @@ function wrapIterator(rawObj: Object, rawIt: { next: () => { value: any; done: b return { value: createProxy(value, hookObserver), done }; } - tracker.useProp(COLLECTION_CHANGE); + observer.useProp(COLLECTION_CHANGE); let newVal; if (isPair) { diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 709c68a9..28749a57 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -1,5 +1,6 @@ import { isSame } from '../../CommonUtils'; import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; +import { OBSERVER_KEY } from '../../Constants'; export function createObjectProxy(rawObj: T): ProxyHandler { const proxy = new Proxy(rawObj, { @@ -10,7 +11,12 @@ export function createObjectProxy(rawObj: T): ProxyHandler return proxy; } -export function get(rawObj: object, key: string, receiver: any): any { +export function get(rawObj: object, key: string | symbol, receiver: any): any { + // The observer object of symbol ('_horizonObserver') cannot be accessed from Proxy to prevent errors caused by clonedeep. + if (key === OBSERVER_KEY) { + return undefined; + } + const observer = getObserver(rawObj); if (key === 'addListener') { @@ -25,10 +31,15 @@ export function get(rawObj: object, key: string, receiver: any): any { const value = Reflect.get(rawObj, key, receiver); - // 对于value也需要进一步代理 - const valProxy = createProxy(value, hookObserverMap.get(rawObj)); + // 对于prototype不做代理 + if (key !== 'prototype') { + // 对于value也需要进一步代理 + const valProxy = createProxy(value, hookObserverMap.get(rawObj)); - return valProxy; + return valProxy; + } + + return value; } export function set(rawObj: object, key: string, value: any, receiver: any): boolean { diff --git a/libs/horizon/src/horizonx/types.d.ts b/libs/horizon/src/horizonx/types.d.ts index 3fed3afe..9b54c139 100644 --- a/libs/horizon/src/horizonx/types.d.ts +++ b/libs/horizon/src/horizonx/types.d.ts @@ -1,23 +1,23 @@ export interface IObserver { - useProp: (key: string) => void; + useProp: (key: string | symbol) => void; addListener: (listener: () => void) => void; removeListener: (listener: () => void) => void; - setProp: (key: string) => void; + setProp: (key: string | symbol) => void; triggerChangeListeners: () => void; triggerUpdate: (vNode: any) => void; allChange: () => void; - + clearByVNode: (vNode: any) => void; } -type RemoveFirstFromTuple = +type RemoveFirstFromTuple = T['length'] extends 0 ? [] : (((...b: T) => void) extends (a, ...b: infer I) => void ? I : []) @@ -36,7 +36,7 @@ type ComputedValues> = { [K in type PostponedAction = (state: object, ...args: any[]) => Promise; type PostponedActions = { [key:string]: PostponedAction } -export type StoreHandler,C extends UserComputedValues> = +export type StoreHandler,C extends UserComputedValues> = {$subscribe: ((listener: () => void) => void), $unsubscribe: ((listener: () => void) => void), $state: S, @@ -78,4 +78,4 @@ type ReduxMiddleware = (store:ReduxStoreHandler, extraArgument?:any) => (action:( ReduxAction| ((dispatch:(action:ReduxAction)=>void,store:ReduxStoreHandler,extraArgument?:any)=>any) - )) => ReduxStoreHandler \ No newline at end of file + )) => ReduxStoreHandler diff --git a/libs/horizon/src/renderer/ErrorHandler.ts b/libs/horizon/src/renderer/ErrorHandler.ts index 8eaed4f8..1f629d97 100644 --- a/libs/horizon/src/renderer/ErrorHandler.ts +++ b/libs/horizon/src/renderer/ErrorHandler.ts @@ -15,7 +15,7 @@ import {updateShouldUpdateOfTree} from './vnode/VNodeShouldUpdate'; import {BuildErrored, setBuildResult} from './GlobalVar'; function consoleError(error: any): void { - if (isDev) { + if (isTest) { // 只打印message为了让测试用例能pass console['error']('The codes throw the error: ' + error.message); } else { diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/cloneDeep.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/cloneDeep.test.js new file mode 100644 index 00000000..9f0dd806 --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/cloneDeep.test.js @@ -0,0 +1,106 @@ +import * as Horizon from '@cloudsop/horizon/index.ts'; +import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler'; +import { OBSERVER_KEY } from '../../../../libs/horizon/src/horizonx/Constants'; +import { App, Text, triggerClickEvent } from '../../jest/commonComponents'; + +describe('测试对store.state对象进行深度克隆', () => { + const { unmountComponentAtNode } = Horizon; + let container = null; + beforeEach(() => { + // 创建一个 DOM 元素作为渲染目标 + container = document.createElement('div'); + document.body.appendChild(container); + + createStore({ + id: 'user', + state: { + type: 'bing dun dun', + persons: [ + { name: 'p1', age: 1 }, + { name: 'p2', age: 2 }, + ], + }, + actions: { + addOnePerson: (state, person) => { + state.persons.push(person); + }, + delOnePerson: state => { + state.persons.pop(); + }, + clearPersons: state => { + state.persons = null; + }, + }, + }); + }); + + afterEach(() => { + // 退出时进行清理 + unmountComponentAtNode(container); + container.remove(); + container = null; + + clearStore('user'); + }); + + const newPerson = { name: 'p3', age: 3 }; + + function Parent({ children }) { + const userStore = useStore('user'); + const addOnePerson = function() { + userStore.addOnePerson(newPerson); + }; + const delOnePerson = function() { + userStore.delOnePerson(); + }; + return ( +
+ + +
{children}
+
+ ); + } + + it('The observer object of symbol (\'_horizonObserver\') cannot be accessed to from Proxy', () => { + let userStore = null; + function Child(props) { + userStore = useStore('user'); + + return ( +
+ +
+ ); + } + + Horizon.render(, container); + + // The observer object of symbol ('_horizonObserver') cannot be accessed to from Proxy prevent errors caused by clonedeep. + expect(userStore.persons[0][OBSERVER_KEY]).toBe(undefined); + }); + + it('The observer object of symbol (\'_horizonObserver\') cannot be accessed to from Proxy', () => { + let userStore = null; + function Child(props) { + userStore = useStore('user'); + + return ( +
+ +
+ ); + } + + Horizon.render(, container); + + // NO throw this Exception, TypeError: 'get' on proxy: property 'prototype' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value + const proxyObj = userStore.persons[0].constructor; + expect(proxyObj.prototype !== undefined).toBeTruthy(); + }); + +}); diff --git a/scripts/__tests__/jest/jestSetting.js b/scripts/__tests__/jest/jestSetting.js index 2111a635..a62c3372 100644 --- a/scripts/__tests__/jest/jestSetting.js +++ b/scripts/__tests__/jest/jestSetting.js @@ -5,6 +5,7 @@ import { getLogUtils } from './testUtils'; //failOnConsole(); const LogUtils = getLogUtils(); global.isDev = process.env.NODE_ENV === 'development'; +global.isTest = true; global.container = null; global.beforeEach(() => { LogUtils.clear();