Match-id-a610f3bb353c9901f46e8f676e3d6b2092a862b6

This commit is contained in:
* 2022-11-18 22:13:06 +08:00
parent ef3e42adbe
commit f37a70552a
10 changed files with 221 additions and 53 deletions

View File

@ -19,19 +19,31 @@ export function isObject(obj: any): boolean {
} }
export function isSet(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 { 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 { 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 { 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 { export function isArray(obj: any): boolean {
@ -68,3 +80,74 @@ export function isSame(x, y) {
return Object.is(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 };
}
}
}

View File

@ -41,6 +41,7 @@ function makeProxySnapshot(obj) {
export const devtools = { export const devtools = {
emit: (type, data) => { emit: (type, data) => {
if (!window['__HORIZON_DEV_HOOK__']) return;
console.log('store snapshot:', makeStoreSnapshot({ type, data })); console.log('store snapshot:', makeStoreSnapshot({ type, data }));
window.postMessage({ window.postMessage({
type: 'HORIZON_DEV_TOOLS', type: 'HORIZON_DEV_TOOLS',

View File

@ -19,28 +19,28 @@ import type { IObserver } from './Observer';
* Observer * Observer
*/ */
export class HooklessObserver implements IObserver { export class HooklessObserver implements IObserver {
listeners: (() => void)[] = []; listeners: ((mutation) => void)[] = [];
useProp(key: string | symbol): void {} useProp(key: string | symbol): void {}
addListener(listener: () => void) { addListener(listener: (mutation) => void) {
this.listeners.push(listener); this.listeners.push(listener);
} }
removeListener(listener: () => void) { removeListener(listener: (mutation) => void) {
this.listeners = this.listeners.filter(item => item != listener); this.listeners = this.listeners.filter(item => item != listener);
} }
setProp(key: string | symbol): void { setProp(key: string | symbol, mutation: any): void {
this.triggerChangeListeners(); this.triggerChangeListeners(mutation);
} }
triggerChangeListeners(): void { triggerChangeListeners(mutation: any): void {
this.listeners.forEach(listener => { this.listeners.forEach(listener => {
if (!listener) { if (!listener) {
return; return;
} }
listener(); listener(mutation);
}); });
} }

View File

@ -24,9 +24,9 @@ export interface IObserver {
removeListener: (listener: () => void) => void; removeListener: (listener: () => void) => void;
setProp: (key: string) => void; setProp: (key: string, mutation: any) => void;
triggerChangeListeners: () => void; triggerChangeListeners: (mutation: any) => void;
triggerUpdate: (vNode: any) => void; triggerUpdate: (vNode: any) => void;
@ -43,9 +43,9 @@ export class Observer implements IObserver {
keyVNodes = new Map(); 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 { 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); const vNodes = this.keyVNodes.get(key);
vNodes?.forEach((vNode: VNode) => { vNodes?.forEach((vNode: VNode) => {
if (vNode.isStoreChange) { if (vNode.isStoreChange) {
@ -89,7 +89,7 @@ export class Observer implements IObserver {
this.triggerUpdate(vNode); this.triggerUpdate(vNode);
}); });
this.triggerChangeListeners(); this.triggerChangeListeners(mutation);
} }
triggerUpdate(vNode: VNode): void { triggerUpdate(vNode: VNode): void {
@ -101,16 +101,16 @@ export class Observer implements IObserver {
launchUpdateFromVNode(vNode); launchUpdateFromVNode(vNode);
} }
addListener(listener: () => void): void { addListener(listener: (mutation) => void): void {
this.listeners.push(listener); this.listeners.push(listener);
} }
removeListener(listener: () => void): void { removeListener(listener: (mutation) => void): void {
this.listeners = this.listeners.filter(item => item != listener); this.listeners = this.listeners.filter(item => item != listener);
} }
triggerChangeListeners(): void { triggerChangeListeners(mutation: any): void {
this.listeners.forEach(listener => listener()); this.listeners.forEach(listener => listener(mutation));
} }
// 触发所有使用的props的VNode更新 // 触发所有使用的props的VNode更新
@ -118,7 +118,7 @@ export class Observer implements IObserver {
const keyIt = this.keyVNodes.keys(); const keyIt = this.keyVNodes.keys();
let keyItem = keyIt.next(); let keyItem = keyIt.next();
while (!keyItem.done) { while (!keyItem.done) {
this.setProp(keyItem.value); this.setProp(keyItem.value, {});
keyItem = keyIt.next(); keyItem = keyIt.next();
} }
} }

View File

@ -16,6 +16,7 @@
import { getObserver } from '../ProxyHandler'; import { getObserver } from '../ProxyHandler';
import { isSame, isValidIntegerKey } from '../../CommonUtils'; import { isSame, isValidIntegerKey } from '../../CommonUtils';
import { get as objectGet } from './ObjectProxyHandler'; import { get as objectGet } from './ObjectProxyHandler';
import { resolveMutation } from '../../CommonUtils';
export function createArrayProxy(rawObj: any[]): any[] { export function createArrayProxy(rawObj: any[]): any[] {
const handle = { const handle = {
@ -53,6 +54,8 @@ function set(rawObj: any[], key: string, value: any, receiver: any) {
const oldLength = rawObj.length; const oldLength = rawObj.length;
const newValue = value; const newValue = value;
const oldArray = JSON.parse(JSON.stringify(rawObj));
const ret = Reflect.set(rawObj, key, newValue, receiver); const ret = Reflect.set(rawObj, key, newValue, receiver);
const newLength = rawObj.length; const newLength = rawObj.length;
@ -62,17 +65,17 @@ function set(rawObj: any[], key: string, value: any, receiver: any) {
// 值不一样,触发监听器 // 值不一样,触发监听器
if (observer.watchers?.[key]) { if (observer.watchers?.[key]) {
observer.watchers[key].forEach(cb => { 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) { if (oldLength !== newLength) {
// 触发数组的大小变化 // 触发数组的大小变化
observer.setProp('length'); observer.setProp('length', resolveMutation(oldValue, rawObj));
} }
return ret; return ret;

View File

@ -15,6 +15,7 @@
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
import { isMap, isWeakMap, isSame } from '../../CommonUtils'; import { isMap, isWeakMap, isSame } from '../../CommonUtils';
import { resolveMutation } from '../../CommonUtils';
const COLLECTION_CHANGE = '_collectionChange'; const COLLECTION_CHANGE = '_collectionChange';
const handler = { const handler = {
@ -91,17 +92,17 @@ function set(
const observer = getObserver(rawObj); const observer = getObserver(rawObj);
if (valChange || !rawObj.has(key)) { if (valChange || !rawObj.has(key)) {
observer.setProp(COLLECTION_CHANGE); observer.setProp(COLLECTION_CHANGE, resolveMutation(oldValue, rawObj));
} }
if (valChange) { if (valChange) {
if (observer.watchers?.[key]) { if (observer.watchers?.[key]) {
observer.watchers[key].forEach(cb => { 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; return rawObj;
@ -109,12 +110,13 @@ function set(
// Set的add方法 // Set的add方法
function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object { 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)) { if (!rawObj.has(value)) {
rawObj.add(value); rawObj.add(value);
const observer = getObserver(rawObj); const observer = getObserver(rawObj);
observer.setProp(value); observer.setProp(value, resolveMutation(oldCollection, rawObj));
observer.setProp(COLLECTION_CHANGE); observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj));
} }
return 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) { function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) {
const oldCollection = JSON.parse(JSON.stringify(rawObj));
if (rawObj.has(key)) { if (rawObj.has(key)) {
rawObj.delete(key); rawObj.delete(key);
const observer = getObserver(rawObj); const observer = getObserver(rawObj);
observer.setProp(key); observer.setProp(key, resolveMutation(oldCollection, rawObj));
observer.setProp(COLLECTION_CHANGE); observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj));
return true; return true;
} }

View File

@ -13,7 +13,7 @@
* See the Mulan PSL v2 for more details. * See the Mulan PSL v2 for more details.
*/ */
import { isSame } from '../../CommonUtils'; import { isSame, resolveMutation } from '../../CommonUtils';
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
import { OBSERVER_KEY } from '../../Constants'; 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 { export function set(rawObj: object, key: string, value: any, receiver: any): boolean {
console.log('ObjectProxyHandler.set()'); const oldObject = JSON.parse(JSON.stringify(rawObj));
const oldObject = JSON.stringify(rawObj);
const observer = getObserver(rawObj); const observer = getObserver(rawObj);
if (value && key == 'removeListener') { 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 (!isSame(newValue, oldValue)) {
if (observer.watchers?.[key]) { if (observer.watchers?.[key]) {
observer.watchers[key].forEach(cb => { 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; return ret;
} }

View File

@ -191,9 +191,10 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
store: storeObj, store: storeObj,
}); });
storeObj.$subscribe(() => { proxyObj.addListener(mutation => {
devtools.emit(STATE_CHANGE, { devtools.emit(STATE_CHANGE, {
store: storeObj, store: storeObj,
mutation,
}); });
}); });

View File

@ -16,13 +16,13 @@
export interface IObserver { export interface IObserver {
useProp: (key: string | symbol) => void; 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; triggerUpdate: (vNode: any) => void;
@ -42,13 +42,13 @@ export type StoreConfig<S extends object, A extends UserActions<S>, C extends Us
}; };
export type UserActions<S extends object> = { export type UserActions<S extends object> = {
[K: string]: ActionFunction<S> [K: string]: ActionFunction<S>;
}; };
type ActionFunction<S extends object> = (this: StoreObj<S, any, any>, state: S, ...args: any[]) => any; type ActionFunction<S extends object> = (this: StoreObj<S, any, any>, state: S, ...args: any[]) => any;
export type StoreActions<S extends object, A extends UserActions<S>> = { export type StoreActions<S extends object, A extends UserActions<S>> = {
[K in keyof A]: Action<A[K], S> [K in keyof A]: Action<A[K], S>;
}; };
type Action<T extends ActionFunction<any>, S extends object> = ( type Action<T extends ActionFunction<any>, S extends object> = (
@ -61,8 +61,8 @@ export type StoreObj<S extends object, A extends UserActions<S>, C extends UserC
$a: StoreActions<S, A>; $a: StoreActions<S, A>;
$c: UserComputedValues<S>; $c: UserComputedValues<S>;
$queue: QueuedStoreActions<S, A>; $queue: QueuedStoreActions<S, A>;
$subscribe: (listener: () => void) => void; $subscribe: (listener: (mutation) => void) => void;
$unsubscribe: (listener: () => void) => void; $unsubscribe: (listener: (mutation) => void) => void;
} & { [K in keyof S]: S[K] } & { [K in keyof A]: Action<A[K], S> } & { [K in keyof C]: ReturnType<C[K]> }; } & { [K in keyof S]: S[K] } & { [K in keyof A]: Action<A[K], S> } & { [K in keyof C]: ReturnType<C[K]> };
export type PlannedAction<S extends object, F extends ActionFunction<S>> = { export type PlannedAction<S extends object, F extends ActionFunction<S>> = {
@ -74,11 +74,11 @@ export type PlannedAction<S extends object, F extends ActionFunction<S>> = {
type RemoveFirstFromTuple<T extends any[]> = T['length'] extends 0 type RemoveFirstFromTuple<T extends any[]> = T['length'] extends 0
? [] ? []
: ((...b: T) => void) extends (a, ...b: infer I) => void : ((...b: T) => void) extends (a, ...b: infer I) => void
? I ? I
: []; : [];
export type UserComputedValues<S extends object> = { export type UserComputedValues<S extends object> = {
[K: string]: ComputedFunction<S> [K: string]: ComputedFunction<S>;
}; };
type ComputedFunction<S extends object> = (state: S) => any; type ComputedFunction<S extends object> = (state: S) => any;
@ -89,9 +89,9 @@ export type AsyncAction<T extends ActionFunction<any>, S extends object> = (
) => Promise<ReturnType<T>>; ) => Promise<ReturnType<T>>;
export type QueuedStoreActions<S extends object, A extends UserActions<S>> = { export type QueuedStoreActions<S extends object, A extends UserActions<S>> = {
[K in keyof A]: AsyncAction<A[K], S> [K in keyof A]: AsyncAction<A[K], S>;
}; };
export type ComputedValues<S extends object, C extends UserComputedValues<S>> = { export type ComputedValues<S extends object, C extends UserComputedValues<S>> = {
[K in keyof C]: ReturnType<C[K]> [K in keyof C]: ReturnType<C[K]>;
}; };

View File

@ -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);
});
});