diff --git a/doc/docusaurus.config.js b/doc/docusaurus.config.js index 3323af2e..8297827e 100644 --- a/doc/docusaurus.config.js +++ b/doc/docusaurus.config.js @@ -33,13 +33,13 @@ const config = { docs: { sidebarPath: require.resolve('./sidebars.js'), editUrl: ({ docPath }) => { - return `https://holocron.so/github/pr/heluxjs/helux/master/editor/doc/docs/${docPath}` + return `https://holocron.so/github/pr/heluxjs/helux/master/editor/doc/docs/${docPath}`; }, }, blog: { showReadingTime: true, editUrl: ({ docPath }) => { - return `https://holocron.so/github/pr/heluxjs/helux/master/editor/doc/blog/${docPath}` + return `https://holocron.so/github/pr/heluxjs/helux/master/editor/doc/blog/${docPath}`; }, }, theme: { diff --git a/package.json b/package.json index eaf77211..581ed7c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "helux", - "version": "3.4.6", + "version": "3.4.10", "description": "A state library core that integrates atom, signal, collection dep, derive and watch, it supports all react like frameworks( including react 18 ).", "keywords": [], "author": { diff --git a/packages/helux-core/src/consts/index.ts b/packages/helux-core/src/consts/index.ts index 4c9f6a2c..41443dca 100644 --- a/packages/helux-core/src/consts/index.ts +++ b/packages/helux-core/src/consts/index.ts @@ -2,7 +2,7 @@ import { createSymbol, HAS_SYMBOL } from '../helpers/sym'; export { EVENT_NAME, FROM, LOADING_MODE } from './user'; export { HAS_SYMBOL }; -export const VER = '3.4.7'; +export const VER = '3.4.10'; export const PROD_FLAG = true; @@ -70,3 +70,9 @@ export const STATE_TYPE = { // fn type export const DERIVE = 'derive'; export const WATCH = 'watch'; + +/** 来自 limu 的字典类型表达 */ +export const DICT = 'Object'; +/** 来自 limu 的数组类型表达 */ +export const ARR = 'Array'; +export const OTHER = 'Other'; diff --git a/packages/helux-core/src/factory/common/sharedScope.ts b/packages/helux-core/src/factory/common/sharedScope.ts index 96215adc..e766c93c 100644 --- a/packages/helux-core/src/factory/common/sharedScope.ts +++ b/packages/helux-core/src/factory/common/sharedScope.ts @@ -33,6 +33,12 @@ export function diffVal(internal: TInternal, depKey: string) { } export function hasChangedNode(internal: TInternal, depKeys: string[], depKey: string) { + // 优先比较自身值有无变化 + if (depKeys.includes(depKey) && diffVal(internal, depKey)) { + return true; + } + + // 再查找子串值有无变化 let subValChanged = false; for (const storedDepKey of depKeys) { // 是 key 的子串,比较值是否有变化 diff --git a/packages/helux-core/src/factory/common/stopDep.ts b/packages/helux-core/src/factory/common/stopDep.ts index 8a3472d3..db05016f 100644 --- a/packages/helux-core/src/factory/common/stopDep.ts +++ b/packages/helux-core/src/factory/common/stopDep.ts @@ -22,8 +22,8 @@ function getArrIndexKey(confKey: string, fullKey: string) { * 辅助 stopDep 主逻辑之用 */ export function recordArrKey(arrKeys: string[], depKey: string) { - const faterDepKey = matchListItem(arrKeys, depKey); - if (faterDepKey) return; + const parentDepKey = matchListItem(arrKeys, depKey); + if (parentDepKey) return; nodupPush(arrKeys, depKey); } diff --git a/packages/helux-core/src/factory/common/util.ts b/packages/helux-core/src/factory/common/util.ts index bbe340bc..c1765e91 100644 --- a/packages/helux-core/src/factory/common/util.ts +++ b/packages/helux-core/src/factory/common/util.ts @@ -18,8 +18,6 @@ export interface IMutateCtx { triggerReasons: TriggerReason[]; ids: NumStrSymbol[]; globalIds: NumStrSymbol[]; - /** 记录 depKey 对应值是否是字典 */ - isDictInfo: Dict; writeKeys: Dict; arrKeyDict: Dict; writeKeyPathInfo: Dict; @@ -65,7 +63,6 @@ export function newMutateCtx(options: ISetStateOptions): IMutateCtx { triggerReasons: [], ids, globalIds, - isDictInfo: {}, writeKeys: {}, arrKeyDict: {}, // 记录读取过程中遇到的数组 key writeKeyPathInfo: {}, @@ -76,7 +73,17 @@ export function newMutateCtx(options: ISetStateOptions): IMutateCtx { } export function newOpParams(key: string, value: any, isChange = true): IOperateParams { - return { isChange, op: 'set', key, value, parentType: 'Object', keyPath: [], fullKeyPath: [key], isBuiltInFnKey: false }; + return { + isChange, + op: 'set', + key, + value, + proxyValue: value, + parentType: 'Object', + keyPath: [], + fullKeyPath: [key], + isBuiltInFnKey: false, + }; } export function getDepKeyInfo(depKey: string): DepKeyInfo { @@ -142,3 +149,11 @@ export function runPartialCb(forAtom: boolean, mayCb: any, draft: any) { const val = !isFn(mayCb) ? mayCb : mayCb(draft); return wrapPartial(forAtom, val); } + +export function chooseVal(mayReplacedVal: any, value: any) { + return mayReplacedVal !== undefined ? mayReplacedVal : value; +} + +export function chooseProxyVal(mayReplacedVal: any, proxyVal: any, rawVal: any) { + return mayReplacedVal !== undefined ? { proxyVal: mayReplacedVal, rawVal: mayReplacedVal } : { proxyVal, rawVal }; +} diff --git a/packages/helux-core/src/factory/createWatch.ts b/packages/helux-core/src/factory/createWatch.ts index 0ba66f40..d143cf68 100644 --- a/packages/helux-core/src/factory/createWatch.ts +++ b/packages/helux-core/src/factory/createWatch.ts @@ -3,7 +3,7 @@ import { SCOPE_TYPE, WATCH } from '../consts'; import { markFnEnd, markFnStart, registerFn } from '../helpers/fnCtx'; import { recordFnDepKeys } from '../helpers/fnDep'; import { getInternal, getSharedKey } from '../helpers/state'; -import { getAtomValInternal } from '../hooks/common/shared'; +import { getRootValInternal } from '../hooks/common/shared'; import type { Fn, IFnCtx, IWatchFnParams, ScopeType, SharedState, WatchOptionsType } from '../types/base'; import { parseWatchOptions } from './creator/parse'; @@ -21,7 +21,7 @@ function putSharedToDep(list: any[]) { if (Array.isArray(list)) { list.forEach((sharedState: any) => { // sharedState may a atom val passed useWatch deps fn - const internal = getInternal(sharedState) || getAtomValInternal(sharedState); + const internal = getInternal(sharedState) || getRootValInternal(sharedState); if (internal) { const { sharedKey, forAtom } = internal; // deps 列表里的 atom 结果自动拆箱 diff --git a/packages/helux-core/src/factory/creator/buildShared.ts b/packages/helux-core/src/factory/creator/buildShared.ts index af93950f..6de92518 100644 --- a/packages/helux-core/src/factory/creator/buildShared.ts +++ b/packages/helux-core/src/factory/creator/buildShared.ts @@ -8,6 +8,7 @@ import { mapSharedState } from '../../helpers/state'; import type { Dict } from '../../types/base'; import { getMarkAtomMap } from '../common/atom'; import { recordLastest } from '../common/blockScope'; +import { chooseProxyVal, chooseVal, newOpParams } from '../common/util'; import type { ParsedOptions } from './parse'; /** @@ -15,7 +16,7 @@ import type { ParsedOptions } from './parse'; */ export function buildSharedState(options: ParsedOptions) { let sharedState: any = {}; - const { rawState, sharedKey, deep, forAtom } = options; + const { rawState, sharedKey, deep, forAtom, onRead } = options; const collectDep = (valKey: string, keyPath: string[], val: any) => { const depKey = prefixValKey(valKey, sharedKey); // using shared state in derived/watch callback @@ -29,8 +30,13 @@ export function buildSharedState(options: ParsedOptions) { customKeys: [IS_ATOM as symbol], customGet: () => forAtom, onOperate: (params) => { - const { isBuiltInFnKey, fullKeyPath } = params; - !isBuiltInFnKey && collectDep(fullKeyPath.join(KEY_SPLITER), fullKeyPath, params.value); + const { isBuiltInFnKey } = params; + if (!isBuiltInFnKey) { + const { fullKeyPath, value, proxyValue } = params; + const { proxyVal, rawVal } = chooseProxyVal(onRead(params), proxyValue, value); + collectDep(fullKeyPath.join(KEY_SPLITER), fullKeyPath, rawVal); + return proxyVal; + } }, }); } else { @@ -48,8 +54,9 @@ export function buildSharedState(options: ParsedOptions) { if (isSymbol(key)) { return val; } - collectDep(key, [key], val); - return val; + const finalVar = chooseVal(onRead(newOpParams(key, val, false)), val); + collectDep(key, [key], finalVar); + return finalVar; }, }); } diff --git a/packages/helux-core/src/factory/creator/loading.ts b/packages/helux-core/src/factory/creator/loading.ts index 9164aba7..d3f563ba 100644 --- a/packages/helux-core/src/factory/creator/loading.ts +++ b/packages/helux-core/src/factory/creator/loading.ts @@ -146,7 +146,9 @@ export function initLoadingCtx(createFn: Fn, options: IInitLoadingCtxOpt) { if (isUserState && NONE !== loadingMode) { useLoading = () => { const loadingProxy = getLoadingInfo(createFn, options).loadingProxy; - const { proxyState, internal, extra, renderInfo } = useSharedLogic(apiCtx, loadingProxy); + const { + insCtx: { proxyState, internal, extra, renderInfo }, + } = useSharedLogic(apiCtx, loadingProxy); // 注意此处用实例的 extra 记录 safeLoading,实例存在期间 safeLoading 创建一次后会被后续一直复用 return [createSafeLoading(extra, proxyState, from), internal.setState, renderInfo]; }; diff --git a/packages/helux-core/src/factory/creator/mutateDeep.ts b/packages/helux-core/src/factory/creator/mutateDeep.ts index cd555545..e00e2ed4 100644 --- a/packages/helux-core/src/factory/creator/mutateDeep.ts +++ b/packages/helux-core/src/factory/creator/mutateDeep.ts @@ -1,6 +1,6 @@ import { isJsObj, isObj, noop } from '@helux/utils'; import { createDraft, finishDraft, limuUtils } from 'limu'; -import type { Dict, IInnerSetStateOptions } from '../../types/base'; +import type { Dict, Ext, IInnerSetStateOptions } from '../../types/base'; import { genRenderSN } from '../common/key'; import { runMiddlewares } from '../common/middleware'; import { emitDataChanged } from '../common/plugin'; @@ -25,7 +25,7 @@ interface ISnCommitOpts extends ICommitOpts { sn: number; } -function handlePartial(opts: any) { +function handlePartial(opts: Ext<{ mutateCtx: IMutateCtx }>) { const { partial, forAtom, mutateCtx, isPrimitive, draftRoot, draftNode } = opts; // 把深依赖和浅依赖收集到的 keys 合并起来 if (!isObj(partial)) { diff --git a/packages/helux-core/src/factory/creator/notify.ts b/packages/helux-core/src/factory/creator/notify.ts index 02988fe4..de9a794f 100644 --- a/packages/helux-core/src/factory/creator/notify.ts +++ b/packages/helux-core/src/factory/creator/notify.ts @@ -18,10 +18,14 @@ function updateIns(insCtxMap: InsCtxMap, insKey: number, sn: number) { } } +/** + * 相关依赖函数执行(render渲染函数,derive派生函数,watch观察函数) + */ export function execDepFns(opts: ICommitStateOptions) { const { mutateCtx, internal, desc, isFirstCall, from, sn } = opts; - const { ids, globalIds, depKeys, triggerReasons, isDictInfo } = mutateCtx; + const { ids, globalIds, depKeys, triggerReasons } = mutateCtx; const { key2InsKeys, id2InsKeys, insCtxMap, rootValKey } = internal; + console.log('depKeys ', depKeys); internal.ver += 1; // find associate ins keys @@ -36,39 +40,38 @@ export function execDepFns(opts: ICommitStateOptions) { markFnEnd(); } - const analyzeDepKey = (key: string, skipFindIns?: boolean) => { + const analyzeDepKey = (key: string) => { // 值相等就忽略 if (!diffVal(internal, key)) { return; } - if (!skipFindIns) { - const insKeys = key2InsKeys[key] || []; - let validInsKeys: number[] = insKeys; + const insKeys = key2InsKeys[key] || []; + const validInsKeys: number[] = []; + for (const insKey of insKeys) { + if (allInsKeys.includes(insKey)) { + continue; + } + const insCtx = insCtxMap.get(insKey); + if (!insCtx) { + continue; + } + const depKeys = insCtx.getDeps(); - // TODO 支持 compareDict 设置 - // 值为字典对象 {},对比 depKey 相关子路径依赖值是否真的发生变化 - if (isDictInfo[key]) { - // 重置 validInsKeys,按节点变化去过滤出目标 ins - validInsKeys = []; - for (const insKey of insKeys) { - if (allInsKeys.includes(insKey)) { - continue; - } - const insCtx = insCtxMap.get(insKey); - if (!insCtx) { - continue; - } - const depKeys = insCtx.renderInfo.getDeps(); - if (hasChangedNode(internal, depKeys, key)) { - validInsKeys.push(insKey); - } + // 未对 useState useAtom 返回值有任何读操作时 + if (depKeys[0] === rootValKey) { + if (diffVal(internal, rootValKey)) { + validInsKeys.push(insKey); } + continue; } - allInsKeys = allInsKeys.concat(validInsKeys); + if (hasChangedNode(internal, depKeys, key)) { + validInsKeys.push(insKey); + } } + allInsKeys = allInsKeys.concat(validInsKeys); const { firstLevelFnKeys, asyncFnKeys } = getDepFnStats(internal, key, runCountStats); allFirstLevelFnKeys = allFirstLevelFnKeys.concat(firstLevelFnKeys); allAsyncFnKeys = allAsyncFnKeys.concat(asyncFnKeys); @@ -79,7 +82,7 @@ export function execDepFns(opts: ICommitStateOptions) { // 因这里补上 rootValKey 仅为了查 watch derive 函数,故刻意传递 skipFindIns = true 跳过 ins 查询 // 否则会导致不该更新的实例也触发更新了,影响精确更新结果 if (!depKeys.includes(rootValKey)) { - analyzeDepKey(rootValKey, true); + analyzeDepKey(rootValKey); } // clear cached diff result clearDiff(); @@ -100,7 +103,7 @@ export function execDepFns(opts: ICommitStateOptions) { allAsyncFnKeys.forEach((fnKey) => markComputing(fnKey, runCountStats[fnKey])); allFirstLevelFnKeys.forEach((fnKey) => runFn(fnKey, { sn, from, triggerReasons, internal, desc, isFirstCall })); - // start update + // start trigger rerender allInsKeys.forEach((insKey) => updateIns(insCtxMap, insKey, sn)); // start update globalId ins if (globalInsKeys.length) { diff --git a/packages/helux-core/src/factory/creator/operateState.ts b/packages/helux-core/src/factory/creator/operateState.ts index b20f612b..57cf6130 100644 --- a/packages/helux-core/src/factory/creator/operateState.ts +++ b/packages/helux-core/src/factory/creator/operateState.ts @@ -1,12 +1,12 @@ import { matchDictKey, nodupPush, prefixValKey } from '@helux/utils'; -import { IOperateParams, limuUtils } from 'limu'; +import { IOperateParams } from 'limu'; import type { KeyIdsDict, NumStrSymbol } from '../../types/base'; import { cutDepKeyByStop } from '../common/stopDep'; import { getDepKeyByPath, IMutateCtx } from '../common/util'; import type { TInternal } from './buildInternal'; export function handleOperate(opParams: IOperateParams, opts: { internal: TInternal; mutateCtx: IMutateCtx }) { - const { isChange, fullKeyPath, keyPath, parentType, value } = opParams; + const { isChange, fullKeyPath, keyPath, parentType } = opParams; const { internal, mutateCtx } = opts; const { arrKeyDict } = mutateCtx; if (!isChange) { @@ -20,7 +20,7 @@ export function handleOperate(opParams: IOperateParams, opts: { internal: TInter // const oldVal = getVal(internal.snap, fullKeyPath); // if (opParams.value === oldVal) return; const { moduleName, exact, sharedKey, ruleConf, level1ArrKeys } = internal; - const { writeKeyPathInfo, ids, globalIds, writeKeys, isDictInfo } = mutateCtx; + const { writeKeyPathInfo, ids, globalIds, writeKeys } = mutateCtx; mutateCtx.level1Key = fullKeyPath[0]; mutateCtx.handleAtomCbReturn = false; @@ -34,7 +34,6 @@ export function handleOperate(opParams: IOperateParams, opts: { internal: TInter const { idsDict, globalIdsDict, stopDepInfo } = ruleConf; const writeKey = getDepKeyByPath(fullKeyPath, sharedKey); writeKeyPathInfo[writeKey] = { sharedKey, moduleName, keyPath: fullKeyPath }; - isDictInfo[writeKey] = limuUtils.isObject(value); // 设定了非精确更新策略时,提取出第一层更新路径即可 if (!exact) { diff --git a/packages/helux-core/src/factory/creator/parse.ts b/packages/helux-core/src/factory/creator/parse.ts index 881839da..88e7a15a 100644 --- a/packages/helux-core/src/factory/creator/parse.ts +++ b/packages/helux-core/src/factory/creator/parse.ts @@ -153,6 +153,7 @@ export function parseOptions(innerOptions: IInnerOptions, options: ICreateOption const rules = options.rules || []; const before = options.before || noop; const mutate = options.mutate || noop; + const onRead = options.onRead || noop; // 后续 parseRules 步骤会转 stopArrDep stopDepth 到 stopDepInfo 上 const stopArrDep = options.stopArrDep ?? true; const stopDepth = options.stopDepth || STOP_DEPTH; @@ -179,6 +180,7 @@ export function parseOptions(innerOptions: IInnerOptions, options: ICreateOption before, mutate, mutateFnDict, + onRead, stateType, loadingMode, stopArrDep, diff --git a/packages/helux-core/src/helpers/insCtx.ts b/packages/helux-core/src/helpers/insCtx.ts index d79c6fa4..0d50174c 100644 --- a/packages/helux-core/src/helpers/insCtx.ts +++ b/packages/helux-core/src/helpers/insCtx.ts @@ -1,8 +1,9 @@ import { delListItem, isFn, isSymbol, nodupPush, prefixValKey, warn } from '@helux/utils'; import { immut, limuUtils } from 'limu'; -import { EXPIRE_MS, IS_DERIVED_ATOM, KEY_SPLITER, NOT_MOUNT, RENDER_END, RENDER_START } from '../consts'; +import { ARR, DICT, EXPIRE_MS, IS_DERIVED_ATOM, KEY_SPLITER, NOT_MOUNT, OTHER, RENDER_END, RENDER_START } from '../consts'; import { genInsKey } from '../factory/common/key'; import { cutDepKeyByStop, recordArrKey } from '../factory/common/stopDep'; +import { chooseProxyVal, chooseVal, newOpParams } from '../factory/common/util'; import type { InsCtxDef } from '../factory/creator/buildInternal'; import { mapGlobalId } from '../factory/creator/globalId'; import type { Dict, Ext, IFnCtx, IUseSharedStateOptions } from '../types/base'; @@ -13,8 +14,18 @@ import { createOb } from './obj'; import { getInternal } from './state'; const { isObject: isDict } = limuUtils; -const DICT = 'Object'; // 来自 limu 的字典类型表达 -const OTHER = 'Other'; + +function collectDep(insCtx: InsCtxDef, info: DepKeyInfo, parentType: string, value: any) { + if (!insCtx.canCollect) { + // 无需收集依赖 + return; + } + const isValArr = Array.isArray(value); + if (isValArr) { + recordArrKey(insCtx.internal.level1ArrKeys, info.depKey); + } + insCtx.recordDep(info, parentType, isValArr); +} export function runInsUpdater(insCtx: InsCtxDef | undefined) { if (!insCtx) return; @@ -22,31 +33,23 @@ export function runInsUpdater(insCtx: InsCtxDef | undefined) { if (mountStatus === NOT_MOUNT && Date.now() - createTime > EXPIRE_MS) { return clearDep(insCtx); } - updater(); } export function attachInsProxyState(insCtx: InsCtxDef) { - const { internal, recordDep } = insCtx; - const { rawState, isDeep, level1ArrKeys, sharedKey } = internal; - - const collectDep = (info: DepKeyInfo, parentType: string, value: any) => { - if (!insCtx.canCollect) { - // 无需收集依赖 - return; - } - if (Array.isArray(value)) { - recordArrKey(level1ArrKeys, info.depKey); - } - recordDep(info, parentType); - }; - + const { internal } = insCtx; + const { rawState, isDeep, sharedKey, onRead } = internal; if (isDeep) { insCtx.proxyState = immut(rawState, { - onOperate: ({ isBuiltInFnKey, fullKeyPath, keyPath, parentType, value }) => { - if (isBuiltInFnKey) return; + onOperate: (opParams) => { + if (opParams.isBuiltInFnKey) return; + const { fullKeyPath, keyPath, parentType, value, proxyValue } = opParams; + // 触发用户定义的钩子函数 + const { proxyVal, rawVal } = chooseProxyVal(onRead(opParams), proxyValue, value); const depKey = prefixValKey(fullKeyPath.join(KEY_SPLITER), sharedKey); - collectDep({ depKey, keyPath: fullKeyPath, parentKeyPath: keyPath, sharedKey }, parentType, value); + const depKeyInfo = { depKey, keyPath: fullKeyPath, parentKeyPath: keyPath, sharedKey }; + collectDep(insCtx, depKeyInfo, parentType, rawVal); + return proxyVal; }, compareVer: true, }); @@ -57,28 +60,31 @@ export function attachInsProxyState(insCtx: InsCtxDef) { return true; }, get: (target: Dict, key: string) => { + const value = target[key]; if (isSymbol(key)) { - return target[key]; + return value; } + + const finalVal = chooseVal(onRead(newOpParams(key, value, false)), value); const depKey = prefixValKey(key, sharedKey); - const value = target[key]; const parentType = isDict(target) ? DICT : OTHER; - collectDep({ depKey, keyPath: [key], sharedKey }, parentType, value); - return value; + collectDep(insCtx, { depKey, keyPath: [key], sharedKey }, parentType, finalVal); + return finalVal; }, }); } } export function buildInsCtx(options: Ext): InsCtxDef { - const { updater, sharedState, id = '', globalId = '', collectType = 'every', deps, pure = false } = options; + const { updater, sharedState, id = '', globalId = '', collectType = 'every', deps, pure = false, arrDep = true } = options; + const arrIndexDep = !arrDep ? true : options.arrIndexDep ?? true; const internal = getInternal(sharedState); if (!internal) { throw new Error('ERR_OBJ_NOT_SHARED: input object is not a result returned by share api'); } const insKey = genInsKey(); - const { rawState, isDeep, ver, ruleConf, level1ArrKeys, forAtom } = internal; + const { rawState, isDeep, ver, ruleConf, level1ArrKeys, forAtom, sharedKey, sharedKeyStr, snap } = internal; const { stopDepInfo } = ruleConf; const insCtx: InsCtxDef = { readMap: {}, @@ -98,7 +104,7 @@ export function buildInsCtx(options: Ext): InsCtxDef { mountStatus: NOT_MOUNT, renderStatus: RENDER_START, createTime: Date.now(), - atomVal: null, + rootVal: null, ver, id, globalId, @@ -112,13 +118,16 @@ export function buildInsCtx(options: Ext): InsCtxDef { }, /** 记录一些需复用的中间生成的数据 */ extra: {}, + getDeps: () => insCtx.currentDepKeys, renderInfo: { sn: 0, - getDeps: () => insCtx.currentDepKeys, + snap, + insKey, + getDeps: () => insCtx.currentDepKeys.slice(), // depKeys 的后续更新流程在 helpers/insDep.resetReadMap 和 updateDep 函数里,做了双保险备份 - getPrevDeps: () => insCtx.depKeys, + getPrevDeps: () => insCtx.depKeys.slice(), }, - recordDep: (depKeyInfo: DepKeyInfo, parentType?: string) => { + recordDep: (depKeyInfo: DepKeyInfo, parentType?: string, isValArr?: boolean) => { let depKey = depKeyInfo.depKey; // depKey 可能因为配置了 rules[]stopDep 的关系被 recordCb 改写 cutDepKeyByStop(depKeyInfo, { @@ -128,34 +137,49 @@ export function buildInsCtx(options: Ext): InsCtxDef { depKey = key; }, }); - const { readMap, insKey, renderStatus, currentDepKeys, delReadMap } = insCtx; + const { renderStatus } = insCtx; if (renderStatus === RENDER_END) { return; } + const { readMap, insKey, currentDepKeys, delReadMap } = insCtx; + + // record watch dep + // 支持 useWatch 的 deps 函数直接传入 useShared 返回的 state 作为依赖项传入 + fnDep.recordFnDepKeys([depKey], {}); + const doRecord = () => { + nodupPush(currentDepKeys, depKey); + // 注意 depKey 对应的 insKeys,和 insKey->insCtx.depKeys 记录是不对称的 + // 即 depKey a, a.b, a.b.c 都会记录 insKey 1 + // 但 insKey 1 在 pure 模式下 depKeys 就只有 a.b.c + internal.recordDep(depKey, insKey); + readMap[depKey] = 1; + }; // 还未被记录,也未被标记删除 if (!readMap[depKey] && !delReadMap[depKey]) { // pure 模式下针对字典只记录最长路径依赖 if (pure && parentType === DICT) { - const parentKeyPath = depKeyInfo.parentKeyPath || []; - const parentDepKey = prefixValKey(parentKeyPath.join(KEY_SPLITER), internal.sharedKey); + const { parentKeyPath } = depKeyInfo; + // 无 parentKeyPath 的话就是dict根对象自身,此时 parentDepKey 指向 sharedKey + const isValidPath = parentKeyPath && parentKeyPath.length; + const parentDepKey = isValidPath ? prefixValKey(parentKeyPath.join(KEY_SPLITER), sharedKey) : sharedKeyStr; if (readMap[parentDepKey]) { delete readMap[parentDepKey]; delReadMap[parentDepKey] = 1; delListItem(currentDepKeys, parentDepKey); } } - nodupPush(currentDepKeys, depKey); - // 注意 depKey 对应的 insKey,和 insKey->insCtx.depKeys 记录是不对称的 - // 即 depKey a, a.b, a.b.c 都会记录 insKey 1 - // 但 insKey 1 在 pure 模式下 depKeys 就只有 a.b.c - internal.recordDep(depKey, insKey); - readMap[depKey] = 1; - } - // record watch dep - // 支持 useWatch 的 deps 函数直接传入 useShared 返回的 state 作为依赖项传入 - fnDep.recordFnDepKeys([depKey], {}); + const isParentArr = parentType === ARR; + if (isParentArr) { + arrIndexDep && doRecord(); + return; + } + // 值是数组时,开启了 arrDep 才记录 + if (!isValArr || (!isParentArr && arrDep)) { + doRecord(); + } + } }, }; globalId && mapGlobalId(globalId, insKey); diff --git a/packages/helux-core/src/helpers/insDep.ts b/packages/helux-core/src/helpers/insDep.ts index 42b6f6ba..1c520cbd 100644 --- a/packages/helux-core/src/helpers/insDep.ts +++ b/packages/helux-core/src/helpers/insDep.ts @@ -38,31 +38,25 @@ export function clearDep(insCtx: InsCtxDef) { } /** - * 每轮渲染完毕的 effect 里触发更新依赖更新 + * 每轮渲染完毕的 effect 里触发依赖数据更新 */ export function updateDep(insCtx: InsCtxDef) { - const { insKey, readMap, readMapPrev, internal, canCollect, pure, currentDepKeys } = insCtx; + const { canCollect, isFirstRender, currentDepKeys } = insCtx; // 标记了不能收集依赖,则运行期间不做更新依赖的动作 if (!canCollect) { - insCtx.depKeys = currentDepKeys.slice(); + if (isFirstRender) { + insCtx.depKeys = currentDepKeys.slice(); + } return; } - if (!pure) { - Object.keys(readMapPrev).forEach((prevKey) => { - if (!readMap[prevKey]) { - // lost dep - internal.delDep(prevKey, insKey); - } - }); - } insCtx.depKeys = currentDepKeys.slice(); insCtx.readMapStrict = null; } /** - * 重置读依赖 map + * 重置记录读依赖需要的辅助数据 */ -export function resetReadMap(insCtx: InsCtxDef) { +export function resetDepHelpData(insCtx: InsCtxDef) { const { readMap, readMapStrict, canCollect } = insCtx; // 标记了不能收集依赖,则运行期间不做重置依赖的动作 if (!canCollect) { @@ -74,6 +68,7 @@ export function resetReadMap(insCtx: InsCtxDef) { insCtx.readMapPrev = readMap; insCtx.readMapStrict = readMap; insCtx.readMap = {}; // reset read map + insCtx.delReadMap = {}; insCtx.depKeys = insCtx.currentDepKeys.slice(); insCtx.currentDepKeys.length = 0; } else { diff --git a/packages/helux-core/src/hooks/common/shared.ts b/packages/helux-core/src/hooks/common/shared.ts index 2aa8ff07..6c418878 100644 --- a/packages/helux-core/src/hooks/common/shared.ts +++ b/packages/helux-core/src/hooks/common/shared.ts @@ -1,34 +1,55 @@ +import { DICT } from '../../consts'; import { isAtom } from '../../factory/common/atom'; import type { InsCtxDef, TInternal } from '../../factory/creator/buildInternal'; import { delGlobalId, mapGlobalId } from '../../factory/creator/globalId'; import { attachInsProxyState } from '../../helpers/insCtx'; import { clearDep, recoverDep } from '../../helpers/insDep'; import { getInternal } from '../../helpers/state'; -import type { Dict } from '../../types/base'; +import type { Dict, Fn, IInsRenderInfo } from '../../types/base'; /** - * let code beblow works; + * let code below works + * ```ts * const [dict] = useAtom(dictAtom); * useWatch(()=>{}, ()=>[dict]); + * + * const [state] = useShared(dictShared); + * useWatch(()=>{}, ()=>[state]); + * ``` */ -const atomValMap = new Map(); -window.ww = atomValMap; +const rootValMap = new Map(); -export function recordAtomVal(insCtx: InsCtxDef) { - if (insCtx.isFirstRender && insCtx.internal.forAtom) { - insCtx.atomVal = insCtx.proxyState.val; +/** + * 记录一些必要的辅助数据,返回 useAtom useShared 需要的元组数据 + */ +export function prepareTuple(insCtx: InsCtxDef, forAtom?: boolean): [any, Fn, IInsRenderInfo] { + const { proxyState, internal, renderInfo } = insCtx; + const { sharedKey, sharedKeyStr, setDraft } = internal; + renderInfo.snap = internal.snap; + // atom 自动拆箱,注意这里 proxyState.val 已触发记录根值依赖 + const rootVal = forAtom ? proxyState.val : proxyState; + // 首次渲染时,记录一下 rootVal + if (insCtx.isFirstRender) { + // ATTENTION:这里提前触发一次 .val 根值依赖记录 + insCtx.rootVal = rootVal; // 如果 val 是原始值,多个相同的值会覆盖,造成 useWatch 判断失误 // 这里会写到文档的常见使用错误里,警示作者避免直接传递原始值给 useWatch deps 函数 - atomValMap.set(insCtx.atomVal, insCtx.internal); + rootValMap.set(insCtx.rootVal, internal); } + if (!forAtom) { + // 记录一次根值依赖,让未对 useAtom useShared 返回值有任何读操作的组件也响应更新 + insCtx.recordDep({ depKey: sharedKeyStr, keyPath: [], sharedKey }, DICT); + } + + return [rootVal, setDraft, renderInfo]; } -export function delAtomVal(val: any) { - atomValMap.delete(val); +export function delRootVal(val: any) { + rootValMap.delete(val); } -export function getAtomValInternal(val: any): TInternal | undefined { - return atomValMap.get(val); +export function getRootValInternal(val: any): TInternal | undefined { + return rootValMap.get(val); } export function checkAtom(mayAtom: any, forAtom?: boolean) { @@ -43,7 +64,7 @@ export function checkStateVer(insCtx: InsCtxDef) { internal: { ver: dataVer }, } = insCtx; if (ver !== dataVer) { - // 替换 proxyState,让把共享对象透传给 memo 组件的场景也能正常触发重新渲染 + // 替换 proxyState,让把共享对象透传给 memo 组件、useEffect deps 的场景也能正常触发重新渲染 insCtx.ver = dataVer; attachInsProxyState(insCtx); } diff --git a/packages/helux-core/src/hooks/common/useSharedLogic.ts b/packages/helux-core/src/hooks/common/useSharedLogic.ts index ca73b521..3e6ad504 100644 --- a/packages/helux-core/src/hooks/common/useSharedLogic.ts +++ b/packages/helux-core/src/hooks/common/useSharedLogic.ts @@ -1,11 +1,12 @@ +import { Fn } from 'helux'; import { MOUNTED, RENDER_END, RENDER_START } from '../../consts'; import type { InsCtxDef } from '../../factory/creator/buildInternal'; import { buildInsCtx } from '../../helpers/insCtx'; -import { resetReadMap, updateDep } from '../../helpers/insDep'; +import { resetDepHelpData, updateDep } from '../../helpers/insDep'; import { getInternal } from '../../helpers/state'; import type { CoreApiCtx } from '../../types/api-ctx'; -import type { Dict, IInnerUseSharedOptions } from '../../types/base'; -import { checkAtom, checkStateVer, delAtomVal, delInsCtx, isSharedKeyChanged, recordAtomVal, recoverInsCtx } from './shared'; +import type { Dict, IInnerUseSharedOptions, IInsRenderInfo } from '../../types/base'; +import { checkAtom, checkStateVer, delInsCtx, delRootVal, isSharedKeyChanged, prepareTuple, recoverInsCtx } from './shared'; import { useSync } from './useSync'; // for skip ts check out of if block @@ -15,8 +16,9 @@ const nullInsCtx = null as unknown as InsCtxDef; * 生成组件对应的上下文 */ function useInsCtx(apiCtx: CoreApiCtx, sharedState: T, options: IInnerUseSharedOptions): InsCtxDef { - const updater = apiCtx.hookImpl.useForceUpdate(); - const ctxRef = apiCtx.react.useRef<{ ctx: InsCtxDef }>({ ctx: nullInsCtx }); + const { hookImpl, react } = apiCtx; + const updater = hookImpl.useForceUpdate(); + const ctxRef = react.useRef<{ ctx: InsCtxDef }>({ ctx: nullInsCtx }); // start build or rebuild ins ctx let insCtx = ctxRef.current.ctx; if (!insCtx || isSharedKeyChanged(insCtx, sharedState)) { @@ -32,7 +34,7 @@ function useInsCtx(apiCtx: CoreApiCtx, sharedState: T, options: IInner */ function useClearEffect(apiCtx: CoreApiCtx, insCtx: InsCtxDef) { apiCtx.react.useEffect(() => { - delAtomVal(insCtx.atomVal); + delRootVal(insCtx.rootVal); // 设定了 options.collect='first' 则首轮渲染结束后标记不能再收集依赖,阻值后续新的渲染流程里继续收集依赖的行为 if (insCtx.collectType === 'first') { insCtx.canCollect = false; @@ -51,7 +53,7 @@ function useClearEffect(apiCtx: CoreApiCtx, insCtx: InsCtxDef) { */ function useCollectDep(apiCtx: CoreApiCtx, sharedState: T, insCtx: InsCtxDef, options: IInnerUseSharedOptions) { insCtx.renderStatus = RENDER_START; - resetReadMap(insCtx); + resetDepHelpData(insCtx); // adapt to react 18 useSync(apiCtx, insCtx.subscribe, () => getInternal(sharedState).snap); @@ -79,12 +81,18 @@ export function useSharedSimpleLogic( return insCtx; } -export function useSharedLogic(apiCtx: CoreApiCtx, sharedState: T, options: IInnerUseSharedOptions = {}): InsCtxDef { - checkAtom(sharedState, options.forAtom); +export function useSharedLogic( + apiCtx: CoreApiCtx, + sharedState: T, + options: IInnerUseSharedOptions = {}, +): { tuple: [any, Fn, IInsRenderInfo]; insCtx: InsCtxDef } { + const { forAtom } = options; + checkAtom(sharedState, forAtom); const insCtx = useInsCtx(apiCtx, sharedState, options); useCollectDep(apiCtx, sharedState, insCtx, options); useClearEffect(apiCtx, insCtx); checkStateVer(insCtx); - recordAtomVal(insCtx); - return insCtx; + const tuple = prepareTuple(insCtx, forAtom); + + return { tuple, insCtx }; } diff --git a/packages/helux-core/src/hooks/useShared.ts b/packages/helux-core/src/hooks/useShared.ts index d77b373f..d0f7dce5 100644 --- a/packages/helux-core/src/hooks/useShared.ts +++ b/packages/helux-core/src/hooks/useShared.ts @@ -7,9 +7,8 @@ export function useShared( sharedState: T, options: IUseSharedStateOptions = {}, ): [T, SetState, IRenderInfo] { - const insCtx = useSharedLogic(apiCtx, sharedState, options); - const { proxyState, internal, renderInfo } = insCtx; - return [proxyState, internal.setState, renderInfo]; + const { tuple } = useSharedLogic(apiCtx, sharedState, options); + return tuple; } export function useAtom( @@ -17,7 +16,6 @@ export function useAtom( sharedState: T, options: IUseSharedStateOptions = {}, ): [T, SetAtom, IRenderInfo] { - const insCtx = useSharedLogic(apiCtx, sharedState, { ...options, forAtom: true }); - const { proxyState, internal, renderInfo } = insCtx; - return [proxyState.val, internal.setAtom, renderInfo]; + const { tuple } = useSharedLogic(apiCtx, sharedState, { ...options, forAtom: true }); + return tuple; } diff --git a/packages/helux-core/src/types/api.d.ts b/packages/helux-core/src/types/api.d.ts index 9fec9b79..dcac7860 100644 --- a/packages/helux-core/src/types/api.d.ts +++ b/packages/helux-core/src/types/api.d.ts @@ -37,6 +37,7 @@ import type { IAtomCtx, IBlockOptions, ICreateOptions, + IInsRenderInfo, IPlugin, IRenderInfo, IRunMutateOptions, @@ -230,7 +231,7 @@ export function watch(watchFn: (fnParams: IWatchFnParams) => void, options?: Wat * const [ obj, setObj ] = useShared(sharedObj); * ``` */ -export function useShared(sharedObject: T, options?: IUseSharedStateOptions): [SharedDict, SetState, IRenderInfo]; +export function useShared(sharedObject: T, options?: IUseSharedStateOptions): [SharedDict, SetState, IInsRenderInfo]; /** * 组件使用 atom,注此接口只接受 atom 生成的对象,如传递 share 生成的对象会报错 @@ -250,7 +251,7 @@ export function useShared(sharedObject: T, options?: IUseSharedStateOp * }); * ``` */ -export function useAtom(sharedState: Atom, options?: IUseSharedStateOptions): [T, SetAtom, IRenderInfo]; +export function useAtom(sharedState: Atom, options?: IUseSharedStateOptions): [T, SetAtom, IInsRenderInfo]; /** * 使用普通对象,需注意此接口只接受普通对象 @@ -400,13 +401,13 @@ export function useStable(data: T): T; * loading['whatever-key']; // 均能返回 status 对象,对于不存在的 mutate key,返回的 status 不变 * ``` */ -export function useMutateLoading(target?: T): [SafeLoading, SetState, IRenderInfo]; +export function useMutateLoading(target?: T): [SafeLoading, SetState, IInsRenderInfo]; /** 组件外部读取 Mutate loading */ export function getMutateLoading(target?: T): SafeLoading; /** 组件外部读取 Action loading */ -export function useActionLoading(target?: T): [SafeLoading, SetState, IRenderInfo]; +export function useActionLoading(target?: T): [SafeLoading, SetState, IInsRenderInfo]; /** 组件外部读取 loading */ export function getActionLoading(target?: T): SafeLoading; diff --git a/packages/helux-core/src/types/base.d.ts b/packages/helux-core/src/types/base.d.ts index 98047d99..5f29d55b 100644 --- a/packages/helux-core/src/types/base.d.ts +++ b/packages/helux-core/src/types/base.d.ts @@ -1,4 +1,5 @@ import type { ForwardedRef, FunctionComponent, PropsWithChildren, ReactNode } from '@helux/types'; +import type { IOperateParams } from 'limu'; import type { DepKeyInfo } from './inner'; /** @@ -446,15 +447,15 @@ export interface ISharedCtx = ICrea setState: SetState; sync: SyncFnBuilder; syncer: Syncer; - useState: (options?: IUseSharedStateOptions) => [T, SetState, IRenderInfo]; + useState: (options?: IUseSharedStateOptions) => [T, SetState, IInsRenderInfo]; /** 获取 Mutate 状态 */ getMutateLoading: () => SafeLoading; /** 使用 Mutate 状态 */ - useMutateLoading: () => [SafeLoading, SetState, IRenderInfo]; + useMutateLoading: () => [SafeLoading, SetState, IInsRenderInfo]; /** 获取 Action 状态 */ getActionLoading: () => SafeLoading; /** 使用 Action 状态 */ - useActionLoading: () => [SafeLoading, SetState, IRenderInfo]; + useActionLoading: () => [SafeLoading, SetState, IInsRenderInfo]; } export interface IAtomCtx = IAtomCreateOptions> { @@ -467,15 +468,15 @@ export interface IAtomCtx = IAtomCreate setState: SetAtom; sync: AtomSyncFnBuilder; syncer: AtomSyncer; - useState: (options?: IUseSharedStateOptions) => [T, SetAtom, IRenderInfo]; + useState: (options?: IUseSharedStateOptions) => [T, SetAtom, IInsRenderInfo]; /** 获取 Mutate 状态 */ getMutateLoading: () => AtomSafeLoading; /** 使用 Mutate 状态 */ - useMutateLoading: () => [AtomSafeLoading, SetState, IRenderInfo]; + useMutateLoading: () => [AtomSafeLoading, SetState, IInsRenderInfo]; /** 获取 Action 状态 */ getActionLoading: () => AtomSafeLoading; /** 使用 Action 状态 */ - useActionLoading: () => [AtomSafeLoading, SetState, IRenderInfo]; + useActionLoading: () => [AtomSafeLoading, SetState, IInsRenderInfo]; setAtomVal: (val: T) => void; } @@ -578,6 +579,11 @@ export interface ICreateOptionsBaseFull { * 配置状态变更联动视图更新规则 */ rules: IDataRule[]; + /** + * 暴露给开发者使用的钩子函数,所有值读取操作均触发此钩子函数, + * 如果读操作返回了具体指,则会透传给用户,这是一个危险的操作,用户需自己为此负责 + */ + onRead: (opParams: IOperateParams) => any; } export interface ICreateOptionsFull extends ICreateOptionsBaseFull { @@ -629,11 +635,11 @@ export interface IUseSharedStateOptions { */ id?: NumStrSymbol; /** - * default: false,是否以 pure 模式使用状态 + * default: false,是否以 pure 模式使用状态,此参数只影响字典数据的依赖收集规则 * ``` * 1 为 false,表示状态不只是用于当前组件ui渲染,还会透传给 memo 的子组件,透传给 useEffect 依赖数组, - * 此模式下会收集中间态依赖,不丢弃记录过的字典依赖 - * 2 为 true,表示状态仅用于当前组件ui渲染,此模式下不会收集中间态依赖,只记录最长路径依赖 + * 此模式下会收集中间态字典依赖,不丢弃记录过的字典依赖 + * 2 为 true,表示状态仅用于当前组件ui渲染,此模式下不会收集中间态字典依赖,只记录字典最长依赖 * ``` * 组件 Demo 使用示例 * ```ts @@ -675,6 +681,42 @@ export interface IUseSharedStateOptions { * 此时组件的依赖是 deps 返回依赖和渲染完毕收集到的依赖合集 */ deps?: (readOnlyState: T) => any[] | void; + /** + * default: true,是否记录数组自身依赖,当确认是孩子组件自己读数组下标渲染的场景,可设置为 false, + * 这样数组被重置时不会触发重渲染 + * ```ts + * // true: 记录数组自身依赖 + * const [ dict ] = useAtom(dictAtom); + * // 此时依赖是 dict, dict.list[0] + * dict.list[0]; + * // 重置 list,引发当前组件重渲染 + * setDictAtom(draft=> draft.list = draft.list.slice()); + * + * // false: 不记录数组自身依赖,适用于孩子组件自己读数组下标渲染的场景 + * const [ dict ] = useAtom(dictAtom, { arrDep: false }); + * // 此时依赖是 dict.list[0] + * dict.list[0]; + * // 重置 list,不会引发当前组件重渲染 + * setDictAtom(draft=> draft.list = draft.list.slice()); + * ``` + */ + arrDep?: boolean; + /** + * default: true,是否记录数组下标依赖,当通过循环数组生成孩子的场景,可设置为 false,减少组件自身的依赖记录数量, + * 此参数在 arrDep=true 时设置有效,arrDep=false 时,arrIndexDep 被自动强制设为 true + * + * ```ts + * arrDep=true arrIndexDep = true + * deps: list list[0] list[...] + * + * arrDep=true arrIndexDep = false + * deps: list + * + * arrDep=false + * deps: list[0] list[...] + * ``` + */ + arrIndexDep?: boolean; } export interface IInnerUseSharedOptions extends IUseSharedStateOptions { @@ -880,11 +922,14 @@ export interface IRenderInfo { export interface IInsRenderInfo { /** 渲染序号,多个实例拥有相同的此值表示属于同一批次被触发渲染 */ sn: number; + /** 实例 key */ + insKey: number; /** * 获取组件的当前渲染周期里收集到依赖列表,通常需要再 useEffect 里调用能获取当前渲染周期收集的所有依赖, * 如在渲染过程中直接调用获取的是正在收集中的依赖 */ getDeps: () => string[]; + snap: any; /** * 获取组件的前一次渲染周期里收集到依赖列表 */ @@ -915,7 +960,7 @@ export interface IInsCtx { rawState: Dict; sharedState: Dict; proxyState: Dict; - atomVal: any; + rootVal: any; updater: Fn; /** 未挂载 已挂载 已卸载 */ mountStatus: MountStatus; @@ -939,8 +984,9 @@ export interface IInsCtx { * 计算出的能否收集依赖标记,如透传了 options.collect=false,会在首轮渲染结束后标记为 false */ canCollect: boolean; + getDeps: IInsRenderInfo['getDeps']; renderInfo: IInsRenderInfo; - recordDep: (depKeyInfo: DepKeyInfo, parentType?: string) => void; + recordDep: (depKeyInfo: DepKeyInfo, parentType?: string, isValArr?: boolean) => void; } export type InsCtxMap = Map;