Match-id-f14dd4c421a2527b83579268a6c6f27bddc91913
This commit is contained in:
commit
919ace19a7
|
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,9 +34,9 @@ export function Provider({
|
|||
return createElement(Context.Provider, { value: store }, children);
|
||||
}
|
||||
|
||||
export function createStoreHook(context: Context) {
|
||||
export function createStoreHook(context: Context): () => ReduxStoreHandler {
|
||||
return () => {
|
||||
return useContext(context);
|
||||
return useContext(context) as unknown as ReduxStoreHandler;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -85,17 +85,19 @@ export function connect(
|
|||
mergeProps?: (stateProps: object, dispatchProps: object, ownProps: object) => object,
|
||||
options?: {
|
||||
areStatesEqual?: (oldState: any, newState: any) => boolean;
|
||||
context?: any; // TODO: type this
|
||||
context?: Context;
|
||||
}
|
||||
) {
|
||||
): Connector<OwnProps, MergedProps> {
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
return Component => {
|
||||
//this component should bear the type returned from mapping functions
|
||||
return (Component: OriginalComponent<MergedProps>): WrappedComponent<OwnProps> => {
|
||||
const useStore = createStoreHook(options?.context || DefaultContext);
|
||||
|
||||
function Wrapper(props) {
|
||||
//this component should mimic original type of component used
|
||||
const Wrapper: WrappedComponent<OwnProps> = (props: OwnProps) => {
|
||||
const [f, forceReload] = useState(true);
|
||||
|
||||
const store = useStore() as unknown as ReduxStoreHandler;
|
||||
|
@ -112,36 +114,34 @@ export function connect(
|
|||
mappedState: {},
|
||||
}) as {
|
||||
current: {
|
||||
state: {};
|
||||
mappedState: {};
|
||||
state: { [key: string]: any };
|
||||
mappedState: StateProps;
|
||||
};
|
||||
};
|
||||
|
||||
let mappedState;
|
||||
let mappedState: StateProps;
|
||||
if (options?.areStatesEqual) {
|
||||
if (options.areStatesEqual(previous.current.state, store.getState())) {
|
||||
mappedState = previous.current.mappedState;
|
||||
mappedState = previous.current.mappedState as StateProps;
|
||||
} else {
|
||||
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {};
|
||||
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps);
|
||||
previous.current.mappedState = mappedState;
|
||||
}
|
||||
} else {
|
||||
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {};
|
||||
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps);
|
||||
previous.current.mappedState = mappedState;
|
||||
}
|
||||
let mappedDispatch: { dispatch?: (action) => void } = {};
|
||||
let mappedDispatch: DispatchProps = {} as DispatchProps;
|
||||
if (mapDispatchToProps) {
|
||||
if (typeof mapDispatchToProps === 'object') {
|
||||
Object.entries(mapDispatchToProps).forEach(([key, value]) => {
|
||||
mappedDispatch[key] = (...args) => {
|
||||
mappedDispatch[key] = (...args: ReduxAction[]) => {
|
||||
store.dispatch(value(...args));
|
||||
};
|
||||
});
|
||||
} else {
|
||||
mappedDispatch = mapDispatchToProps(store.dispatch, props);
|
||||
}
|
||||
} else {
|
||||
mappedDispatch.dispatch = store.dispatch;
|
||||
}
|
||||
const mergedProps = (
|
||||
mergeProps ||
|
||||
|
@ -154,7 +154,7 @@ export function connect(
|
|||
|
||||
const node = createElement(Component, mergedProps);
|
||||
return node;
|
||||
}
|
||||
};
|
||||
|
||||
return Wrapper;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
export const INITIALIZED = 'horizonx store initialized';
|
||||
export const STATE_CHANGE = 'horizonx state change';
|
||||
export const SUBSCRIBED = 'horizonx subscribed';
|
||||
export const UNSUBSCRIBED = 'horizonx unsubscribed';
|
||||
export const ACTION = 'horizonx action';
|
||||
export const ACTION_QUEUED = 'horizonx action queued';
|
||||
export const QUEUE_PENDING = 'horizonx queue pending';
|
||||
export const QUEUE_FINISHED = 'horizonx queue finished';
|
||||
export const RENDER_TRIGGERED = 'horizonx render triggered';
|
||||
export const OBSERVED_COMPONENTS = 'horizonx observed components';
|
|
@ -0,0 +1,120 @@
|
|||
import { getStore, getAllStores } from '../store/StoreHandler';
|
||||
import { OBSERVED_COMPONENTS } from './constants';
|
||||
|
||||
const sessionId = Date.now();
|
||||
|
||||
// this function is used to detect devtool connection
|
||||
export function isPanelActive() {
|
||||
return window['__HORIZON_DEV_HOOK__'];
|
||||
}
|
||||
|
||||
// serializes store and creates expanded object with baked-in containing current computed values
|
||||
function makeStoreSnapshot({ type, data }) {
|
||||
const expanded = {};
|
||||
Object.keys(data.store.$c).forEach(key => {
|
||||
expanded[key] = data.store[key];
|
||||
});
|
||||
data.store.expanded = expanded;
|
||||
const snapshot = makeProxySnapshot({
|
||||
data,
|
||||
type,
|
||||
sessionId,
|
||||
});
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
// safely serializes variables containing values wrapped in Proxy object
|
||||
function makeProxySnapshot(obj) {
|
||||
let clone;
|
||||
try {
|
||||
if (!obj) {
|
||||
return obj;
|
||||
}
|
||||
if (obj.nativeEvent) return obj.type + 'Event';
|
||||
if (typeof obj === 'function') {
|
||||
return obj.toString();
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
clone = [];
|
||||
obj.forEach(item => clone.push(makeProxySnapshot(item)));
|
||||
return clone;
|
||||
} else if (typeof obj === 'object') {
|
||||
clone = {};
|
||||
Object.entries(obj).forEach(([id, value]) => (clone[id] = makeProxySnapshot(value)));
|
||||
return clone;
|
||||
}
|
||||
return obj;
|
||||
} catch (err) {
|
||||
throw console.log('cannot serialize object. ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
export const devtools = {
|
||||
// returns vNode id from horizon devtools
|
||||
getVNodeId: vNode => {
|
||||
if (!isPanelActive()) return;
|
||||
window['__HORIZON_DEV_HOOK__'].send(); // update list first
|
||||
return window['__HORIZON_DEV_HOOK__'].getVnodeId(vNode);
|
||||
},
|
||||
// sends horizonx devtool message to extension
|
||||
emit: (type, data) => {
|
||||
if (!isPanelActive()) return;
|
||||
window.postMessage({
|
||||
type: 'HORIZON_DEV_TOOLS',
|
||||
payload: makeStoreSnapshot({ type, data }),
|
||||
from: 'dev tool hook',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// collects components that are dependant on horizonx store and their ids
|
||||
function getAffectedComponents() {
|
||||
const allStores = getAllStores();
|
||||
const keys = Object.keys(allStores);
|
||||
let res = {};
|
||||
keys.forEach(key => {
|
||||
const subRes = new Set();
|
||||
const process = Array.from(allStores[key].$config.state._horizonObserver.keyVNodes.values());
|
||||
while (process.length) {
|
||||
let pivot = process.shift();
|
||||
if (pivot?.tag) subRes.add(pivot);
|
||||
if (pivot?.toString() === '[object Set]') Array.from(pivot).forEach(item => process.push(item));
|
||||
}
|
||||
res[key] = Array.from(subRes).map(vnode => {
|
||||
return {
|
||||
name: vnode?.type
|
||||
.toString()
|
||||
.replace(/\{.*\}/gms, '{...}')
|
||||
.replace('function ', ''),
|
||||
nodeId: window.__HORIZON_DEV_HOOK__.getVnodeId(vnode),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// listens to messages from background
|
||||
window.addEventListener('message', messageEvent => {
|
||||
if (messageEvent?.data?.payload?.type === 'horizonx request observed components') {
|
||||
// get observed components
|
||||
setTimeout(() => {
|
||||
window.postMessage({
|
||||
type: 'HORIZON_DEV_TOOLS',
|
||||
payload: { type: OBSERVED_COMPONENTS, data: getAffectedComponents() },
|
||||
from: 'dev tool hook',
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// executes store action
|
||||
if (messageEvent.data.payload.type === 'horizonx executue action') {
|
||||
const data = messageEvent.data.payload.data;
|
||||
const store = getStore(data.storeId);
|
||||
if (!store?.[data.action]) return;
|
||||
|
||||
const action = store[data.action];
|
||||
const params = data.params;
|
||||
action(...params);
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
import { launchUpdateFromVNode } from '../../renderer/TreeBuilder';
|
||||
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
||||
import { VNode } from '../../renderer/vnode/VNode';
|
||||
import { devtools } from '../devtools';
|
||||
|
||||
export interface IObserver {
|
||||
useProp: (key: string) => void;
|
||||
|
@ -24,9 +25,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 +44,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 +77,7 @@ export class Observer implements IObserver {
|
|||
}
|
||||
|
||||
// 对象的属性被赋值时调用
|
||||
setProp(key: string | symbol): void {
|
||||
setProp(key: string | symbol, mutation: any): void {
|
||||
const vNodes = this.keyVNodes.get(key);
|
||||
//NOTE: using Set directly can lead to deadlock
|
||||
const vNodeArray = Array.from(vNodes || []);
|
||||
|
@ -91,7 +92,8 @@ export class Observer implements IObserver {
|
|||
this.triggerUpdate(vNode);
|
||||
});
|
||||
|
||||
this.triggerChangeListeners();
|
||||
// NOTE: mutations are different in dev and production.
|
||||
this.triggerChangeListeners({ mutation, vNodes });
|
||||
}
|
||||
|
||||
triggerUpdate(vNode: VNode): void {
|
||||
|
@ -103,16 +105,35 @@ 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, vNodes }): void {
|
||||
const nodesList = vNodes ? Array.from(vNodes) : [];
|
||||
this.listeners.forEach(listener =>
|
||||
listener({
|
||||
mutation,
|
||||
vNodes: nodesList.map(vNode => {
|
||||
let realNode = vNode.realNode;
|
||||
let searchedNode = vNode;
|
||||
while (!realNode) {
|
||||
searchedNode = searchedNode.child;
|
||||
realNode = searchedNode.realNode;
|
||||
}
|
||||
return {
|
||||
type: vNode?.type?.name,
|
||||
id: devtools.getVNodeId(vNode),
|
||||
path: vNode.path,
|
||||
element: realNode?.outerHTML?.substr(0, 100),
|
||||
};
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 触发所有使用的props的VNode更新
|
||||
|
@ -120,7 +141,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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,9 +27,9 @@ const proxyMap = new WeakMap();
|
|||
|
||||
export const hookObserverMap = new WeakMap();
|
||||
|
||||
export function createProxy(rawObj: any, isHookObserver = true): any {
|
||||
export function createProxy(rawObj: any, id, isHookObserver = true): any {
|
||||
// 不是对象(是原始数据类型)不用代理
|
||||
if (!isObject(rawObj)) {
|
||||
if (!(rawObj && isObject(rawObj))) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
import { getObserver } from '../ProxyHandler';
|
||||
import { isSame, isValidIntegerKey } from '../../CommonUtils';
|
||||
import { get as objectGet } from './ObjectProxyHandler';
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
import { isPanelActive } from '../../devtools';
|
||||
|
||||
export function createArrayProxy(rawObj: any[]): any[] {
|
||||
const handle = {
|
||||
|
@ -53,26 +55,30 @@ function set(rawObj: any[], key: string, value: any, receiver: any) {
|
|||
const oldLength = rawObj.length;
|
||||
const newValue = value;
|
||||
|
||||
const oldArray = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
|
||||
const ret = Reflect.set(rawObj, key, newValue, receiver);
|
||||
|
||||
const newLength = rawObj.length;
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
const mutation = isPanelActive() ? resolveMutation(oldArray, rawObj) : { mutation: true, from: [], to: rawObj };
|
||||
|
||||
if (!isSame(newValue, oldValue)) {
|
||||
// 值不一样,触发监听器
|
||||
if (observer.watchers?.[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, newValue);
|
||||
cb(key, oldValue, newValue, mutation);
|
||||
});
|
||||
}
|
||||
|
||||
// 触发属性变化
|
||||
observer.setProp(key);
|
||||
observer.setProp(key, mutation);
|
||||
}
|
||||
|
||||
if (oldLength !== newLength) {
|
||||
// 触发数组的大小变化
|
||||
observer.setProp('length');
|
||||
observer.setProp('length', mutation);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { isMap, isWeakMap, isSame } from '../../CommonUtils';
|
||||
import { resolveMutation } from '../../CommonUtils';
|
||||
import { isPanelActive } from '../../devtools';
|
||||
|
||||
const COLLECTION_CHANGE = '_collectionChange';
|
||||
const handler = {
|
||||
|
@ -90,18 +92,20 @@ function set(
|
|||
const valChange = !isSame(newValue, oldValue);
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
const mutation = isPanelActive() ? resolveMutation(oldValue, rawObj) : { mutation: true, from: null, to: rawObj };
|
||||
|
||||
if (valChange || !rawObj.has(key)) {
|
||||
observer.setProp(COLLECTION_CHANGE);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
}
|
||||
|
||||
if (valChange) {
|
||||
if (observer.watchers?.[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, newValue);
|
||||
cb(key, oldValue, newValue, mutation);
|
||||
});
|
||||
}
|
||||
|
||||
observer.setProp(key);
|
||||
observer.setProp(key, mutation);
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
|
@ -109,12 +113,16 @@ function set(
|
|||
|
||||
// Set的add方法
|
||||
function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object {
|
||||
const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
if (!rawObj.has(value)) {
|
||||
rawObj.add(value);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
observer.setProp(value);
|
||||
observer.setProp(COLLECTION_CHANGE);
|
||||
const mutation = isPanelActive()
|
||||
? resolveMutation(oldCollection, rawObj)
|
||||
: { mutation: true, from: null, to: rawObj };
|
||||
observer.setProp(value, mutation);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
|
@ -138,12 +146,16 @@ function clear(rawObj: { size: number; clear: () => void }) {
|
|||
}
|
||||
|
||||
function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) {
|
||||
const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
if (rawObj.has(key)) {
|
||||
rawObj.delete(key);
|
||||
|
||||
const observer = getObserver(rawObj);
|
||||
observer.setProp(key);
|
||||
observer.setProp(COLLECTION_CHANGE);
|
||||
const mutation = isPanelActive()
|
||||
? resolveMutation(oldCollection, rawObj)
|
||||
: { mutation: true, from: null, to: rawObj };
|
||||
observer.setProp(key, mutation);
|
||||
observer.setProp(COLLECTION_CHANGE, mutation);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -13,9 +13,10 @@
|
|||
* 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';
|
||||
import { isPanelActive } from '../../devtools';
|
||||
|
||||
export function createObjectProxy<T extends object>(rawObj: T, singleLevel = false): ProxyHandler<T> {
|
||||
const proxy = new Proxy(rawObj, {
|
||||
|
@ -70,6 +71,7 @@ export function get(rawObj: object, key: string | symbol, receiver: any, singleL
|
|||
}
|
||||
|
||||
export function set(rawObj: object, key: string, value: any, receiver: any): boolean {
|
||||
const oldObject = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null;
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
if (value && key == 'removeListener') {
|
||||
|
@ -79,15 +81,15 @@ export function set(rawObj: object, key: string, value: any, receiver: any): boo
|
|||
const newValue = value;
|
||||
|
||||
const ret = Reflect.set(rawObj, key, newValue, receiver);
|
||||
const mutation = isPanelActive() ? resolveMutation(oldObject, rawObj) : { mutation: true, from: null, to: rawObj };
|
||||
|
||||
if (!isSame(newValue, oldValue)) {
|
||||
if (observer.watchers?.[key]) {
|
||||
observer.watchers[key].forEach(cb => {
|
||||
cb(key, oldValue, newValue);
|
||||
cb(key, oldValue, newValue, mutation);
|
||||
});
|
||||
}
|
||||
observer.setProp(key);
|
||||
observer.setProp(key, mutation);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -14,26 +14,44 @@
|
|||
*/
|
||||
|
||||
import { useEffect, useRef } from '../../renderer/hooks/HookExternal';
|
||||
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
||||
import { getProcessingVNode, getStartVNode } 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 { isPromise } from '../CommonUtils';
|
||||
import type {
|
||||
ActionFunction, ComputedValues,
|
||||
PlannedAction, QueuedStoreActions,
|
||||
ActionFunction,
|
||||
ComputedValues,
|
||||
PlannedAction,
|
||||
QueuedStoreActions,
|
||||
StoreActions,
|
||||
StoreConfig,
|
||||
StoreObj,
|
||||
UserActions,
|
||||
UserComputedValues
|
||||
UserComputedValues,
|
||||
} from '../types';
|
||||
import {VNode} from '../../renderer/vnode/VNode';
|
||||
import { VNode } from '../../renderer/vnode/VNode';
|
||||
import { devtools } from '../devtools';
|
||||
import {
|
||||
ACTION,
|
||||
ACTION_QUEUED,
|
||||
INITIALIZED,
|
||||
QUEUE_FINISHED,
|
||||
STATE_CHANGE,
|
||||
SUBSCRIBED,
|
||||
UNSUBSCRIBED,
|
||||
} from '../devtools/constants';
|
||||
|
||||
const idGenerator = {
|
||||
id: 0,
|
||||
get: function (prefix) {
|
||||
return prefix.toString() + this.id++;
|
||||
},
|
||||
};
|
||||
|
||||
const storeMap = new Map<string, StoreObj<any, any, any>>();
|
||||
|
||||
|
||||
export function createStore<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>>(
|
||||
config: StoreConfig<S, A, C>
|
||||
): () => StoreObj<S, A, C> {
|
||||
|
@ -42,7 +60,9 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
|
|||
throw new Error('store obj must be pure object');
|
||||
}
|
||||
|
||||
const proxyObj = createProxy(config.state, !config.options?.isReduxAdapter);
|
||||
const id = config.id || idGenerator.get('UNNAMED_STORE');
|
||||
|
||||
const proxyObj = createProxy(config.state, id, !config.options?.isReduxAdapter);
|
||||
|
||||
proxyObj.$pending = false;
|
||||
|
||||
|
@ -50,15 +70,18 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
|
|||
const $queue: Partial<StoreActions<S, A>> = {};
|
||||
const $c: Partial<ComputedValues<S, C>> = {};
|
||||
const storeObj = {
|
||||
id,
|
||||
$s: proxyObj,
|
||||
$a: $a as StoreActions<S, A>,
|
||||
$c: $c as ComputedValues<S, C>,
|
||||
$queue: $queue as QueuedStoreActions<S, A>,
|
||||
$config: config,
|
||||
$subscribe: listener => {
|
||||
devtools.emit(SUBSCRIBED, { store: storeObj, listener });
|
||||
proxyObj.addListener(listener);
|
||||
},
|
||||
$unsubscribe: listener => {
|
||||
devtools.emit(UNSUBSCRIBED, storeObj);
|
||||
proxyObj.removeListener(listener);
|
||||
},
|
||||
} as unknown as StoreObj<S, A, C>;
|
||||
|
@ -71,6 +94,14 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
|
|||
// 让store.$queue[action]可以访问到action方法
|
||||
// 要达到的效果:如果通过store.$queue[action1]调用的action1返回promise,会阻塞下一个store.$queue[action2]
|
||||
($queue as any)[action] = (...payload) => {
|
||||
devtools.emit(ACTION_QUEUED, {
|
||||
store: storeObj,
|
||||
action: {
|
||||
action,
|
||||
payload,
|
||||
},
|
||||
fromQueue: true,
|
||||
});
|
||||
return new Promise(resolve => {
|
||||
if (!proxyObj.$pending) {
|
||||
proxyObj.$pending = true;
|
||||
|
@ -99,6 +130,14 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
|
|||
|
||||
// 让store.$a[action]可以访问到action方法
|
||||
($a as any)[action] = function Wrapped(...payload) {
|
||||
devtools.emit(ACTION, {
|
||||
store: storeObj,
|
||||
action: {
|
||||
action,
|
||||
payload,
|
||||
},
|
||||
fromQueue: false,
|
||||
});
|
||||
return config.actions![action].bind(storeObj, proxyObj)(...payload);
|
||||
};
|
||||
|
||||
|
@ -106,6 +145,14 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
|
|||
Object.defineProperty(storeObj, action, {
|
||||
writable: false,
|
||||
value: (...payload) => {
|
||||
devtools.emit(ACTION, {
|
||||
store: storeObj,
|
||||
action: {
|
||||
action,
|
||||
payload,
|
||||
},
|
||||
fromQueue: false,
|
||||
});
|
||||
return config.actions![action].bind(storeObj, proxyObj)(...payload);
|
||||
},
|
||||
});
|
||||
|
@ -132,13 +179,25 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
|
|||
// 从Proxy对象获取值,会触发代理
|
||||
return proxyObj[key];
|
||||
},
|
||||
set: value => {
|
||||
proxyObj[key] = value;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (config.id) {
|
||||
storeMap.set(config.id, storeObj);
|
||||
}
|
||||
storeMap.set(id, storeObj);
|
||||
|
||||
devtools.emit(INITIALIZED, {
|
||||
store: storeObj,
|
||||
});
|
||||
|
||||
proxyObj.addListener(change => {
|
||||
devtools.emit(STATE_CHANGE, {
|
||||
store: storeObj,
|
||||
change,
|
||||
});
|
||||
});
|
||||
|
||||
return createGetStore(storeObj);
|
||||
}
|
||||
|
@ -217,9 +276,10 @@ function registerDestroyFunction() {
|
|||
vNodeRef.current.observers = null;
|
||||
};
|
||||
}, []);
|
||||
} else if (processingVNode.tag === ClassComponent) { // 类组件
|
||||
} else if (processingVNode.tag === ClassComponent) {
|
||||
// 类组件
|
||||
if (!processingVNode.classComponentWillUnmount) {
|
||||
processingVNode.classComponentWillUnmount = (vNode) => {
|
||||
processingVNode.classComponentWillUnmount = vNode => {
|
||||
clearVNodeObservers(vNode);
|
||||
vNode.observers = null;
|
||||
};
|
||||
|
@ -240,6 +300,14 @@ export function useStore<S extends object, A extends UserActions<S>, C extends U
|
|||
return storeObj as StoreObj<S, A, C>;
|
||||
}
|
||||
|
||||
export function getStore(id: string) {
|
||||
return storeMap.get(id);
|
||||
}
|
||||
|
||||
export function getAllStores() {
|
||||
return Object.fromEntries(storeMap);
|
||||
}
|
||||
|
||||
export function clearStore(id: string): void {
|
||||
storeMap.delete(id);
|
||||
}
|
||||
|
|
|
@ -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<S extends object, A extends UserActions<S>, C extends Us
|
|||
};
|
||||
|
||||
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;
|
||||
|
||||
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> = (
|
||||
|
@ -61,8 +61,8 @@ export type StoreObj<S extends object, A extends UserActions<S>, C extends UserC
|
|||
$a: StoreActions<S, A>;
|
||||
$c: UserComputedValues<S>;
|
||||
$queue: QueuedStoreActions<S, A>;
|
||||
$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<A[K], S> } & { [K in keyof C]: ReturnType<C[K]> };
|
||||
|
||||
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
|
||||
? []
|
||||
: ((...b: T) => void) extends (a, ...b: infer I) => void
|
||||
? I
|
||||
: [];
|
||||
? I
|
||||
: [];
|
||||
|
||||
export type UserComputedValues<S extends object> = {
|
||||
[K: string]: ComputedFunction<S>
|
||||
[K: string]: ComputedFunction<S>;
|
||||
};
|
||||
|
||||
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>>;
|
||||
|
||||
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>> = {
|
||||
[K in keyof C]: ReturnType<C[K]>
|
||||
[K in keyof C]: ReturnType<C[K]>;
|
||||
};
|
||||
|
|
|
@ -51,6 +51,34 @@ describe('Basic store manipulation', () => {
|
|||
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1');
|
||||
});
|
||||
|
||||
it('Should use direct setters', () => {
|
||||
function App() {
|
||||
const logStore = useLogStore();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
id={BUTTON_ID}
|
||||
onClick={() => {
|
||||
logStore.logs = ['q'];
|
||||
}}
|
||||
>
|
||||
add
|
||||
</button>
|
||||
<p id={RESULT_ID}>{logStore.logs[0]}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, BUTTON_ID);
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('q');
|
||||
});
|
||||
|
||||
it('Should use actions and update components', () => {
|
||||
function App() {
|
||||
const logStore = useLogStore();
|
||||
|
@ -89,7 +117,7 @@ describe('Basic store manipulation', () => {
|
|||
increment: state => {
|
||||
state.count++;
|
||||
},
|
||||
doublePlusOne: function(state) {
|
||||
doublePlusOne: function (state) {
|
||||
state.count = state.count * 2;
|
||||
this.increment();
|
||||
},
|
||||
|
@ -130,15 +158,15 @@ describe('Basic store manipulation', () => {
|
|||
count: 2,
|
||||
},
|
||||
actions: {
|
||||
doublePlusOne: function(state) {
|
||||
doublePlusOne: function (state) {
|
||||
state.count = this.double + 1;
|
||||
},
|
||||
},
|
||||
computed:{
|
||||
double: (state) => {
|
||||
return state.count*2
|
||||
}
|
||||
}
|
||||
computed: {
|
||||
double: state => {
|
||||
return state.count * 2;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
|
@ -166,5 +194,5 @@ describe('Basic store manipulation', () => {
|
|||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5');
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -308,4 +308,4 @@ describe('Redux/React binding adapter', () => {
|
|||
expect(getE(BUTTON).innerHTML).toBe('1');
|
||||
expect(getE(BUTTON2).innerHTML).toBe('true');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
import { createElement } from '../../../../libs/horizon/src/external/JSXElement';
|
||||
import { createDomTextVNode } from '../../../../libs/horizon/src/renderer/vnode/VNodeCreator';
|
||||
import { createStore } from '../../../../libs/horizon/src/horizonx/adapters/redux';
|
||||
import { connect } from '../../../../libs/horizon/src/horizonx/adapters/reduxReact';
|
||||
|
||||
createStore((state: number = 0, action): number => {
|
||||
if (action.type === 'add') return state + 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
type WrappedButtonProps = { add: () => void; count: number; text: string };
|
||||
|
||||
function Button(props: WrappedButtonProps) {
|
||||
const { add, count, text } = props;
|
||||
return createElement(
|
||||
'button',
|
||||
{
|
||||
onClick: add,
|
||||
},
|
||||
createDomTextVNode(text),
|
||||
createDomTextVNode(': '),
|
||||
createDomTextVNode(count)
|
||||
);
|
||||
}
|
||||
|
||||
const connector = connect(
|
||||
state => ({ count: state }),
|
||||
dispatch => ({
|
||||
add: (): void => {
|
||||
dispatch({ type: 'add' });
|
||||
},
|
||||
}),
|
||||
(stateProps, dispatchProps, ownProps: { text: string }) => ({
|
||||
add: dispatchProps.add,
|
||||
count: stateProps.count,
|
||||
text: ownProps.text,
|
||||
})
|
||||
);
|
||||
|
||||
const ConnectedButton = connector(Button);
|
||||
|
||||
function App() {
|
||||
return createElement('div', {}, createElement(ConnectedButton, { text: 'click' }));
|
||||
}
|
||||
|
||||
export default App;
|
Loading…
Reference in New Issue