From 53660aa676161914f923e827a30b69634a038f60 Mon Sep 17 00:00:00 2001 From: fantasticsoul Date: Mon, 27 Nov 2023 12:42:09 +0800 Subject: [PATCH] build(3.4.1): automatically unbox atom at all cb logic --- doc/docs/api/atom-and-share/basic-usage.md | 2 +- doc/docs/api/atom-and-share/index.md | 22 ++- doc/docs/api/atom-and-share/shared-ctx.md | 51 ++++--- package.json | 2 +- packages/helux-core/src/api.ts | 3 + .../src/factory/common/middleware.ts | 8 +- .../helux-core/src/factory/common/util.ts | 17 ++- .../helux-core/src/factory/createAction.ts | 2 +- .../helux-core/src/factory/createShared.ts | 7 +- packages/helux-core/src/factory/createSync.ts | 2 +- .../src/factory/creator/buildInternal.ts | 2 +- .../src/factory/creator/buildShared.ts | 1 + .../src/factory/creator/commitState.ts | 4 +- .../helux-core/src/factory/creator/current.ts | 30 +++++ .../src/factory/creator/mapShared.ts | 28 ++-- .../src/factory/creator/mutateDeep.ts | 38 ++++-- .../src/factory/creator/mutateFn.ts | 14 +- .../src/factory/creator/mutateNormal.ts | 13 +- .../factory/creator/{updater.ts => notify.ts} | 2 +- .../src/factory/creator/operateState.ts | 1 + .../helux-core/src/factory/creator/parse.ts | 25 ++-- .../helux-core/src/factory/creator/sync.ts | 68 +++++++--- packages/helux-core/src/types/api.d.ts | 11 ++ packages/helux-core/src/types/base.d.ts | 126 +++++++++++------- packages/helux/__tests__/action/atom.test.ts | 4 +- packages/helux/__tests__/atom/change.test.ts | 24 ++-- packages/helux/__tests__/atom/mutate.test.ts | 12 +- .../helux/__tests__/atom/mutateAsync.test.ts | 12 +- packages/helux/__tests__/atom/render.test.tsx | 3 +- .../helux/__tests__/derive/deriveAtom.test.ts | 2 +- .../helux/__tests__/hooks/useAtom.test.ts | 8 +- packages/helux/__tests__/share/change.test.ts | 2 +- .../helux/__tests__/watch/watchAtom.test.ts | 2 +- packages/helux/src/index.ts | 4 +- 34 files changed, 361 insertions(+), 191 deletions(-) create mode 100644 packages/helux-core/src/factory/creator/current.ts rename packages/helux-core/src/factory/creator/{updater.ts => notify.ts} (98%) diff --git a/doc/docs/api/atom-and-share/basic-usage.md b/doc/docs/api/atom-and-share/basic-usage.md index 7e5bd1f2..27a9f3e0 100644 --- a/doc/docs/api/atom-and-share/basic-usage.md +++ b/doc/docs/api/atom-and-share/basic-usage.md @@ -360,7 +360,7 @@ const snap = runMutateTask(share2, 'suffixedDesc'); ```ts import { share, syncer, sync } from 'helux'; -const [sharedState,,ctx] = share({a:1}); +const [sharedState, , ctx] = share({ a: 1 }); // 基于顶层 api 创建 syncer const mySyncer = syncer(sharedState); diff --git a/doc/docs/api/atom-and-share/index.md b/doc/docs/api/atom-and-share/index.md index 97b65fc3..b37c94c8 100644 --- a/doc/docs/api/atom-and-share/index.md +++ b/doc/docs/api/atom-and-share/index.md @@ -21,31 +21,39 @@ const [numAtom, setAtom] = atom(1); ### 组件外修改 ```ts -const changeAtom = (newNum)=> setAtom(newNum); -const changeAtomByDraft = (newNum)=> setAtom(draft=>{ draft.val = newNum }); +const changeAtom = (newNum) => setAtom(newNum); +const changeAtomByDraft = (newNum) => + setAtom((draft) => { + draft.val = newNum; + }); ``` ### 可变派生 ```ts const [doubleAtom] = atom(0, { - mutate: (draft)=> { draft.val = numAtom.val *2 }, + mutate: (draft) => { + draft.val = numAtom.val * 2; + }, }); ``` ### 全量派生 ```ts -const doubuleResult = deriveAtom(()=> numAtom.val * 2 ); +const doubuleResult = deriveAtom(() => numAtom.val * 2); ``` ### 组件内使用 ```ts -function Demo(){ +function Demo() { const [num, setNum] = useAtom(numAtom); // 此处的调用和 changeAtom changeAtomByDraft 等效 - const innerChangeAtom = (newNum)=> setNum(newNum); - const innerChangeAtomByDraft = (newNum)=> setNum(draft=>{ draft.val = newNum }); + const innerChangeAtom = (newNum) => setNum(newNum); + const innerChangeAtomByDraft = (newNum) => + setNum((draft) => { + draft.val = newNum; + }); } ``` diff --git a/doc/docs/api/atom-and-share/shared-ctx.md b/doc/docs/api/atom-and-share/shared-ctx.md index 55b1eb7c..13f1df4e 100644 --- a/doc/docs/api/atom-and-share/shared-ctx.md +++ b/doc/docs/api/atom-and-share/shared-ctx.md @@ -4,7 +4,7 @@ sidebar_position: 2 # 共享上下文 -调用`atom`或`share`会返回共享上下文对象, 对象里的各个方法自动绑定了当前共享对象,让用户可以免去使用顶层api绑定的步骤 +调用`atom`或`share`会返回共享上下文对象, 对象里的各个方法自动绑定了当前共享对象,让用户可以免去使用顶层 api 绑定的步骤 ## 获取方式 @@ -12,23 +12,24 @@ sidebar_position: 2 ```ts // numCtx 即共享上下文对象 -const [ numAtom, setNum, numAtomCtx ] = atom(1); +const [numAtom, setNum, numAtomCtx] = atom(1); // sharedCtx 即共享上下文对象 -const [ sharedState, setShared, sharedCtx ] = share({a:1}); +const [sharedState, setShared, sharedCtx] = share({ a: 1 }); ``` 也可以使用`shareAtom` 和 `shareState` 接口直接返回共享上下文 ```ts const numCtx = shareAtom(1); -const sharedCtx = shareState({a:1}); +const sharedCtx = shareState({ a: 1 }); ``` -## 可映射为顶层api的方法 +## 可映射为顶层 api 的方法 ### mutate -`ctx.mutate` 映射顶层api `mutate` + +`ctx.mutate` 映射顶层 api `mutate` ```ts import { mutate } from 'helux'; @@ -40,25 +41,24 @@ mutate(numAtom)(fn); ### sync & syncer -`ctx.sync` 映射顶层api `sync`,`ctx.syncer` 映射顶层api `syncer` +`ctx.sync` 映射顶层 api `sync`,`ctx.syncer` 映射顶层 api `syncer` ```ts import { sync, syncer } from 'helux'; // 两者等效 -const infoSyncer = numAtomCtx.sync(to=>to.val.info.name); -const infoSyncer = sync(numAtomCtx)(to=>to.val.info.name); +const infoSyncer = numAtomCtx.sync((to) => to.val.info.name); +const infoSyncer = sync(numAtomCtx)((to) => to.val.info.name); // 两者等效 const nameSyncer = numAtomCtx.syncer.name; const nameSyncer = syncer(numAtom).name; ``` - ```ts // 两者等效 -const infoSyncer = sharedCtx.sync(to=>to.info.name); -const infoSyncer = sync(numAtomCtx)(to=>to.info.name); +const infoSyncer = sharedCtx.sync((to) => to.info.name); +const infoSyncer = sync(numAtomCtx)((to) => to.info.name); // 两者等效 const nameSyncer = numAtomCtx.syncer.name; @@ -67,23 +67,23 @@ const nameSyncer = syncer(numAtom).name; ### runMutate & runMutateTask -`ctx.runMutate` 映射顶层api `runMutate`,`ctx.runMutateTask` 映射顶层api `runMutateTask` +`ctx.runMutate` 映射顶层 api `runMutate`,`ctx.runMutateTask` 映射顶层 api `runMutateTask` ```ts import { runMutate, runMutateTask } from 'helux'; // 两者等效 numAtomCtx.runMutate('fnName'); -runMutate(numAtom) +runMutate(numAtom); // 两者等效 -numAtomCtx.runMutateTask('fnName') -runMutateTask(numAtom) +numAtomCtx.runMutateTask('fnName'); +runMutateTask(numAtom); ``` ### aciton & acitonAsync -`share` 返回的共享上下文里`aciton` 和 `acitonAsync` 方法对应顶层api `aciton` 和 `acitonAsync` +`share` 返回的共享上下文里`aciton` 和 `acitonAsync` 方法对应顶层 api `aciton` 和 `acitonAsync` ```ts import { aciton, acitonAsync } from 'helux'; @@ -97,7 +97,7 @@ const myActionAsync = sharedCtx.acitonAsync(actionFnDef); const myActionAsync = acitonAsync(sharedState)(actionFnDef); ``` -`atom` 返回的共享上下文里`aciton` 和 `acitonAsync` 方法对应顶层api `atomAciton` 和 `atomAcitonAsync` +`atom` 返回的共享上下文里`aciton` 和 `acitonAsync` 方法对应顶层 api `atomAciton` 和 `atomAcitonAsync` ```ts import { atomAciton, atomAcitonAsync } from 'helux'; @@ -119,19 +119,19 @@ const myActionAsync = atomAcitonAsync(numAtom)(actionFnDef); ### useState -`share` 返回的共享上下文里 `useState` 方法对应顶层api `useShared` +`share` 返回的共享上下文里 `useState` 方法对应顶层 api `useShared` ```ts import { useShared } from 'helux'; // 组件里使用,两者等效 -const [ state, setState ] = useShared(sharedState); -const [ state, setState ] = sharedCtx.useState(); +const [state, setState] = useShared(sharedState); +const [state, setState] = sharedCtx.useState(); ``` ### getMutateLoading -`share` 返回的共享上下文里 `getMutateLoading` 方法对应顶层api `getMutateLoading` +`share` 返回的共享上下文里 `getMutateLoading` 方法对应顶层 api `getMutateLoading` ```ts import { getMutateLoading } from 'helux'; @@ -143,7 +143,7 @@ const loadingObj = sharedCtx.getMutateLoading(); ### useMutateLoading -`share` 返回的共享上下文里 `useMutateLoading` 方法对应顶层api `useMutateLoading` +`share` 返回的共享上下文里 `useMutateLoading` 方法对应顶层 api `useMutateLoading` ```ts import { getMutateLoading } from 'helux'; @@ -155,7 +155,7 @@ const loadingObj = sharedCtx.useMutateLoading(); ### getActionLoading -`share` 返回的共享上下文里 `getActionLoading` 方法对应顶层api `getActionLoading` +`share` 返回的共享上下文里 `getActionLoading` 方法对应顶层 api `getActionLoading` ```ts import { getActionLoading } from 'helux'; @@ -165,10 +165,9 @@ const loadingObj = getActionLoading(sharedState); const loadingObj = sharedCtx.getActionLoading(); ``` - ### useActionLoading -`share` 返回的共享上下文里 `useActionLoading` 方法对应顶层api `useActionLoading` +`share` 返回的共享上下文里 `useActionLoading` 方法对应顶层 api `useActionLoading` ```ts import { useActionLoading } from 'helux'; diff --git a/package.json b/package.json index acdcc331..4a3436c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "helux", - "version": "3.3.8", + "version": "3.4.1", "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/api.ts b/packages/helux-core/src/api.ts index e21d0af5..3486c64a 100644 --- a/packages/helux-core/src/api.ts +++ b/packages/helux-core/src/api.ts @@ -10,6 +10,7 @@ import { atomMutate, mutate, mutateDict, runMutate, runMutateTask } from './fact import { atom, share, shareAtom, shareState } from './factory/createShared'; import { sync, syncer } from './factory/createSync'; import { watch } from './factory/createWatch'; +import { currentDraftRoot, setAtomVal } from './factory/creator/current'; import { getDeriveLoading, runDerive, runDeriveAsync } from './helpers/fnRunner'; import { getRawState, getSnap } from './helpers/state'; import { useDerived, useDerivedAtom } from './hooks/useDerived'; @@ -74,6 +75,8 @@ export { emit, on, // util api + currentDraftRoot, + setAtomVal, storeSrv, produce, shallowCompare, diff --git a/packages/helux-core/src/factory/common/middleware.ts b/packages/helux-core/src/factory/common/middleware.ts index 7baab9e7..18bccf5d 100644 --- a/packages/helux-core/src/factory/common/middleware.ts +++ b/packages/helux-core/src/factory/common/middleware.ts @@ -1,4 +1,4 @@ -import { Dict, IMiddlewareCtx, Middleware, MutableDraft } from '../../types/base'; +import { Dict, DraftRoot, IMiddlewareCtx, Middleware } from '../../types/base'; import type { TInternal } from '../creator/buildInternal'; import { getRootCtx } from '../root'; @@ -10,16 +10,16 @@ export function addMiddleware(mid: Middleware) { /** * middle only support sync call, so no next fn handler in middleware fn args */ -export function runMiddlewares(internal: TInternal, draft: MutableDraft, sn: number) { +export function runMiddlewares(internal: TInternal, draftRoot: DraftRoot, draft: DraftRoot, sn: number) { const { middlewares } = getRootCtx(); if (!middlewares.length) { return; } const data: Dict = {}; - const { sharedKey, moduleName } = internal; + const { sharedKey, moduleName, forAtom } = internal; const setData = (key: string, value: any) => (data[key] = value); - const midCtx: IMiddlewareCtx = { draft, sharedKey, moduleName, setData, idx: 0, sn }; + const midCtx: IMiddlewareCtx = { forAtom, draftRoot, draft, sharedKey, moduleName, setData, data, idx: 0, sn }; middlewares.forEach((fn, idx) => { fn({ ...midCtx, idx }); }); diff --git a/packages/helux-core/src/factory/common/util.ts b/packages/helux-core/src/factory/common/util.ts index 9e869363..a5e16700 100644 --- a/packages/helux-core/src/factory/common/util.ts +++ b/packages/helux-core/src/factory/common/util.ts @@ -21,8 +21,21 @@ export interface IMutateCtx { writeKeys: Dict; arrKeyDict: Dict; writeKeyPathInfo: Dict; - /** TODO :记录变化值的路径,用于异步执行环境合并到 rawState 时,仅合并变化的那一部分节点,避免数据脏写 */ + /** + * default: true + * 是否处理 atom setState((draft)=>xxx) 返回结果xxx, + * 目前规则是修改了 draft 则 handleAtomCbReturn 被会置为 false, + * 避免无括号写法 draft=>draft.xx = 1 隐式返回的结果 1 被写入到草稿, + * 备注:安全写法应该是draft=>{draft.xx = 1} + */ + handleAtomCbReturn: boolean; + /** + * TODO :记录变化值的路径,用于异步执行环境合并到 rawState 时,仅合并变化的那一部分节点,避免数据脏写 + * 但异步执行环境直接修改 draft 本身就是很危险的行为,该特性需要慎重考虑是否要实现 + */ keyPathValue: Map; + /** 为 atom 记录的 draft.val 引用 */ + draftVal: any; } // for hot reload of buildShared @@ -54,6 +67,8 @@ export function newMutateCtx(options: ISetStateOptions): IMutateCtx { arrKeyDict: {}, // 记录读取过程中遇到的数组key writeKeyPathInfo: {}, keyPathValue: new Map(), + handleAtomCbReturn: true, + draftVal: null, }; } diff --git a/packages/helux-core/src/factory/createAction.ts b/packages/helux-core/src/factory/createAction.ts index 412cedee..56c480ae 100644 --- a/packages/helux-core/src/factory/createAction.ts +++ b/packages/helux-core/src/factory/createAction.ts @@ -20,7 +20,7 @@ function innerCreate(state: T, options: { fn: Fn; desc: string; return callMutateFnLogic(state, { fn, desc, - getArgs: ({ draft, setState, desc }) => [{ draft, setState, desc, args }], + getArgs: ({ draft, draftRoot, setState, desc }) => [{ draft, draftRoot, setState, desc, args }], from: 'Action', }); }; // now fn can have a name 'action' at dev mode diff --git a/packages/helux-core/src/factory/createShared.ts b/packages/helux-core/src/factory/createShared.ts index 92cb0760..5843478a 100644 --- a/packages/helux-core/src/factory/createShared.ts +++ b/packages/helux-core/src/factory/createShared.ts @@ -1,3 +1,4 @@ +import { noop } from '@helux/utils'; import { FROM, STATE_TYPE } from '../consts'; import { useAtom, useShared } from '../hooks/useShared'; import type { CoreApiCtx } from '../types/api-ctx'; @@ -5,6 +6,7 @@ import type { Dict, Fn, IAtomCreateOptions, IAtomCtx, ICreateOptions, IRunMutate import { action, actionAsync, atomAction, atomActionAsync } from './createAction'; import { atomMutate, mutate, runMutate, runMutateTask } from './createMutate'; import { buildSharedObject } from './creator'; +import { setAtomVal } from './creator/current'; import { getGlobalEmpty, initGlobalEmpty } from './creator/globalId'; import { initGlobalLoading, initLoadingCtx } from './creator/loading'; import type { IInnerOptions } from './creator/parse'; @@ -19,6 +21,7 @@ function getFns(state: any, forAtom: boolean) { actionCreator: atomAction(state), actionAsyncCreator: atomActionAsync(state), mutateCreator: atomMutate(state), + setAtomVal, // ctx上也放一个,方便推导类型 }; } return { @@ -26,6 +29,7 @@ function getFns(state: any, forAtom: boolean) { actionCreator: action(state), actionAsyncCreator: actionAsync(state), mutateCreator: mutate(state), + setAtomVal: noop, // 非 atom 调用此方法无效,类型上目前不不标记此方法的存在 }; } @@ -42,7 +46,7 @@ export function createSharedLogic(innerOptions: IInnerOptions, createOptions?: a ensureGlobal(apiCtx, stateType); const { sharedState: state, internal } = buildSharedObject(innerOptions, createOptions); const { syncer, sync, forAtom, setDraft: setState } = internal; - const { useFn, actionCreator, actionAsyncCreator, mutateCreator } = getFns(state, forAtom); + const { useFn, actionCreator, actionAsyncCreator, mutateCreator, setAtomVal } = getFns(state, forAtom); const opt = { internal, from: MUTATE, apiCtx }; const ldMutate = initLoadingCtx(createSharedLogic, opt); const ldAction = initLoadingCtx(createSharedLogic, { ...opt, from: ACTION }); @@ -64,6 +68,7 @@ export function createSharedLogic(innerOptions: IInnerOptions, createOptions?: a useActionLoading: ldAction.useLoading, sync, syncer, + setAtomVal, }; } diff --git a/packages/helux-core/src/factory/createSync.ts b/packages/helux-core/src/factory/createSync.ts index fa050c01..c193922c 100644 --- a/packages/helux-core/src/factory/createSync.ts +++ b/packages/helux-core/src/factory/createSync.ts @@ -6,7 +6,7 @@ function innerCreate(target: any, options: { label: string; forAtom?: boolean; i const { label, forAtom, isSyncer } = options; const { sharedKey, rawState, innerSetState } = checkSharedStrict(target, { label, forAtom }); const fn = isSyncer ? createSyncerBuilder : createSyncFnBuilder; - return fn(sharedKey, rawState, innerSetState); + return fn(rawState, { forAtom, sharedKey, setState: innerSetState }); } export function sync(target: T) { diff --git a/packages/helux-core/src/factory/creator/buildInternal.ts b/packages/helux-core/src/factory/creator/buildInternal.ts index d8e59416..ccdf6e53 100644 --- a/packages/helux-core/src/factory/creator/buildInternal.ts +++ b/packages/helux-core/src/factory/creator/buildInternal.ts @@ -28,7 +28,7 @@ export function buildInternal( setDraft: SetAtom | SetState; asyncSetState: AsyncSetState; innerSetState: InnerSetState; - setStateImpl: (...any: any[]) => { draft: any; finishMutate: Fn; getPartial: Fn }; + setStateImpl: (...any: any[]) => { draftRoot: any; draftNode: any; finishMutate: Fn; getPartial: Fn }; sharedState: Ext; ruleConf: IRuleConf; isDeep: boolean; diff --git a/packages/helux-core/src/factory/creator/buildShared.ts b/packages/helux-core/src/factory/creator/buildShared.ts index 0ce56187..09718f0e 100644 --- a/packages/helux-core/src/factory/creator/buildShared.ts +++ b/packages/helux-core/src/factory/creator/buildShared.ts @@ -34,6 +34,7 @@ export function buildSharedState(options: ParsedOptions) { }, }); } else { + // TODO 为{}对象型的 atom.val 再包一层监听 sharedState = createOb(rawState, { set: () => { warn('changing shared state is invalid'); diff --git a/packages/helux-core/src/factory/creator/commitState.ts b/packages/helux-core/src/factory/creator/commitState.ts index 80c03c5c..6ecd1bc9 100644 --- a/packages/helux-core/src/factory/creator/commitState.ts +++ b/packages/helux-core/src/factory/creator/commitState.ts @@ -4,7 +4,7 @@ import { createOb } from '../../helpers/obj'; import type { Dict, IInnerSetStateOptions } from '../../types/base'; import { getDepKeyByPath, IMutateCtx } from '../common/util'; import type { TInternal } from './buildInternal'; -import { execDepFnAndInsUpdater } from './updater'; +import { execDepFns } from './notify'; export interface ICommitStateOptions extends IInnerSetStateOptions { state: Dict; @@ -72,5 +72,5 @@ export function commitState(opts: ICommitStateOptions) { } interveneDeps(true, opts); interveneDeps(false, opts); - execDepFnAndInsUpdater(opts); + execDepFns(opts); } diff --git a/packages/helux-core/src/factory/creator/current.ts b/packages/helux-core/src/factory/creator/current.ts new file mode 100644 index 00000000..0f8fc474 --- /dev/null +++ b/packages/helux-core/src/factory/creator/current.ts @@ -0,0 +1,30 @@ +import { newMutateCtx } from '../common/util'; + +const fakeDraftRoot = { val: null, isFake: true }; +const fakeMutateCtx = newMutateCtx({}); +/** 正在执行中的 rootDrft */ +let CURRENT_DRAFT_ROOT = fakeDraftRoot; +/** 正在执行中的 mutateCtx */ +let CURRENT_MUTATE_CTX = fakeMutateCtx; + +export function setAtomVal(val: any) { + CURRENT_DRAFT_ROOT.val = val; +} + +export function currentDraftRoot() { + return CURRENT_DRAFT_ROOT; +} + +export const DRAFT_ROOT = { + /** may use ' get current(){}...' in the future */ + current: () => CURRENT_DRAFT_ROOT, + set: (draftRoot: any) => (CURRENT_DRAFT_ROOT = draftRoot), + del: () => (CURRENT_DRAFT_ROOT = fakeDraftRoot), +}; + +export const MUTATE_CTX = { + /** may use ' get current(){}...' in the future */ + current: () => CURRENT_MUTATE_CTX, + set: (mctx: any) => (CURRENT_MUTATE_CTX = mctx), + del: () => (CURRENT_MUTATE_CTX = fakeMutateCtx), +}; diff --git a/packages/helux-core/src/factory/creator/mapShared.ts b/packages/helux-core/src/factory/creator/mapShared.ts index 905c8398..13725439 100644 --- a/packages/helux-core/src/factory/creator/mapShared.ts +++ b/packages/helux-core/src/factory/creator/mapShared.ts @@ -1,7 +1,7 @@ -import { canUseDeep, isFn } from '@helux/utils'; +import { canUseDeep } from '@helux/utils'; import { setInternal } from '../../helpers/state'; import type { AsyncSetState, IInnerSetStateOptions, InnerSetState, SetAtom, SetState, SharedState } from '../../types/base'; -import { runPartialCb, wrapPartial } from '../common/util'; +import { runPartialCb } from '../common/util'; import { buildInternal } from './buildInternal'; import { prepareDeepMutate } from './mutateDeep'; import { prepareNormalMutate } from './mutateNormal'; @@ -16,22 +16,17 @@ export function mapSharedToInternal(sharedState: SharedState, options: ParsedOpt const setStateImpl = (partialState: any, options: IInnerSetStateOptions = {}) => { if (partialState === internal.snap) { // do nothing - return { draft: {}, getPartial: () => partialState, finishMutate: () => partialState }; + const obj = {}; + const fn = () => partialState; + return { draftRoot: obj, draftNode: obj, getPartial: fn, finishMutate: fn }; } const mutateOptions = { ...options, forAtom, internal, sharedState }; // deep 模式修改: setState(draft=>{draft.x.y=1}) - if (isFn(partialState) && isDeep) { - // now partialState is a draft recipe callback - const handleCtx = prepareDeepMutate(mutateOptions); - // 后续流程会使用到 getPartial 的返回结果,这样做是为了对齐非 deep 模式的 setState,此时只支持一层依赖收集 - const getPartial = () => wrapPartial(forAtom, partialState(handleCtx.draft)); - return { ...handleCtx, getPartial }; - } - - const handleCtx = prepareNormalMutate(mutateOptions); - const getPartial = () => runPartialCb(forAtom, partialState, handleCtx.draft); - return { ...handleCtx, getPartial }; + const preparedInfo = isDeep ? prepareDeepMutate(mutateOptions) : prepareNormalMutate(mutateOptions); + // 后续流程会使用到 getPartial 的返回结果,注意非 deep 模式的 setState只支持一层依赖收集 + const getPartial = () => runPartialCb(forAtom, partialState, preparedInfo.draftNode); + return { ...preparedInfo, getPartial }; }; // for inner call const innerSetState: InnerSetState = (partialState, options) => { @@ -53,8 +48,9 @@ export function mapSharedToInternal(sharedState: SharedState, options: ParsedOpt const setAtom: SetAtom = (atomVal, options) => { setState(atomVal, parseSetOptions(options)); }; - const sync = createSyncFnBuilder(sharedKey, rawState, innerSetState); - const syncer = createSyncerBuilder(sharedKey, rawState, innerSetState); + const syncOpts = { forAtom, sharedKey, setState: innerSetState }; + const sync = createSyncFnBuilder(rawState, syncOpts); + const syncer = createSyncerBuilder(rawState, syncOpts); const setDraft = forAtom ? setAtom : setState; const internal = buildInternal(options, { diff --git a/packages/helux-core/src/factory/creator/mutateDeep.ts b/packages/helux-core/src/factory/creator/mutateDeep.ts index 82c9566e..a99534cd 100644 --- a/packages/helux-core/src/factory/creator/mutateDeep.ts +++ b/packages/helux-core/src/factory/creator/mutateDeep.ts @@ -7,6 +7,7 @@ import { emitDataChanged } from '../common/plugin'; import { IMutateCtx, newMutateCtx } from '../common/util'; import type { TInternal } from './buildInternal'; import { commitState } from './commitState'; +import { DRAFT_ROOT, MUTATE_CTX } from './current'; import { handleOperate } from './operateState'; interface IPrepareDeepMutateOpts extends IInnerSetStateOptions { @@ -27,7 +28,7 @@ interface ISnCommitOpts extends ICommitOpts { /** * mutateNormal 和 mutateDepp 的 finishMutate 里提交之前可复用的公共逻辑 */ -export function beforeCommit(opts: ICommitOpts, innerSetOptions: IInnerSetStateOptions, draft: any) { +export function beforeCommit(opts: ICommitOpts, innerSetOptions: IInnerSetStateOptions, draftRoot: any) { Object.assign(opts, innerSetOptions); // sn 序号相同表示同一批次触发重渲染 // 注意 sn 和 internal.ver 不能画等号,sn 对应的将要执行函数的会有很多(包括异步函数) @@ -35,8 +36,9 @@ export function beforeCommit(opts: ICommitOpts, innerSetOptions: IInnerSetStateO opts.sn = opts.sn || genRenderSN(); opts.from = opts.from || 'SetState'; const { from, sn, desc, internal } = opts; - internal.before({ from, draft, desc, sn }); - runMiddlewares(internal, draft, sn); + const draft = internal.forAtom ? draftRoot.val : draftRoot; + internal.before({ from, draftRoot, draft, desc, sn }); + runMiddlewares(internal, draftRoot, draft, sn); return opts as ISnCommitOpts; // 已确保打上 sn 标记 } @@ -47,31 +49,45 @@ export function prepareDeepMutate(opts: IPrepareDeepMutateOpts) { const { internal, desc } = opts; const mutateCtx = newMutateCtx(opts); const commitOpts = { state: {}, mutateCtx, ...opts, desc }; - const draft = createDraft(internal.rawState, { + const draftRoot = createDraft(internal.rawState, { onOperate(opParams) { handleOperate(opParams, { internal, mutateCtx }); }, }); + const { forAtom, isPrimitive } = internal; + // 记录正在执行中的 draftRoot mutateCtx + DRAFT_ROOT.set(draftRoot); + MUTATE_CTX.set(mutateCtx); + // atom draft 自动拆箱 + const draftNode = forAtom ? draftRoot.val : draftRoot; return { - draft, + draftRoot, + draftNode, finishMutate(partial?: Dict, innerSetOptions: IInnerSetStateOptions = {}) { const { writeKeys, writeKeyPathInfo } = mutateCtx; // 把深依赖和浅依赖收集到的keys合并起来 if (isObj(partial)) { // 触发 writeKeys 里记录当前变化key - if (internal.forAtom) { - draft.val = partial.val; + if (forAtom && isPrimitive) { + // 对于 primitive atom 来说,如果操作了 currentDraftRoot().val=xx 赋值,则丢弃 partial.val + // 例如写为 sync(to=>to.a, ()=>currentDraftRoot().val=1) 而不是 sync(to=>to.a, draft=>{currentDraftRoot().val=1}) 时 + // 这里的 ()=>currentDraftRoot().val=1 其实还同时返回了值 1,这个 1 被包裹为 { val: 1 } 到这里后, + // 如果写入就会造成草稿对象被污染,故这里判断一下 mutateCtx.isChanged 做个保护 + if (mutateCtx.handleAtomCbReturn) { + draftRoot.val = partial.val; + } } else { Object.keys(partial).forEach((key) => { - draft[key] = partial[key]; + draftRoot[key] = partial[key]; }); } } - const opts = beforeCommit(commitOpts, innerSetOptions, draft); - + const opts = beforeCommit(commitOpts, innerSetOptions, draftRoot); mutateCtx.depKeys = Object.keys(writeKeys); - opts.state = finishDraft(draft); // a structural shared obj generated by limu + DRAFT_ROOT.del(); + MUTATE_CTX.del(); + opts.state = finishDraft(draftRoot); // a structural shared obj generated by limu mutateCtx.triggerReasons = Object.values(writeKeyPathInfo); commitState(opts); emitDataChanged(internal, innerSetOptions, desc); diff --git a/packages/helux-core/src/factory/creator/mutateFn.ts b/packages/helux-core/src/factory/creator/mutateFn.ts index cd28b69d..b0649f0f 100644 --- a/packages/helux-core/src/factory/creator/mutateFn.ts +++ b/packages/helux-core/src/factory/creator/mutateFn.ts @@ -21,9 +21,10 @@ interface ICallMutateFnLogicOptionsBase { interface ICallMutateFnLogicOptions extends ICallMutateFnLogicOptionsBase { draft?: T; + draftRoot?: T; fn: Fn; /** fn 函数调用入参拼装 */ - getArgs?: (param: { draft: SharedState; setState: Fn; desc: string; input: any[] }) => any[]; + getArgs?: (param: { draft: SharedState; draftRoot: SharedState; setState: Fn; desc: string; input: any[] }) => any[]; } interface ICallAsyncMutateFnLogicOptions extends ICallMutateFnLogicOptionsBase { @@ -64,14 +65,17 @@ export function callMutateFnLogic(targetState: T, options: ICal const { forAtom, setStateImpl, innerSetState } = internal; const innerSetOptions: IInnerSetStateOptions = { desc, sn, from, isFirstCall }; - let draft = options.draft as SharedState; // 如果传递了 draft 表示需要复用 + // 如果传递了 draft 表示需要复用 + let draft = options.draft as SharedState; + let draftRoot = options.draftRoot as SharedState; let finishMutate = noop; // 现阶段一定不会有 draft 传下来,这个判断后续可能会考虑移除 if (!draft) { // 不透传 draft 时,指向一个真正有结束功能的 finishMutate 句柄 const setCtx = setStateImpl(noop); - draft = setCtx.draft; + draft = setCtx.draftNode; + draftRoot = setCtx.draftRoot; finishMutate = setCtx.finishMutate; } // 透传 draft 目的见 watchAndCallMutateDict TODO 解释 @@ -83,7 +87,7 @@ export function callMutateFnLogic(targetState: T, options: ICal return innerSetState(cb, innerSetOptions); // 继续透传 sn from 等信息 }; const input = enureReturnArr(deps, targetState); - const args = getArgs({ draft, setState, desc, input }) || [draft, input]; + const args = getArgs({ draft, draftRoot, setState, desc, input }) || [draft, input]; // TODO 考虑同步函数的错误发送给插件 try { @@ -128,7 +132,7 @@ export function watchAndCallMutateDict(options: IWatchAndCallMutateDictOptions) // TODO: 此段代码为后面的 mutateSelf 接口做准备 // const { setStateImpl, mutateFnDict, usefulName } = internal; // 考虑是否要首次运行会复用 draft ,然后经过多次修改,最后一次才提交,但这样做和死循环探测有逻辑冲突 - // let { draft, finishMutate } = setStateImpl(noop); + // let { draftNode: draft, finishMutate } = setStateImpl(noop); // const lastIdx = keys.length - 1; keys.forEach((descKey) => { diff --git a/packages/helux-core/src/factory/creator/mutateNormal.ts b/packages/helux-core/src/factory/creator/mutateNormal.ts index a0b22e01..b9214fd5 100644 --- a/packages/helux-core/src/factory/creator/mutateNormal.ts +++ b/packages/helux-core/src/factory/creator/mutateNormal.ts @@ -5,6 +5,7 @@ import { emitDataChanged } from '../common/plugin'; import { newMutateCtx, newOpParams } from '../common/util'; import type { TInternal } from './buildInternal'; import { commitState } from './commitState'; +import { DRAFT_ROOT, MUTATE_CTX } from './current'; import { beforeCommit } from './mutateDeep'; import { handleOperate } from './operateState'; @@ -19,7 +20,7 @@ interface IPrepareNormalMutateOpts extends IInnerSetStateOptions { */ export function prepareNormalMutate(opts: IPrepareNormalMutateOpts) { const { internal, desc } = opts; - const { rawState, sharedKey, moduleName } = internal; + const { rawState, sharedKey, moduleName, forAtom } = internal; const newPartial: Dict = {}; const mayChangedKeys: string[] = []; @@ -29,6 +30,7 @@ export function prepareNormalMutate(opts: IPrepareNormalMutateOpts) { handleOperate(newOpParams(key, value), { internal, mutateCtx }); }; + // TODO 为{}对象型的 atom.val 再包一层监听 // 为了和 deep 模式下返回的 setState 保持行为一致 const mockDraft = createOb(rawState, { set: (target: Dict, key: any, value: any) => { @@ -46,9 +48,14 @@ export function prepareNormalMutate(opts: IPrepareNormalMutateOpts) { return has(newPartial, key) ? newPartial[key] : target[key]; }, }); + // 记录正在执行中的 draftRoot mutateCtx + DRAFT_ROOT.set(mockDraft); + MUTATE_CTX.set(mutateCtx); + const draftNode = forAtom ? mockDraft.val : mockDraft; return { - draft: mockDraft, + draftRoot: mockDraft, + draftNode, finishMutate(partial?: Dict, innerSetOptions: IInnerSetStateOptions = {}) { /** * 兼容非 deep 模式下用户的以下代码 @@ -85,6 +92,8 @@ export function prepareNormalMutate(opts: IPrepareNormalMutateOpts) { nodupPush(depKeys, depKey); triggerReasons.push({ sharedKey, moduleName, keyPath: [depKey] }); }); + DRAFT_ROOT.del(); + MUTATE_CTX.del(); opts.state = newPartial; commitState(opts); emitDataChanged(internal, innerSetOptions, desc); diff --git a/packages/helux-core/src/factory/creator/updater.ts b/packages/helux-core/src/factory/creator/notify.ts similarity index 98% rename from packages/helux-core/src/factory/creator/updater.ts rename to packages/helux-core/src/factory/creator/notify.ts index 9552e7d9..a564b138 100644 --- a/packages/helux-core/src/factory/creator/updater.ts +++ b/packages/helux-core/src/factory/creator/notify.ts @@ -10,7 +10,7 @@ import type { InsCtxDef } from './buildInternal'; import type { ICommitStateOptions } from './commitState'; import { getGlobalEmptyInternal, getGlobalIdInsKeys } from './globalId'; -export function execDepFnAndInsUpdater(opts: ICommitStateOptions) { +export function execDepFns(opts: ICommitStateOptions) { const { mutateCtx, internal, desc, isFirstCall, from, sn } = opts; const { ids, globalIds, depKeys, triggerReasons } = mutateCtx; const { key2InsKeys, id2InsKeys, insCtxMap, rootValKey } = internal; diff --git a/packages/helux-core/src/factory/creator/operateState.ts b/packages/helux-core/src/factory/creator/operateState.ts index 1e5fa476..d83b51cb 100644 --- a/packages/helux-core/src/factory/creator/operateState.ts +++ b/packages/helux-core/src/factory/creator/operateState.ts @@ -22,6 +22,7 @@ export function handleOperate(opParams: IOperateParams, opts: { internal: TInter const { moduleName, sharedKey, exact, ruleConf, level1ArrKeys } = internal; const { writeKeyPathInfo, ids, globalIds, writeKeys } = mutateCtx; mutateCtx.level1Key = fullKeyPath[0]; + mutateCtx.handleAtomCbReturn = false; // 主动把数组自身节点 key 也记录一下 if (parentType === 'Array') { diff --git a/packages/helux-core/src/factory/creator/parse.ts b/packages/helux-core/src/factory/creator/parse.ts index 1c510c89..881839da 100644 --- a/packages/helux-core/src/factory/creator/parse.ts +++ b/packages/helux-core/src/factory/creator/parse.ts @@ -1,4 +1,4 @@ -import { canUseDeep, isFn, isObj, nodupPush, noop, noopArr, safeObjGet, setNoop } from '@helux/utils'; +import { canUseDeep, enureReturnArr, isFn, isJsObj, isObj, nodupPush, noop, noopArr, safeObjGet, setNoop } from '@helux/utils'; import { immut } from 'limu'; import { FROM, LOADING_MODE, SINGLE_MUTATE, STATE_TYPE, STOP_ARR_DEP, STOP_DEPTH } from '../../consts'; import { createOb, injectHeluxProto } from '../../helpers/obj'; @@ -56,8 +56,12 @@ export function parseRawState(innerOptions: IInnerOptions) { const { forAtom = false } = innerOptions; let rawState: any = innerOptions.rawState; const isStateFn = isFn(rawState); + let isPrimitive = false; if (forAtom) { rawState = isStateFn ? { val: rawState() } : { val: rawState }; + // 记录 atom 值的最初类型,如果是 undefined null 也当做原始类型记录 + // TODO disscussion 这里后续是否需要进一步细分待用户讨论 + isPrimitive = !rawState.val || !isJsObj(rawState.val); } else { rawState = isStateFn ? rawState() : rawState; if (!isObj(rawState)) { @@ -67,7 +71,7 @@ export function parseRawState(innerOptions: IInnerOptions) { throw new Error('ERR_ALREADY_SHARED: pass a shared object to createShared!'); } } - return rawState; + return { isPrimitive, rawState }; } export function parseDesc(fnKey: any, itemDesc?: any) { @@ -139,7 +143,7 @@ export function parseMutate(mutate?: IInnerCreateOptions['mutate'] | null, cache export function parseOptions(innerOptions: IInnerOptions, options: ICreateOptions = {}) { const { forAtom = false, forGlobal = false, stateType = STATE_TYPE.USER_STATE } = innerOptions; - const rawState = parseRawState(innerOptions); + const { rawState, isPrimitive } = parseRawState(innerOptions); const sharedKey = markSharedKeyOnState(rawState); const moduleName = options.moduleName || ''; const deep = options.deep ?? true; @@ -179,6 +183,7 @@ export function parseOptions(innerOptions: IInnerOptions, options: ICreateOption loadingMode, stopArrDep, stopDepth, + isPrimitive, }; } @@ -188,7 +193,7 @@ export type ParsedOptions = ReturnType; * 解析出 createShared 里配置的 rules */ export function parseRules(options: ParsedOptions): IRuleConf { - const { rawState, sharedKey, deep, rules, stopDepth, stopArrDep } = options; + const { rawState, sharedKey, deep, rules, stopDepth, stopArrDep, forAtom } = options; const idsDict: KeyIdsDict = {}; const globalIdsDict: KeyIdsDict = {}; const stopDepInfo: IRuleConf['stopDepInfo'] = { keys: [], isArrDict: {}, arrKeyStopDcit: {}, depth: stopDepth, stopArrDep }; @@ -236,7 +241,9 @@ export function parseRules(options: ParsedOptions): IRuleConf { }); } - const result = when(state); + // atom 自动拆箱 + const stateNode = forAtom ? state.val : state; + const result = enureReturnArr(when, stateNode); // record id, globalId, stopDep const setRuleConf = (confKey: string) => { const idList = safeObjGet(idsDict, confKey, [] as NumStrSymbol[]); @@ -259,12 +266,8 @@ export function parseRules(options: ParsedOptions): IRuleConf { }; confKeys.forEach(setRuleConf); - // 为让 globalId 机制能够正常工作,需要补上 sharedKey - if ( - keyReaded - || result === state // 返回了state自身 - || (Array.isArray(result) && result.includes(state)) // 返回了数组,包含有state自身 - ) { + // 为让 globalId 机制能够正常工作,有 key 读取或返回的数组包含有state自身时,需补上 sharedKey + if (keyReaded || result.includes(stateNode)) { setRuleConf(`${sharedKey}`); } }); diff --git a/packages/helux-core/src/factory/creator/sync.ts b/packages/helux-core/src/factory/creator/sync.ts index 94d9a2b9..ff62fd37 100644 --- a/packages/helux-core/src/factory/creator/sync.ts +++ b/packages/helux-core/src/factory/creator/sync.ts @@ -1,7 +1,8 @@ -import { setVal } from '@helux/utils'; +import { isJsObj, setVal } from '@helux/utils'; import { createOneLevelOb } from '../../helpers/obj'; import type { Dict, Fn, SetState } from '../../types/base'; import { createImmut, getDepKeyByPath } from '../common/util'; +import { DRAFT_ROOT, MUTATE_CTX } from './current'; export function getEventVal(e: any) { let val = e; @@ -32,8 +33,13 @@ function createSyncFn(setState: Fn, path: string[], before?: Fn) { let val = getEventVal(evOrVal); setState( (draft: any) => { - setVal(draft, path, val); - before?.(val, draft); // 用户设置了想修改其他数据或自身数据的函数 + // 使用 draftRoot 做赋值,透传拆箱后的 draft 给用户( 如果是 atom ) + const draftRoot = DRAFT_ROOT.current(); + setVal(draftRoot, path, val); + // 刻意再次标记为 true,让 before 返回的结果生效(如用户未修改 draftRoot的话) + MUTATE_CTX.current().handleAtomCbReturn = true; + // 用户设置了想修改其他数据或自身数据的函数 + return before?.(val, draft); }, { from: 'Sync' }, ); @@ -41,26 +47,46 @@ function createSyncFn(setState: Fn, path: string[], before?: Fn) { return syncFn; } +interface ICreateOpts { + forAtom?: boolean; + sharedKey: number; + setState: SetState; +} + +function syncerFn(keyPath: string[], options: ICreateOpts) { + const { sharedKey, setState } = options; + let cacheKey = getDepKeyByPath(keyPath, sharedKey); + let dataSyncer = dataSyncerCahce.get(cacheKey); + if (!dataSyncer) { + dataSyncer = createSyncFn(setState, keyPath); + dataSyncerCahce.set(cacheKey, dataSyncer); + } + return dataSyncer; +} + const dataSyncerCahce = new Map(); /** * 注意 syncer 只能同步一层key的数据,如需要同步多层的,使用 sync 函数 *
* @return syncerBuilder */ -export function createSyncerBuilder(sharedKey: number, rawState: Dict, setState: SetState) { - const syncer = createOneLevelOb(rawState, { - get(target, key) { - let cacheKey = getDepKeyByPath([key], sharedKey); - let dataSyncer = dataSyncerCahce.get(cacheKey); - if (!dataSyncer) { - dataSyncer = createSyncFn(setState, [key]); - dataSyncerCahce.set(cacheKey, dataSyncer); - } +export function createSyncerBuilder(rawState: Dict, options: ICreateOpts) { + if (options.forAtom) { + // 原始值 atom,用户可使用 syncer 直接绑定 + //
+ if (!isJsObj(rawState.val)) { + return syncerFn(['val'], options); + } + + // 对象结果自动拆箱,对 rawState.val 的各个 key 做 syncer 函数集合 + return createOneLevelOb(rawState.val, { + get: (target, key) => syncerFn(['val', key], options), + }); + } - return dataSyncer; - }, + return createOneLevelOb(rawState, { + get: (target, key) => syncerFn([key], options), }); - return syncer; } const syncFnCahce = new Map(); @@ -70,15 +96,19 @@ const syncFnCahce = new Map(); *
t.a.b, val=>val+1)}>
* @return syncFnBuilder */ -export function createSyncFnBuilder(sharedKey: number, rawState: Dict, setState: SetState) { +export function createSyncFnBuilder(rawState: Dict, options: ICreateOpts) { + const { forAtom, sharedKey, setState } = options; const targetWrap = createTargetWrap(rawState); return (pathOrRecorder: any, before?: (val: any) => any) => { let path: string[] = []; if (Array.isArray(pathOrRecorder)) { - path = pathOrRecorder; + // atom 自动补齐 val + path = forAtom ? ['val', ...pathOrRecorder] : pathOrRecorder; } else { - pathOrRecorder(targetWrap.target); - path = targetWrap.getPath(); + const { target, getPath } = targetWrap; + // atom sync 读路径回调自动拆箱 + pathOrRecorder(forAtom ? target.val : target); + path = getPath(); } let cacheKey = getDepKeyByPath(path, sharedKey); diff --git a/packages/helux-core/src/types/api.d.ts b/packages/helux-core/src/types/api.d.ts index aa9c114f..1943a96b 100644 --- a/packages/helux-core/src/types/api.d.ts +++ b/packages/helux-core/src/types/api.d.ts @@ -588,6 +588,17 @@ export function atomActionAsync( atom: Atom, ): (fn: AtomActionAsyncFnDef, desc?: string) => AtomActionAsync; +/** + * get current draft root + * here pass state just for get return type + */ +export function currentDraftRoot(state?: T): T; + +/** + * setAtomVal('xx') 等效于 currentDraftRoot().val = 'xx'; + */ +export function setAtomVal(val?: T): T; + // ----------- shallowCompare isDiff produce 二次重导出会报错,这里手动声明一下 -------------- // err: 如果没有引用 "../../helux-core/node_modules/limu/lib",则无法命名 "produce" 的推断类型。这很可能不可移植。需要类型注释 diff --git a/packages/helux-core/src/types/base.d.ts b/packages/helux-core/src/types/base.d.ts index fc8b2dbb..d0692a76 100644 --- a/packages/helux-core/src/types/base.d.ts +++ b/packages/helux-core/src/types/base.d.ts @@ -123,8 +123,6 @@ export type NextSharedDict = T; export type NextAtom = { val: T }; -export type MutableDraft = T; - /** returned by atom */ export type Atom = { val: T }; @@ -138,8 +136,6 @@ export type NextAtomVal = T; export type ReadonlyAtom = { readonly val: T }; -export type MutableAtomDraft = { val: T }; - export type Ext = T & { [key: string]: V }; export type KeyBoolDict = Record; @@ -148,9 +144,13 @@ export type KeyIdsDict = Record; export type KeyInsKeysDict = Record; -export type Draft = T; +export type DraftRoot = T; + +/** boxed atom draft */ +export type AtomDraftRoot = { val: T }; -export type AtomDraft = { val: T }; +/** unboxed atom draft */ +export type AtomDraft = T; export type SharedState = SharedDict | Atom; @@ -170,21 +170,33 @@ export type LoadingState = { [key in keyof T]: LoadingStatus; }; -export type ActionFnParam = { draft: Draft; setState: SetState; desc: string; args: A }; +export type ActionFnParam = { + draft: DraftRoot; + draftRoot: DraftRoot; + setState: SetState; + desc: string; + args: A; +}; -export type AsyncActionFnParam = { setState: SetState; desc: string; args: A }; +export type ActionAsyncFnParam = { setState: SetState; desc: string; args: A }; -export type ActionFnDef = (param: ActionFnParam) => Partial | void; +export type ActionFnDef = (param: ActionFnParam) => Partial | void; -export type Action = (...args: A) => NextSharedDict; +export type Action = (...args: A) => NextSharedDict; -export type ActionAsyncFnDef = (param: AsyncActionFnParam) => void; +export type ActionAsyncFnDef = (param: ActionAsyncFnParam) => void; -export type ActionAsync = (...args: A) => Promise>; +export type ActionAsync = (...args: A) => Promise>; // atom action series -export type AtomActionFnParam = { draft: AtomDraft; setState: SetAtom; desc: string; args: A }; +export type AtomActionFnParam = { + draft: AtomDraft; + draftRoot: AtomDraftRoot; + setState: SetAtom; + desc: string; + args: A; +}; export type AtomActionAsyncFnParam = { setState: SetAtom; desc: string; args: A }; @@ -194,7 +206,7 @@ export type AtomAction = (...args: A) => NextA export type AtomActionAsyncFnDef = (param: AtomActionAsyncFnParam) => void; -export type AtomActionAsync = (...args: A) => Promise>; +export type AtomAsyncAction = (...args: A) => Promise>; export type ReadOnlyArr = readonly any[]; @@ -243,7 +255,7 @@ export type MutateWitness = { export type MutateTask = (param: IMutateTaskParam) => Promise; /** 如定义了 task 函数,则 fn 在异步函数执行之前回执行一次,且只在首次执行一次,后续不会执行 */ -export type MutateFn = (draft: Draft, input: A) => Partial | void; +export type MutateFn = (draft: DraftRoot, input: A) => void; export type MutateFnItem = { /** 异步 mutate 的依赖项列表 */ @@ -280,7 +292,7 @@ export type MutateFnList = Array | MutateFnLooseItem export type AtomMutateTask = (param: IAtomMutateTaskParam) => Promise; /** 如定义了 task 函数,则 atom fn 在异步函数执行之前回执行一次,且只在首次执行一次,后续不会执行 */ -export type AtomMutateFn = (draft: AtomDraft, input: A) => T | void; +export type AtomMutateFn = (draft: AtomDraft, input: A) => void; export type AtomMutateFnItem = { /** 如定义了 task,fn 只会执行一次 */ @@ -333,49 +345,61 @@ export type DeriveAtomFnItem = { }; export type SetState = ( - partialStateOrRecipeCb: Partial | ((mutable: MutableDraft) => void | Partial), + partialStateOrRecipeCb: Partial | ((draft: DraftRoot) => void | Partial), options?: ISetStateOptions, ) => NextSharedDict; /** dangerous asyn set state */ export type AsyncSetState = ( - partialStateOrRecipeCb: Partial | ((mutable: MutableDraft) => void | Partial), + partialStateOrRecipeCb: Partial | ((draft: DraftRoot) => void | Partial), options?: ISetStateOptions, ) => Promise>; export type SetAtom = ( - newAtomOrRecipeCb: T | ((mutable: MutableAtomDraft) => void | T), + newAtomOrRecipeCb: T | ((draft: AtomDraft) => void | T), options?: ISetStateOptions>, ) => NextAtomVal; export type InnerSetState = ( - partialStateOrRecipeCb: Partial | ((mutable: MutableDraft) => void | Partial), + partialStateOrRecipeCb: Partial | ((draft: DraftRoot) => void | Partial), options?: IInnerSetStateOptions, ) => NextSharedDict; export type Call = ( - srvFn: (ctx: { args: A; state: Readonly; draft: MutableDraft; setState: SetState }) => Partial | void, + srvFn: (ctx: { args: A; state: Readonly; draft: DraftRoot; draftRoot: DraftRoot; setState: SetState }) => Partial | void, ...args: A ) => NextSharedDict; /** * 👿 呼叫异步函数修改 draft 是危险的行为,可能会造成数据脏覆盖的情况产生 */ -export type AsyncCall = ( - srvFn: (ctx: { args: A; state: Readonly; draft: MutableDraft; setState: SetState }) => Promise | void>, +export type CallAsync = ( + srvFn: (ctx: { + args: A; + state: Readonly; + draft: DraftRoot; + draftRoot: DraftRoot; + setState: SetState; + }) => Promise | void>, ...args: A ) => Promise>; export type AtomCall = ( - srvFn: (ctx: { args: A; state: ReadonlyAtom; draft: MutableAtomDraft; setState: SetAtom }) => T | void, + srvFn: (ctx: { args: A; state: ReadonlyAtom; draft: AtomDraft; draftRoot: AtomDraftRoot; setState: SetAtom }) => T | void, ...args: A ) => NextAtomVal; /** * 👿 呼叫异步函数修改 atom draft 是危险的行为,可能会造成数据脏覆盖的情况产生 */ -export type AtomAsyncCall = ( - srvFn: (ctx: { args: A; state: ReadonlyAtom; draft: MutableAtomDraft; setState: SetAtom }) => Promise, +export type AtomCallAsync = ( + srvFn: (ctx: { + args: A; + state: ReadonlyAtom; + draft: AtomDraft; + draftRoot: AtomDraftRoot; + setState: SetAtom; + }) => Promise, ...args: A ) => Promise>; @@ -384,14 +408,22 @@ export type SyncerFn = (mayEvent: any, ...args: any[]) => void; export type PathRecorder = (target: T) => V; // 此处用 V 约束 before 函数的返回类型 -export type SyncFnBuilder = ( +export type SyncFnBuilder = ( pathOrRecorder: string[] | PathRecorder, /** 在提交数据之前,还可以修改其他数据或自身数据的函数 */ before?: (eventNewVal: V, draft: T) => void, ) => SyncerFn; +export type AtomSyncFnBuilder = ( + pathOrRecorder: string[] | PathRecorder, + /** 在提交数据之前,还可以修改其他数据或自身数据的函数 */ + before?: (eventNewVal: V, draft: AtomDraft) => void, +) => SyncerFn; + export type Syncer = { [key in keyof T]: SyncerFn }; +export type AtomSyncer = T extends Dict ? { [key in keyof T]: SyncerFn } : SyncerFn; + export type SafeLoading = ICreateOptions> = O['mutate'] extends MutateFnDict ? Ext, LoadingStatus> : Ext; @@ -405,7 +437,7 @@ export interface ISharedCtx = ICrea runMutate: (descOrOptions: string | IRunMutateOptions) => T; runMutateTask: (descOrOptions: string | IRunMutateOptions) => T; call: Call; - callAsync: AsyncCall; + callAsync: CallAsync; action: (fn: ActionFnDef, desc?: FnDesc) => Action; actionAsync: (fn: ActionAsyncFnDef, desc?: FnDesc) => ActionAsync; state: SharedDict; @@ -426,13 +458,13 @@ export interface ISharedCtx = ICrea export interface IAtomCtx = IAtomCreateOptions> { mutate: (fnItem: AtomMutateFnLooseItem | AtomMutateFn) => MutateWitness; call: AtomCall; - callAsync: AtomAsyncCall; + callAsync: AtomCallAsync; action: (fn: AtomActionFnDef, desc?: FnDesc) => AtomAction; - actionAsync: (fn: AtomActionAsyncFnDef, desc?: FnDesc) => AtomActionAsync; + actionAsync: (fn: AtomActionAsyncFnDef, desc?: FnDesc) => AtomAsyncAction; state: Atom; setState: SetAtom; - sync: SyncFnBuilder>; - syncer: Syncer>; + sync: AtomSyncFnBuilder; + syncer: AtomSyncer; useState: (IUseSharedOptions?: IUseAtomOptions) => [T, SetAtom, IRenderInfo]; /** 获取 Mutate 状态 */ getMutateLoading: () => AtomSafeLoading; @@ -442,6 +474,7 @@ export interface IAtomCtx = IAtomCreate getActionLoading: () => AtomSafeLoading; /** 使用 Action 状态 */ useActionLoading: () => [AtomSafeLoading, SetState, IRenderInfo]; + setAtomVal: (val: T) => void; } interface IMutateFnParamsBase { @@ -451,18 +484,21 @@ interface IMutateFnParamsBase { } export interface IMutateFnParams extends IMutateFnParamsBase { - draft: T; + draftRoot: DraftRoot; + draft: DraftRoot; } export interface IAtomMutateFnParams extends IMutateFnParamsBase { - draft: Atom; + draftRoot: AtomDraftRoot; + draft: AtomDraft; } export interface IDataRule { /** * 当这些数据节点发生变化时和被读取时,对应的各种行为 + * 对于 atom ,回调里的 stateNode 是已拆箱的结果 */ - when: (state: T) => any | void; + when: (stateNode: T) => any[] | void; /** * 变化时,需要触发重渲染的和共享状态绑定关系的 id 对应的组件( id 可在调用 useShared 时可设定 ) */ @@ -478,7 +514,7 @@ export interface IDataRule { stopDep?: boolean; } -export interface ICreateOptionsBaseFull { +export interface ICreateOptionsBaseFull { /** * 模块名称,方便用户可以查看到语义化的状态树,不传递的话内部会以生成的自增序号 作为 key * 传递的话如果重复了,目前的策略仅仅是做个警告,helux 内部始终以生成的自增序号作为模块命名空间控制其他逻辑 @@ -536,13 +572,13 @@ export interface ICreateOptionsBaseFull { * 如:a|b|c|list|0,针对数组结构,stopDepthOfArr 会是 stopDepth + 1,多的一层用于记录下标值 */ stopArrDep: boolean; -} - -export interface ICreateOptionsFull extends ICreateOptionsBaseFull { /** * 配置状态变更联动视图更新规则 */ rules: IDataRule[]; +} + +export interface ICreateOptionsFull extends ICreateOptionsBaseFull { /** * 定义当前状态对其他状态有依赖的 mutate 函数集合或函数,它们将被自动执行,并收集到每个函数各自对应的上游数据依赖 */ @@ -553,11 +589,7 @@ export interface ICreateOptionsFull extends ICreateOptionsBaseFull { before: (params: IMutateFnParams) => void | Partial; } -export interface IAtomCreateOptionsFull extends ICreateOptionsBaseFull { - /** - * 配置状态变更联动视图更新规则 - */ - rules: IDataRule>[]; +export interface IAtomCreateOptionsFull extends ICreateOptionsBaseFull { /** * 定义当前状态对其他状态有依赖的 mutate 函数集合或函数,它们将被自动执行,并收集到每个函数各自对应的上游数据依赖 */ @@ -919,7 +951,9 @@ export interface IChangeInfoBase { } export interface IDataChangingInfo extends IChangeInfoBase { - draft: MutableDraft; + draftRoot: DraftRoot | AtomDraftRoot; + draft: DraftRoot | AtomDraft; + forAtom: boolean; } export interface IDataChangedInfo extends IChangeInfoBase { @@ -928,6 +962,8 @@ export interface IDataChangedInfo extends IChangeInfoBase { } export interface IMiddlewareCtx extends IDataChangingInfo { + /** setData 存储的数据,下一个中间件可获取 */ + data: Dict; setData(key: any, value: any); /** 中间件下标 */ idx: number; diff --git a/packages/helux/__tests__/action/atom.test.ts b/packages/helux/__tests__/action/atom.test.ts index 0c078fa0..1986be02 100644 --- a/packages/helux/__tests__/action/atom.test.ts +++ b/packages/helux/__tests__/action/atom.test.ts @@ -7,7 +7,7 @@ describe('create atom action', () => { test('add num action', async () => { const [numAtom] = atom(1); const addAtom = atomAction(numAtom)<[toAdd: number]>(({ draft, args }) => { - draft.val += args[0]; + return draft + args[0]; }); const snap = addAtom(10); @@ -23,7 +23,7 @@ describe('create atom action', () => { const [numAtom] = atom(1); const addAtom = atomActionAsync(numAtom)<[toAdd: number]>(async ({ setState, args }) => { await delay(100); - setState((draft) => (draft.val += args[0])); + setState((draft) => (draft + args[0])); }); const snap = await addAtom(10); diff --git a/packages/helux/__tests__/atom/change.test.ts b/packages/helux/__tests__/atom/change.test.ts index b994dfc2..1fb06893 100644 --- a/packages/helux/__tests__/atom/change.test.ts +++ b/packages/helux/__tests__/atom/change.test.ts @@ -1,5 +1,5 @@ import '@testing-library/jest-dom'; -import { atom, shallowCompare } from 'helux'; +import { atom, shallowCompare } from '../helux'; import { describe, expect, test } from 'vitest'; import { expectEqual, expectMatch, expectTruthy } from '../util'; @@ -12,7 +12,7 @@ describe('change atom', () => { test('change primitive by draft cb', async () => { const [numAtom, setNum] = atom(1); - setNum((draft) => (draft.val += 1)); + setNum((draft) => (draft + 1)); expect(numAtom.val === 2).toBeTruthy(); }); @@ -22,16 +22,22 @@ describe('change atom', () => { expect(dictAtom.val).toMatchObject({ a: 3, b: 4 }); }); + test('change dict by new state cb', async () => { + const [dictAtom, setAtom] = atom({ a: 1, b: 2 }); + setAtom(() => ({ a: 3, b: 4 })); + expect(dictAtom.val).toMatchObject({ a: 3, b: 4 }); + }); + test('change dict by draft cb', async () => { const [dictAtom, setAtom] = atom({ a: 1, b: 2 }); setAtom((draft) => { - draft.val.a = 3; + draft.a = 3; }); expect(dictAtom.val.a === 3).toBeTruthy(); expect(dictAtom.val.b === 2).toBeTruthy(); setAtom((draft) => { - draft.val.b = 4; + draft.b = 4; }); expect(dictAtom.val.a === 3).toBeTruthy(); expect(dictAtom.val.b === 4).toBeTruthy(); @@ -41,7 +47,7 @@ describe('change atom', () => { const [listAtom, setAtom] = atom([1, 2, 3]); expectTruthy(listAtom); setAtom((draft) => { - draft.val = [4, 5, 6]; + return [4, 5, 6]; }); expectMatch(listAtom.val, [4, 5, 6]); }); @@ -50,7 +56,7 @@ describe('change atom', () => { const [listAtom, setAtom] = atom([1, 2, 3]); expect(listAtom).toBeTruthy(); setAtom((draft) => { - draft.val[0] = 4; + draft[0] = 4; }); expectEqual(listAtom.val[0], 4); expectEqual(listAtom.val[1], 2); @@ -63,8 +69,8 @@ describe('change atom', () => { { a: 2, b: { name: 4 } }, ]); expect(listAtom).toBeTruthy(); - setAtom((draft) => { - draft.val = [{ a: 6, b: { name: 8 } }]; + setAtom(() => { + return [{ a: 6, b: { name: 8 } }]; }); expectMatch(listAtom.val, [{ a: 6, b: { name: 8 } }]); }); @@ -77,7 +83,7 @@ describe('change atom', () => { const prevItem0 = listAtom.val[0]; const prevItem1 = listAtom.val[1]; setAtom((draft) => { - draft.val[0].b.name = 100; + draft[0].b.name = 100; }); const currItem0 = listAtom.val[0]; const currItem1 = listAtom.val[1]; diff --git a/packages/helux/__tests__/atom/mutate.test.ts b/packages/helux/__tests__/atom/mutate.test.ts index c7db8884..45af5210 100644 --- a/packages/helux/__tests__/atom/mutate.test.ts +++ b/packages/helux/__tests__/atom/mutate.test.ts @@ -17,7 +17,7 @@ describe('create atom mutate', () => { test('single mutate, change draft', async () => { const [numAtom, setAtom] = atom(1); const [bAtom] = atom(0, { - mutate: (draft) => (draft.val = numAtom.val + 10), + mutate: (draft) => (numAtom.val + 10), }); expect(bAtom.val).toBe(11); @@ -32,10 +32,10 @@ describe('create atom mutate', () => { { mutate: { a: (draft) => { - draft.val.a = numAtom.val + 10; + draft.a = numAtom.val + 10; }, b: (draft) => { - draft.val.b = numAtom.val + 100; + draft.b = numAtom.val + 100; }, }, }, @@ -56,12 +56,12 @@ describe('create atom mutate', () => { mutate: { a: { fn: (draft) => { - draft.val.a = numAtom.val + 10; + draft.a = numAtom.val + 10; }, }, b: { fn: (draft) => { - draft.val.b = numAtom.val + 100; + draft.b = numAtom.val + 100; }, }, }, @@ -85,7 +85,7 @@ describe('create atom mutate', () => { a: { fn: (draft) => { runCount += 1; - draft.val.a = numAtom.val + 10; + draft.a = numAtom.val + 10; }, }, }, diff --git a/packages/helux/__tests__/atom/mutateAsync.test.ts b/packages/helux/__tests__/atom/mutateAsync.test.ts index 5cd0841a..c7daa6be 100644 --- a/packages/helux/__tests__/atom/mutateAsync.test.ts +++ b/packages/helux/__tests__/atom/mutateAsync.test.ts @@ -11,10 +11,10 @@ describe('create atom mutate', () => { mutate: [ { deps: () => [numAtom.val], - fn: (state, [num]) => state.val + num, + fn: (state, [num]) => state + num, task: async ({ setState, input: [num] }) => { await delay(100); - setState((draft) => (draft.val += num)); + setState((draft) => (draft + num)); }, }, ], @@ -31,10 +31,10 @@ describe('create atom mutate', () => { mutate: [ { deps: () => [numAtom.val], - fn: (state, [num]) => state.val + num, + fn: (state, [num]) => state + num, task: async ({ setState, input: [num] }) => { await delay(100); - setState((draft) => (draft.val += num)); + setState((draft) => (draft + num)); }, immediate: true, desc: 'changeVal', @@ -59,7 +59,7 @@ describe('create atom mutate', () => { deps: () => [numAtom.val], task: async ({ setState, input: [num] }) => { await delay(100); - setState((draft) => (draft.val += num)); + setState((draft) => (draft + num)); }, // immediate: undefined, }, @@ -78,7 +78,7 @@ describe('create atom mutate', () => { deps: () => [numAtom.val], task: async ({ setState, input: [num] }) => { await delay(100); - setState((draft) => (draft.val += num)); + setState((draft) => (draft + num)); }, immediate: false, }, diff --git a/packages/helux/__tests__/atom/render.test.tsx b/packages/helux/__tests__/atom/render.test.tsx index e4e71bed..8ea2a034 100644 --- a/packages/helux/__tests__/atom/render.test.tsx +++ b/packages/helux/__tests__/atom/render.test.tsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import '@testing-library/jest-dom'; import { act, render, screen, waitFor } from '@testing-library/react'; import { describe, expect, test } from 'vitest'; @@ -51,7 +52,7 @@ describe('use atom', () => { act(() => { setAtom((draft) => { - draft.val.a = 100; + draft.a = 100; }); }); diff --git a/packages/helux/__tests__/derive/deriveAtom.test.ts b/packages/helux/__tests__/derive/deriveAtom.test.ts index a5a95aa4..a6957937 100644 --- a/packages/helux/__tests__/derive/deriveAtom.test.ts +++ b/packages/helux/__tests__/derive/deriveAtom.test.ts @@ -24,7 +24,7 @@ describe('derive atom', () => { expect(result2.val.plus200Double).toBe(402); setAtom((draft) => { - draft.val.a = 10; + draft.a = 10; }); expect(result1.val.plus100).toBe(110); expect(result1.val.plus200).toBe(210); diff --git a/packages/helux/__tests__/hooks/useAtom.test.ts b/packages/helux/__tests__/hooks/useAtom.test.ts index a6d125eb..1244835e 100644 --- a/packages/helux/__tests__/hooks/useAtom.test.ts +++ b/packages/helux/__tests__/hooks/useAtom.test.ts @@ -28,17 +28,13 @@ describe('useAtom', () => { test('top set by draft cb', async () => { await runSetTest((params) => { - params.topSet((draft) => { - draft.val = 2; - }); + params.topSet(() => 2); }); }); test('hook set by draft cb', async () => { await runSetTest((params) => { - params.hookSet((draft) => { - draft.val = 2; - }); + params.hookSet(() => 2); }); }); diff --git a/packages/helux/__tests__/share/change.test.ts b/packages/helux/__tests__/share/change.test.ts index 48632bf8..a9424fb6 100644 --- a/packages/helux/__tests__/share/change.test.ts +++ b/packages/helux/__tests__/share/change.test.ts @@ -1,5 +1,5 @@ import '@testing-library/jest-dom'; -import { getSnap, share } from 'helux'; +import { getSnap, share } from '../helux'; import { isDiff } from 'limu'; import { describe, expect, test } from 'vitest'; diff --git a/packages/helux/__tests__/watch/watchAtom.test.ts b/packages/helux/__tests__/watch/watchAtom.test.ts index 7dfff4d3..e32e10cf 100644 --- a/packages/helux/__tests__/watch/watchAtom.test.ts +++ b/packages/helux/__tests__/watch/watchAtom.test.ts @@ -50,7 +50,7 @@ describe('watch atom', () => { ); setAtom((draft) => { - draft.val.books.push({ name: 'book2' }); + draft.books.push({ name: 'book2' }); }); expect(triggerCount).toBe(1); diff --git a/packages/helux/src/index.ts b/packages/helux/src/index.ts index 5ccfd445..30954560 100644 --- a/packages/helux/src/index.ts +++ b/packages/helux/src/index.ts @@ -60,6 +60,8 @@ export const { emit, on, // util api + currentDraftRoot, + setAtomVal, storeSrv, shallowCompare, isDiff, @@ -75,5 +77,3 @@ export const { EVENT_NAME, LOADING_MODE, } = api; - -export default api;