Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 41 additions & 23 deletions src/pages/components/ScriptMenuList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ const ListMenuItem = React.memo(
});
};

// console.log("Rendered - " + item.name); // 用于检查垃圾React有否过度更新

return (
<Collapse
activeKey={isActive ? item.uuid : undefined}
Expand Down Expand Up @@ -324,11 +326,45 @@ ListMenuItem.displayName = "ListMenuItem";

type TGrouppedMenus = Record<string, GroupScriptMenuItemsProp> & { __length__?: number };

type ScriptMenuEntry = ScriptMenu & {
type ScriptMenuEntryBase = ScriptMenu & {
menuUpdated?: number;
};

// ScriptMenuEntryBase 加了 metadata 后变成 ScriptMenuEntry
type ScriptMenuEntry = ScriptMenuEntryBase & {
metadata: SCMetadata;
};

const cacheMetadata = new Map<string, SCMetadata | undefined>();
// 使用 WeakMap:当 ScriptMenuEntryBase 替换后,ScriptMenuEntryBase的引用会失去,ScriptMenuEntry能被自动回收。
const cacheMergedItem = new WeakMap<ScriptMenuEntryBase, ScriptMenuEntry>();
// scriptList 更新后会合并 从异步取得的 metadata 至 mergedList
const fetchMergedList = async (item: ScriptMenuEntryBase) => {
const uuid = item.uuid;
// 检查 cacheMetadata 有没有记录
let metadata = cacheMetadata.get(uuid);
if (!metadata) {
// 如没有记录,对 scriptDAO 发出请求 (通常在首次React元件绘画时进行)
const script = await scriptDAO.get(uuid);
metadata = script?.metadata || {}; // 即使 scriptDAO 返回失败也 fallback 一个空物件
cacheMetadata.set(uuid, metadata);
}
// 检查 cacheMergedItem 有没有记录
let merged = cacheMergedItem.get(item);
if (!merged || merged.uuid !== item.uuid) {
// 如没有记录或记录不正确,则重新生成记录 (新物件参考)
merged = { ...item, metadata };
cacheMergedItem.set(item, merged);
}
// 如 cacheMergedItem 的记录中的 metadata 跟 (新)metadata 物件参考不一致,则更新 merged
if (merged.metadata !== metadata) {
// 新物件参考触发 React UI 重绘
merged = { ...merged, metadata: metadata };
cacheMergedItem.set(item, merged);
}
return merged;
};

// Popup 页面使用的脚本/选单清单元件:只负责渲染与互动,状态与持久化交由外部 client 处理。
const ScriptMenuList = React.memo(
({
Expand All @@ -337,9 +373,7 @@ const ScriptMenuList = React.memo(
currentUrl,
menuExpandNum,
}: {
script: (ScriptMenu & {
menuUpdated?: number;
})[];
script: ScriptMenuEntryBase[];
isBackscript: boolean;
currentUrl: string;
menuExpandNum: number;
Expand Down Expand Up @@ -406,34 +440,18 @@ const ScriptMenuList = React.memo(
return url;
}, [currentUrl]);

const cache = useMemo(() => new Map<string, SCMetadata | undefined>(), []);
// 以 异步方式 取得 metadata 放入 extraData
// script 或 extraData 的更新时都会再次执行
useEffect(() => {
let isMounted = true;
// 先从 cache 读取,避免重复请求相同 uuid 的 metadata
Promise.all(
script.map(async (item) => {
let metadata = cache.get(item.uuid);
if (!metadata) {
const script = await scriptDAO.get(item.uuid);
if (script) {
metadata = script.metadata || {};
}
cache.set(item.uuid, metadata);
}
return { ...item, metadata: metadata || {} };
})
).then((newScriptMenuList) => {
Promise.all(script.map(fetchMergedList)).then((newList) => {
if (!isMounted) {
return;
}
updateScriptMenuList(newScriptMenuList);
updateScriptMenuList(newList);
});
return () => {
isMounted = false;
};
}, [cache, script]);
}, [script]);

useEffect(() => {
// 注册菜单快速键(accessKey):以各分组第一个项目的 accessKey 作为触发条件。
Expand Down
2 changes: 1 addition & 1 deletion src/pages/components/ScriptSetting/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const ScriptSetting: React.FC<{
},
];
return ret;
}, [script, scriptRunEnv, scriptRunAt, t]);
}, [script.uuid, scriptRunEnv, scriptRunAt, t]);

useEffect(() => {
const scriptDAO = new ScriptDAO();
Expand Down
92 changes: 61 additions & 31 deletions src/pages/popup/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,32 @@ const scriptListSorter = (a: ScriptMenu, b: ScriptMenu) =>
b.runNum - a.runNum ||
b.updatetime - a.updatetime;

type TUpdateEntryFn = (item: ScriptMenu) => ScriptMenu | undefined;

type TUpdateListOption = { sort?: boolean };

const updateList = (list: ScriptMenu[], update: TUpdateEntryFn, options: TUpdateListOption | undefined) => {
// 如果更新跟当前 list 的子项无关,则不用更改 list 的物件参考
const newList = [];
let changed = false;
for (let i = 0; i < list.length; i++) {
const oldItem = list[i];
const newItem = update(oldItem); // 如没有更改,物件参考会保持一致
if (newItem !== oldItem) changed = true;
if (newItem) {
newList.push(newItem);
}
}
if (options?.sort) {
newList.sort(scriptListSorter);
}
if (!changed && list.map((e) => e.uuid).join(",") !== newList.map((e) => e.uuid).join(",")) {
// 单一项未有改变,但因为 sort值改变 而改变了次序
changed = true;
}
return changed ? newList : list; // 如子项没任何变化,则返回原list参考
};

function App() {
const [loading, setLoading] = useState(true);
const [scriptList, setScriptList] = useState<(ScriptMenu & { menuUpdated?: number })[]>([]);
Expand All @@ -59,23 +85,48 @@ function App() {
const { t } = useTranslation();
const pageTabIdRef = useRef(0);

// ------------------------------ 重要! 不要隨便更改 ------------------------------
// > scriptList 會隨著 (( 任何 )) 子項狀態更新而進行物件參考更新
// > (( 必須 )) 把物件參考更新切換成 原始类型(例如字串)

// normalEnables: 只随 script 数量和启动状态而改变的state
// 故意生成一个字串 memo 避免因 scriptList 的参考频繁改动而导致 normalScriptCounts 的物件参考出现非预期更改。
const normalEnables = useMemo(() => {
// 返回字串让 React 比对 state 有否改动
return scriptList.map((script) => (script.enable ? 1 : 0)).join(",");
}, [scriptList]);

// backEnables: 只随 script 数量和启动状态而改变的state
// 故意生成一个字串 memo 避免因 scriptList 的参考频繁改动而导致 backScriptCounts 的物件参考出现非预期更改。
const backEnables = useMemo(() => {
// 返回字串让 React 比对 state 有否改动
return backScriptList.map((script) => (script.enable ? 1 : 0)).join(",");
}, [backScriptList]);
// ------------------------------ 重要! 不要隨便更改 ------------------------------

// normalScriptCounts 的物件參考只會隨 原始类型(字串)的 normalEnables 狀態更新而重新生成
const normalScriptCounts = useMemo(() => {
// 拆回array
const enables = normalEnables.split(",").filter(Boolean);
// 计算已开启了的数量
const running = scriptList.reduce((p, c) => p + (c.enable ? 1 : 0), 0);
const running = enables.reduce((p, c) => p + (+c ? 1 : 0), 0);
return {
running,
total: scriptList.length, // 总数
total: enables.length, // 总数
};
}, [scriptList]);
}, [normalEnables]);

// backScriptCounts 的物件參考只會隨 原始类型(字串)的 backEnables 狀態更新而重新生成
const backScriptCounts = useMemo(() => {
// 拆回array
const enables = backEnables.split(",").filter(Boolean);
// 计算已开启了的数量
const running = backScriptList.reduce((p, c) => p + (c.enable ? 1 : 0), 0);
const running = enables.reduce((p, c) => p + (+c ? 1 : 0), 0);
return {
running,
total: backScriptList.length, // 总数
total: enables.length, // 总数
};
}, [backScriptList]);
}, [backEnables]);

const urlHost = useMemo(() => {
let url: URL | undefined;
Expand All @@ -91,31 +142,10 @@ function App() {
useEffect(() => {
let isMounted = true;

const updateScriptList = (
update: (item: ScriptMenu) => ScriptMenu | undefined,
options?: {
sort?: boolean;
}
) => {
const updateList = (list: ScriptMenu[], update: (item: ScriptMenu) => ScriptMenu | undefined) => {
const newList = [];
for (let i = 0; i < list.length; i++) {
const newItem = update(list[i]);
if (newItem) {
newList.push(newItem);
}
}
if (options?.sort) {
newList.sort(scriptListSorter);
}
return newList;
};
setScriptList((prev) => {
return updateList(prev, update);
});
setBackScriptList((prev) => {
return updateList(prev, update);
});
const updateScriptList = (update: TUpdateEntryFn, options?: TUpdateListOption) => {
// 当 启用/禁用/菜单改变 时,如有必要则更新 list 参考
setScriptList((prev) => updateList(prev, update, options));
setBackScriptList((prev) => updateList(prev, update, options));
};

const unhooks = [
Expand Down
Loading