diff --git a/libs/horizon/src/horizonx/devtools/constants.ts b/libs/horizon/src/horizonx/devtools/constants.ts index df41626f..3e105125 100644 --- a/libs/horizon/src/horizonx/devtools/constants.ts +++ b/libs/horizon/src/horizonx/devtools/constants.ts @@ -6,3 +6,5 @@ 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'; diff --git a/libs/horizon/src/horizonx/devtools/index.ts b/libs/horizon/src/horizonx/devtools/index.ts index 5aecbff3..7484fa88 100644 --- a/libs/horizon/src/horizonx/devtools/index.ts +++ b/libs/horizon/src/horizonx/devtools/index.ts @@ -1,5 +1,12 @@ +import { getStore, getAllStores } from '../store/StoreHandler'; +import { OBSERVED_COMPONENTS } from './constants'; + const sessionId = Date.now(); +export function isPanelActive() { + return window['__HORIZON_DEV_HOOK__']; +} + function makeStoreSnapshot({ type, data }) { const expanded = {}; Object.keys(data.store.$c).forEach(key => { @@ -40,9 +47,12 @@ function makeProxySnapshot(obj) { } export const devtools = { + getVNodeId: vNode => { + if (!isPanelActive()) return; + getVNodeId(vNode); + }, emit: (type, data) => { - if (!window['__HORIZON_DEV_HOOK__']) return; - console.log('store snapshot:', makeStoreSnapshot({ type, data })); + if (!isPanelActive()) return; window.postMessage({ type: 'HORIZON_DEV_TOOLS', payload: makeStoreSnapshot({ type, data }), @@ -50,3 +60,58 @@ export const devtools = { }); }, }; + +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; +} + +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); + } + + if (messageEvent.data.payload.type === 'horizonx executue action') { + const data = messageEvent.data.payload.data; + const store = getStore(data.storeId); + if (!store?.[data.action]) { + } + + const action = store[data.action]; + const params = data.params; + action(...params).bind(store); + } +}); + +export function getVNodeId(vNode) { + window['__HORIZON_DEV_HOOK__'].send(); + return window['__HORIZON_DEV_HOOK__'].getVnodeId(vNode); +} diff --git a/libs/horizon/src/horizonx/proxy/Observer.ts b/libs/horizon/src/horizonx/proxy/Observer.ts index 2f3b15f8..adef63fc 100644 --- a/libs/horizon/src/horizonx/proxy/Observer.ts +++ b/libs/horizon/src/horizonx/proxy/Observer.ts @@ -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; @@ -89,7 +90,8 @@ export class Observer implements IObserver { this.triggerUpdate(vNode); }); - this.triggerChangeListeners(mutation); + // NOTE: mutations are different in dev and production. + this.triggerChangeListeners({ mutation, vNodes }); } triggerUpdate(vNode: VNode): void { @@ -109,8 +111,27 @@ export class Observer implements IObserver { this.listeners = this.listeners.filter(item => item != listener); } - triggerChangeListeners(mutation: any): void { - this.listeners.forEach(listener => listener(mutation)); + 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更新 diff --git a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts index ccc54ab1..fda6d653 100644 --- a/libs/horizon/src/horizonx/proxy/ProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/ProxyHandler.ts @@ -27,7 +27,7 @@ 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 (!(rawObj && isObject(rawObj))) { return rawObj; diff --git a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts index 672253f8..9e9c2845 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ArrayProxyHandler.ts @@ -17,6 +17,7 @@ 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 = { @@ -54,28 +55,30 @@ 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 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, resolveMutation(oldArray, rawObj)); + cb(key, oldValue, newValue, mutation); }); } // 触发属性变化 - observer.setProp(key, resolveMutation(oldValue, rawObj)); + observer.setProp(key, mutation); } if (oldLength !== newLength) { // 触发数组的大小变化 - observer.setProp('length', resolveMutation(oldValue, rawObj)); + observer.setProp('length', mutation); } return ret; diff --git a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts index 74fa57bc..05174cc5 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/CollectionProxyHandler.ts @@ -16,6 +16,7 @@ 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 = { @@ -91,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, resolveMutation(oldValue, rawObj)); + observer.setProp(COLLECTION_CHANGE, mutation); } if (valChange) { if (observer.watchers?.[key]) { observer.watchers[key].forEach(cb => { - cb(key, oldValue, newValue, resolveMutation(oldValue, rawObj)); + cb(key, oldValue, newValue, mutation); }); } - observer.setProp(key, resolveMutation(oldValue, rawObj)); + observer.setProp(key, mutation); } return rawObj; @@ -110,13 +113,16 @@ 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)); + const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; if (!rawObj.has(value)) { rawObj.add(value); const observer = getObserver(rawObj); - observer.setProp(value, resolveMutation(oldCollection, rawObj)); - observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj)); + const mutation = isPanelActive() + ? resolveMutation(oldCollection, rawObj) + : { mutation: true, from: null, to: rawObj }; + observer.setProp(value, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); } return rawObj; @@ -140,13 +146,16 @@ 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)); + const oldCollection = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; if (rawObj.has(key)) { rawObj.delete(key); const observer = getObserver(rawObj); - observer.setProp(key, resolveMutation(oldCollection, rawObj)); - observer.setProp(COLLECTION_CHANGE, resolveMutation(oldCollection, rawObj)); + const mutation = isPanelActive() + ? resolveMutation(oldCollection, rawObj) + : { mutation: true, from: null, to: rawObj }; + observer.setProp(key, mutation); + observer.setProp(COLLECTION_CHANGE, mutation); return true; } diff --git a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts index 9e0aac15..80da37fd 100644 --- a/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts +++ b/libs/horizon/src/horizonx/proxy/handlers/ObjectProxyHandler.ts @@ -16,6 +16,7 @@ import { isSame, resolveMutation } from '../../CommonUtils'; import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler'; import { OBSERVER_KEY } from '../../Constants'; +import { isPanelActive } from '../../devtools'; export function createObjectProxy(rawObj: T, singleLevel = false): ProxyHandler { const proxy = new Proxy(rawObj, { @@ -70,7 +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 = JSON.parse(JSON.stringify(rawObj)); + const oldObject = isPanelActive() ? JSON.parse(JSON.stringify(rawObj)) : null; const observer = getObserver(rawObj); if (value && key == 'removeListener') { @@ -80,14 +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, resolveMutation(oldObject, rawObj)); + cb(key, oldValue, newValue, mutation); }); } - observer.setProp(key, resolveMutation(oldObject, rawObj)); + observer.setProp(key, mutation); } return ret; } diff --git a/libs/horizon/src/horizonx/store/StoreHandler.ts b/libs/horizon/src/horizonx/store/StoreHandler.ts index 42db626d..8b3aa154 100644 --- a/libs/horizon/src/horizonx/store/StoreHandler.ts +++ b/libs/horizon/src/horizonx/store/StoreHandler.ts @@ -14,7 +14,7 @@ */ 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'; @@ -60,7 +60,9 @@ export function createStore, 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; @@ -68,6 +70,7 @@ export function createStore, C extend const $queue: Partial> = {}; const $c: Partial> = {}; const storeObj = { + id, $s: proxyObj, $a: $a as StoreActions, $c: $c as ComputedValues, @@ -183,18 +186,16 @@ export function createStore, C extend }); } - if (config.id) { - storeMap.set(config.id, storeObj); - } + storeMap.set(id, storeObj); devtools.emit(INITIALIZED, { store: storeObj, }); - proxyObj.addListener(mutation => { + proxyObj.addListener(change => { devtools.emit(STATE_CHANGE, { store: storeObj, - mutation, + change, }); }); @@ -299,6 +300,14 @@ export function useStore, C extends U return storeObj as StoreObj; } +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); }