From f37a70552a1020aae89a496d3a5545bb4cf415e8 Mon Sep 17 00:00:00 2001 From: * <*> Date: Fri, 18 Nov 2022 22:13:06 +0800 Subject: [PATCH] Match-id-a610f3bb353c9901f46e8f676e3d6b2092a862b6 --- libs/horizon/src/horizonx/CommonUtils.ts | 91 ++++++++++++++++++- libs/horizon/src/horizonx/devtools/index.ts | 1 + .../src/horizonx/proxy/HooklessObserver.ts | 14 +-- libs/horizon/src/horizonx/proxy/Observer.ts | 22 ++--- .../proxy/handlers/ArrayProxyHandler.ts | 9 +- .../proxy/handlers/CollectionProxyHandler.ts | 17 ++-- .../proxy/handlers/ObjectProxyHandler.ts | 11 +-- .../src/horizonx/store/StoreHandler.ts | 3 +- libs/horizon/src/horizonx/types.d.ts | 26 +++--- .../StoreFunctionality/utils.test.js | 80 ++++++++++++++++ 10 files changed, 221 insertions(+), 53 deletions(-) create mode 100644 scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js diff --git a/libs/horizon/src/horizonx/CommonUtils.ts b/libs/horizon/src/horizonx/CommonUtils.ts index e799c963..e6387b6a 100644 --- a/libs/horizon/src/horizonx/CommonUtils.ts +++ b/libs/horizon/src/horizonx/CommonUtils.ts @@ -19,19 +19,31 @@ export function isObject(obj: any): boolean { } export function isSet(obj: any): boolean { - return (obj !== null || obj !== undefined) && (Object.prototype.toString.call(obj) === '[object Set]' || obj.constructor === Set); + 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); + 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); + 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); + return ( + (obj !== null || obj !== undefined) && + (Object.prototype.toString.call(obj) === '[object WeakMap]' || obj.constructor === WeakMap) + ); } export function isArray(obj: any): boolean { @@ -68,3 +80,74 @@ export function isSame(x, y) { return Object.is(x, y); } } + +export function getDetailedType(val: any) { + if (val === undefined) return 'undefined'; + if (val === null) return 'null'; + if (isCollection(val)) return 'collection'; + if (isPromise(val)) return 'promise'; + if (isArray(val)) return 'array'; + if (isWeakMap(val)) return 'weakMap'; + if (isMap(val)) return 'map'; + if (isWeakSet(val)) return 'weakSet'; + if (isSet(val)) return 'set'; + return typeof val; +} + +export function resolveMutation(from, to) { + if (getDetailedType(from) !== getDetailedType(to)) { + return { mutation: true, from, to }; + } + + switch (getDetailedType(from)) { + case 'array': { + let len = Math.max(from.length, to.length); + const res: any[] = []; + let found = false; + for (let i = 0; i < len; i++) { + if (from.length <= i) { + res[i] = { mutation: true, to: to[i] }; + found = true; + } else if (to.length <= i) { + res[i] = { mutation: true, from: from[i] }; + found = true; + } else { + res[i] = resolveMutation(from[i], to[i]); + if (res[i].mutation) found = true; + } + } + // TODO: resolve shifts + return { mutation: found, items: res, from, to }; + } + + case 'object': { + let keys = Object.keys({ ...from, ...to }); + const res = {}; + let found = false; + keys.forEach(key => { + if (!(key in from)) { + res[key] = { mutation: true, to: to[key] }; + found = true; + return; + } + + if (!(key in to)) { + res[key] = { mutation: true, from: from[key] }; + found = true; + return; + } + res[key] = resolveMutation(from[key], to[key]); + if (res[key].mutation) found = true; + }); + return { mutation: found, attributes: res, from, to }; + } + + // TODO: implement collections + + default: { + if (from === to) return { mutation: false }; + + return { mutation: true, from, to }; + } + } +} diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 884daabf..5aecbff3 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -41,6 +41,7 @@ function makeProxySnapshot(obj) { export const devtools = { emit: (type, data) => { + if (!window['__HORIZON_DEV_HOOK__']) return; console.log('store snapshot:', makeStoreSnapshot({ type, data })); window.postMessage({ type: 'HORIZON_DEV_TOOLS', diff --git a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts index ba390a3a..dd5015e9 100644 --- a/libs/horizon/src/horizonx/proxy/HooklessObserver.ts +++ b/libs/horizon/src/horizonx/proxy/HooklessObserver.ts @@ -19,28 +19,28 @@ import type { IObserver } from './Observer'; * 一个对象(对象、数组、集合)对应一个Observer */ export class HooklessObserver implements IObserver { - listeners: (() => void)[] = []; + listeners: ((mutation) => void)[] = []; useProp(key: string | symbol): void {} - addListener(listener: () => void) { + addListener(listener: (mutation) => void) { this.listeners.push(listener); } - removeListener(listener: () => void) { + removeListener(listener: (mutation) => void) { this.listeners = this.listeners.filter(item => item != listener); } - setProp(key: string | symbol): void { - this.triggerChangeListeners(); + setProp(key: string | symbol, mutation: any): void { + this.triggerChangeListeners(mutation); } - triggerChangeListeners(): void { + triggerChangeListeners(mutation: any): void { this.listeners.forEach(listener => { if (!listener) { return; } - listener(); + listener(mutation); }); } diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index 0db792a0..2f3b15f8 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -24,9 +24,9 @@ export interface IObserver { removeListener: (listener: () => void) => void; - setProp: (key: string) => void; + setProp: (key: string, mutation: any) => void; - triggerChangeListeners: () => void; + triggerChangeListeners: (mutation: any) => void; triggerUpdate: (vNode: any) => void; @@ -43,9 +43,9 @@ export class Observer implements IObserver { keyVNodes = new Map(); - listeners: (() => void)[] = []; + listeners: ((mutation) => void)[] = []; - watchers = {} as { [key: string]: ((key: string, oldValue: any, newValue: any) => void)[] }; + watchers = {} as { [key: string]: ((key: string, oldValue: any, newValue: any, mutation: any) => void)[] }; // 对象的属性被使用时调用 useProp(key: string | symbol): void { @@ -76,7 +76,7 @@ export class Observer implements IObserver { } // 对象的属性被赋值时调用 - setProp(key: string | symbol): void { + setProp(key: string | symbol, mutation: any): void { const vNodes = this.keyVNodes.get(key); vNodes?.forEach((vNode: VNode) => { if (vNode.isStoreChange) { @@ -89,7 +89,7 @@ export class Observer implements IObserver { this.triggerUpdate(vNode); }); - this.triggerChangeListeners(); + this.triggerChangeListeners(mutation); } triggerUpdate(vNode: VNode): void { @@ -101,16 +101,16 @@ export class Observer implements IObserver { launchUpdateFromVNode(vNode); } - addListener(listener: () => void): void { + addListener(listener: (mutation) => void): void { this.listeners.push(listener); } - removeListener(listener: () => void): void { + removeListener(listener: (mutation) => void): void { this.listeners = this.listeners.filter(item => item != listener); } - triggerChangeListeners(): void { - this.listeners.forEach(listener => listener()); + triggerChangeListeners(mutation: any): void { + this.listeners.forEach(listener => listener(mutation)); } // 触发所有使用的props的VNode更新 @@ -118,7 +118,7 @@ export class Observer implements IObserver { const keyIt = this.keyVNodes.keys(); let keyItem = keyIt.next(); while (!keyItem.done) { - this.setProp(keyItem.value); + this.setProp(keyItem.value, {}); keyItem = keyIt.next(); } } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index 8279512f..672253f8 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -16,6 +16,7 @@ import { getObserver } from '../ProxyHandler'; import { isSame, isValidIntegerKey } from '../../CommonUtils'; import { get as objectGet } from './ObjectProxyHandler'; +import { resolveMutation } from '../../CommonUtils'; export function createArrayProxy(rawObj: any[]): any[] { const handle = { @@ -53,6 +54,8 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { const oldLength = rawObj.length; const newValue = value; + const oldArray = JSON.parse(JSON.stringify(rawObj)); + const ret = Reflect.set(rawObj, key, newValue, receiver); const newLength = rawObj.length; @@ -62,17 +65,17 @@ function set(rawObj: any[], key: string, value: any, receiver: any) { // 值不一样,触发监听器 if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue); + cb(key, oldValue, newValue, resolveMutation(oldArray, rawObj)); }); } // 触发属性变化 - observer.setProp(key); + observer.setProp(key, resolveMutation(oldValue, rawObj)); } if (oldLength !== newLength) { // 触发数组的大小变化 - observer.setProp('length'); + observer.setProp('length', resolveMutation(oldValue, rawObj)); } return ret; diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index dad28366..74fa57bc 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -15,6 +15,7 @@ import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { isMap, isWeakMap, isSame } from '../../CommonUtils'; +import { resolveMutation } from '../../CommonUtils'; const COLLECTION_CHANGE = '_collectionChange'; const handler = { @@ -91,17 +92,17 @@ function set( const observer = getObserver(rawObj); if (valChange || !rawObj.has(key)) { - observer.setProp(COLLECTION_CHANGE); + observer.setProp(COLLECTION_CHANGE, resolveMutation(oldValue, rawObj)); } if (valChange) { if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue); + cb(key, oldValue, newValue, resolveMutation(oldValue, rawObj)); }); } - observer.setProp(key); + observer.setProp(key, resolveMutation(oldValue, rawObj)); } return rawObj; @@ -109,12 +110,13 @@ function set( // Set的add方法 function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object { + const oldCollection = JSON.parse(JSON.stringify(rawObj)); if (!rawObj.has(value)) { rawObj.add(value); const observer = getObserver(rawObj); - observer.setProp(value); - observer.setProp(COLLECTION_CHANGE); + observer.setProp(value, resolveMutation(oldCollection, rawObj)); + observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj)); } return rawObj; @@ -138,12 +140,13 @@ function clear(rawObj: { size: number; clear: () => void }) { } function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) { + const oldCollection = JSON.parse(JSON.stringify(rawObj)); if (rawObj.has(key)) { rawObj.delete(key); const observer = getObserver(rawObj); - observer.setProp(key); - observer.setProp(COLLECTION_CHANGE); + observer.setProp(key, resolveMutation(oldCollection, rawObj)); + observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj)); return true; } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index b2eba435..9e0aac15 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -13,7 +13,7 @@ * See the Mulan PSL v2 for more details. */ -import { isSame } from '../../CommonUtils'; +import { isSame, resolveMutation } from '../../CommonUtils'; import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { OBSERVER_KEY } from '../../Constants'; @@ -70,8 +70,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL } export function set(rawObj: object, key: string, value: any, receiver: any): boolean { - console.log('ObjectProxyHandler.set()'); - const oldObject = JSON.stringify(rawObj); + const oldObject = JSON.parse(JSON.stringify(rawObj)); const observer = getObserver(rawObj); if (value && key == 'removeListener') { @@ -85,12 +84,10 @@ export function set(rawObj: object, key: string, value: any, receiver: any): boo if (!isSame(newValue, oldValue)) { if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue); + cb(key, oldValue, newValue, resolveMutation(oldObject, rawObj)); }); } - observer.setProp(key); + observer.setProp(key, resolveMutation(oldObject, rawObj)); } - - console.log('mutation from: ', JSON.parse(oldObject), ' to: ', ret); return ret; } diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index dec1a61a..42db626d 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -191,9 +191,10 @@ export function createStore, C extend store: storeObj, }); - storeObj.$subscribe(() => { + proxyObj.addListener(mutation => { devtools.emit(STATE_CHANGE, { store: storeObj, + mutation, }); }); diff --git a/libs/horizon/src/horizonx/types.d.ts b/libs/horizon/src/horizonx/types.d.ts index f1a88e56..eec4a826 100644 --- a/libs/horizon/src/horizonx/types.d.ts +++ b/libs/horizon/src/horizonx/types.d.ts @@ -16,13 +16,13 @@ export interface IObserver { useProp: (key: string | symbol) => void; - addListener: (listener: () => void) => void; + addListener: (listener: (mutation: any) => void) => void; - removeListener: (listener: () => void) => void; + removeListener: (listener: (mutation: any) => void) => void; - setProp: (key: string | symbol) => void; + setProp: (key: string | symbol, mutation: any) => void; - triggerChangeListeners: () => void; + triggerChangeListeners: (mutation: any) => void; triggerUpdate: (vNode: any) => void; @@ -42,13 +42,13 @@ export type StoreConfig, C extends Us }; export type UserActions = { - [K: string]: ActionFunction + [K: string]: ActionFunction; }; type ActionFunction = (this: StoreObj, state: S, ...args: any[]) => any; export type StoreActions> = { - [K in keyof A]: Action + [K in keyof A]: Action; }; type Action, S extends object> = ( @@ -61,8 +61,8 @@ export type StoreObj, C extends UserC $a: StoreActions; $c: UserComputedValues; $queue: QueuedStoreActions; - $subscribe: (listener: () => void) => void; - $unsubscribe: (listener: () => void) => void; + $subscribe: (listener: (mutation) => void) => void; + $unsubscribe: (listener: (mutation) => void) => void; } & { [K in keyof S]: S[K] } & { [K in keyof A]: Action } & { [K in keyof C]: ReturnType }; export type PlannedAction> = { @@ -74,11 +74,11 @@ export type PlannedAction> = { type RemoveFirstFromTuple = T['length'] extends 0 ? [] : ((...b: T) => void) extends (a, ...b: infer I) => void - ? I - : []; + ? I + : []; export type UserComputedValues = { - [K: string]: ComputedFunction + [K: string]: ComputedFunction; }; type ComputedFunction = (state: S) => any; @@ -89,9 +89,9 @@ export type AsyncAction, S extends object> = ( ) => Promise>; export type QueuedStoreActions> = { - [K in keyof A]: AsyncAction + [K in keyof A]: AsyncAction; }; export type ComputedValues> = { - [K in keyof C]: ReturnType + [K in keyof C]: ReturnType; }; diff --git a/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js b/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js new file mode 100644 index 00000000..7d1f786f --- /dev/null +++ b/scripts/__tests__/HorizonXText/StoreFunctionality/utils.test.js @@ -0,0 +1,80 @@ +import { resolveMutation } from '../../../../libs/horizon/src/horizonx/CommonUtils'; + +describe('Mutation resolve', () => { + it('should resolve mutation different types', () => { + const mutation = resolveMutation(null, 42); + + expect(mutation.mutation).toBe(true); + expect(mutation.from).toBe(null); + expect(mutation.to).toBe(42); + }); + + it('should resolve mutation same type types, different values', () => { + const mutation = resolveMutation(13, 42); + + expect(mutation.mutation).toBe(true); + expect(mutation.from).toBe(13); + expect(mutation.to).toBe(42); + }); + + it('should resolve mutation same type types, same values', () => { + const mutation = resolveMutation(42, 42); + + expect(mutation.mutation).toBe(false); + expect(Object.keys(mutation).length).toBe(1); + }); + + it('should resolve mutation same type types, same objects', () => { + const mutation = resolveMutation({ a: { b: { c: 1 } } }, { a: { b: { c: 1 } } }); + + expect(mutation.mutation).toBe(false); + }); + + it('should resolve mutation same type types, same array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4, 5]); + + expect(mutation.mutation).toBe(false); + }); + + it('should resolve mutation same type types, longer array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6]); + + expect(mutation.mutation).toBe(true); + expect(mutation.items[5].mutation).toBe(true); + expect(mutation.items[5].to).toBe(6); + }); + + it('should resolve mutation same type types, shorter array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4]); + + expect(mutation.mutation).toBe(true); + expect(mutation.items[4].mutation).toBe(true); + expect(mutation.items[4].from).toBe(5); + }); + + it('should resolve mutation same type types, changed array', () => { + const mutation = resolveMutation([1, 2, 3, 4, 5], [1, 2, 3, 4, 'a']); + + expect(mutation.mutation).toBe(true); + expect(mutation.items[4].mutation).toBe(true); + expect(mutation.items[4].from).toBe(5); + expect(mutation.items[4].to).toBe('a'); + }); + + it('should resolve mutation same type types, same object', () => { + const mutation = resolveMutation({ a: 1, b: 2 }, { a: 1, b: 2 }); + + console.log(mutation); + expect(mutation.mutation).toBe(false); + }); + + it('should resolve mutation same type types, changed object', () => { + const mutation = resolveMutation({ a: 1, b: 2, c: 3 }, { a: 1, c: 2 }); + + expect(mutation.mutation).toBe(true); + expect(mutation.attributes.a.mutation).toBe(false); + expect(mutation.attributes.b.mutation).toBe(true); + expect(mutation.attributes.b.from).toBe(2); + expect(mutation.attributes.c.to).toBe(2); + }); +});