diff --git a/designable/antd/package.json b/designable/antd/package.json index 392820c4ffa..aa24d614306 100644 --- a/designable/antd/package.json +++ b/designable/antd/package.json @@ -30,7 +30,7 @@ "start": "webpack-dev-server --config playground/webpack.dev.ts" }, "devDependencies": { - "@designable/react-settings-form": "^0.3.22", + "@designable/react-settings-form": "^0.3.24", "autoprefixer": "^9.0", "file-loader": "^5.0.2", "fs-extra": "^8.1.0", @@ -56,9 +56,9 @@ "react-is": ">=16.8.0 || >=17.0.0" }, "dependencies": { - "@designable/core": "^0.3.22", - "@designable/formily": "^0.3.22", - "@designable/react": "^0.3.22", + "@designable/core": "^0.3.24", + "@designable/formily": "^0.3.24", + "@designable/react": "^0.3.24", "@formily/antd": "2.0.0-beta.73", "@formily/core": "2.0.0-beta.73", "@formily/react": "2.0.0-beta.73", diff --git a/docs/guide/advanced/destructor.zh-CN.md b/docs/guide/advanced/destructor.zh-CN.md index 6f493688190..e23230c4987 100644 --- a/docs/guide/advanced/destructor.zh-CN.md +++ b/docs/guide/advanced/destructor.zh-CN.md @@ -6,7 +6,7 @@ 但从前端组件化角度来看,数组结构又是最佳的; -所以哪一边都有其道理,可惜的是,每次都只能前端取消化这样一个不平等条约,不过,有了 Formily,你就完全不需要为这样一个尴尬局面而难受了,**Formily 提供了解构路径的能力,可以帮助用户快速解决这类问题。**,下面可以看看例子 +所以哪一边都有其道理,可惜的是,每次都只能前端去消化这样一个不平等条约,不过,有了 Formily,你就完全不需要为这样一个尴尬局面而难受了,**Formily 提供了解构路径的能力,可以帮助用户快速解决这类问题。**,下面可以看看例子 ## Markup Schema 案例 diff --git a/docs/guide/scenes/more.zh-CN.md b/docs/guide/scenes/more.zh-CN.md index 6161bbbfcff..7ba60b34019 100644 --- a/docs/guide/scenes/more.zh-CN.md +++ b/docs/guide/scenes/more.zh-CN.md @@ -1,5 +1,5 @@ # 更多场景 -因为Formily在表单层面上是一个非常完备的方案,而且还很灵活,支持的场景非常多,但是场景案例,我们无法一一列举。 +因为 Formily 在表单层面上是一个非常完备的方案,而且还很灵活,支持的场景非常多,但是场景案例,我们无法一一列举。 -所以,还是希望社区能帮助Formily完善更多场景案例!我们会不胜感激!😀 \ No newline at end of file +所以,还是希望社区能帮助 Formily 完善更多场景案例!我们会不胜感激!😀 diff --git a/packages/json-schema/src/transformer.ts b/packages/json-schema/src/transformer.ts index 2c5980efa11..58966e34714 100644 --- a/packages/json-schema/src/transformer.ts +++ b/packages/json-schema/src/transformer.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import { untracked } from '@formily/reactive' import { isBool, @@ -391,7 +392,10 @@ const getReactions = (schema: ISchema, options: ISchemaFieldFactoryOptions) => { } } - const queryDepdency = (field: Formily.Core.Models.Field, pattern: string) => { + const queryDependency = ( + field: Formily.Core.Models.Field, + pattern: string + ) => { const [target, path] = String(pattern).split(/\s*#\s*/) return field.query(target).getIn(path || 'value') } @@ -401,12 +405,12 @@ const getReactions = (schema: ISchema, options: ISchemaFieldFactoryOptions) => { dependencies: string[] | object ) => { if (isArr(dependencies)) { - return dependencies.map((pattern) => queryDepdency(field, pattern)) + return dependencies.map((pattern) => queryDependency(field, pattern)) } else if (isPlainObj(dependencies)) { return reduce( dependencies, (buf, pattern, key) => { - buf[key] = queryDepdency(field, pattern) + buf[key] = queryDependency(field, pattern) return buf }, {} diff --git a/packages/reactive/src/__tests__/autorun.spec.ts b/packages/reactive/src/__tests__/autorun.spec.ts index d92c999789f..cbd1b69b1f5 100644 --- a/packages/reactive/src/__tests__/autorun.spec.ts +++ b/packages/reactive/src/__tests__/autorun.spec.ts @@ -84,7 +84,51 @@ test('reaction dirty check', () => { expect(handler).toBeCalledTimes(0) }) -test('reaction in reaction', () => { +test('reaction with shallow equals', () => { + const obs: any = { + aa: { bb: 123 }, + } + define(obs, { + aa: observable.ref, + }) + const handler = jest.fn() + reaction(() => { + return obs.aa + }, handler) + obs.aa = { bb: 123 } + expect(handler).toBeCalledTimes(1) +}) + +test('reaction with deep equals', () => { + const obs: any = { + aa: { bb: 123 }, + } + define(obs, { + aa: observable.ref, + }) + const handler = jest.fn() + reaction( + () => { + return obs.aa + }, + handler, + { + equals: (a, b) => JSON.stringify(a) === JSON.stringify(b), + } + ) + obs.aa = { bb: 123 } + expect(handler).toBeCalledTimes(0) +}) + +test('autorun direct recursive react', () => { + const obs = observable({ value: 1 }) + autorun(() => { + obs.value++ + }) + expect(obs.value).toEqual(2) +}) + +test('autorun direct recursive react with if', () => { const obs1 = observable({}) const obs2 = observable({}) const fn = jest.fn() @@ -96,6 +140,69 @@ test('reaction in reaction', () => { fn(obs1.value, obs2.value) }) obs2.value = '222' - expect(fn).toHaveBeenCalledWith('111', undefined) + expect(fn).not.toHaveBeenCalledWith('111', undefined) + expect(fn).not.toHaveBeenCalledWith('111', '222') +}) + +test('autorun indirect recursive react', () => { + const obs1 = observable({}) + const obs2 = observable({}) + const obs3 = observable({}) + autorun(() => { + obs1.value = obs2.value + 1 + }) + autorun(() => { + obs2.value = obs3.value + 1 + }) + autorun(() => { + if (obs1.value) { + obs3.value = obs1.value + 1 + } else { + obs3.value = 0 + } + }) + obs3.value = 1 + expect(obs1.value).toEqual(3) +}) + +test('autorun indirect alive recursive react', () => { + const aa = observable({}) + const bb = observable({}) + const cc = observable({}) + + batch(() => { + autorun(() => { + if (aa.value) { + bb.value = aa.value + 1 + } + }) + autorun(() => { + if (aa.value && bb.value) { + cc.value = aa.value + bb.value + } + }) + batch(() => { + aa.value = 1 + }) + }) + expect(aa.value).toEqual(1) + expect(bb.value).toEqual(2) + expect(cc.value).toEqual(3) +}) + +test('autorun direct recursive react with head track', () => { + const obs1 = observable({}) + const obs2 = observable({}) + const fn = jest.fn() + autorun(() => { + const obs2Value = obs2.value + if (!obs1.value) { + obs1.value = '111' + return + } + fn(obs1.value, obs2Value) + }) + obs2.value = '222' + expect(fn).not.toHaveBeenCalledWith('111', undefined) expect(fn).toHaveBeenCalledWith('111', '222') }) diff --git a/packages/reactive/src/__tests__/tracker.spec.ts b/packages/reactive/src/__tests__/tracker.spec.ts index aaaaa484338..4d705fdf029 100644 --- a/packages/reactive/src/__tests__/tracker.spec.ts +++ b/packages/reactive/src/__tests__/tracker.spec.ts @@ -34,6 +34,6 @@ test('nested tracker', () => { obs.value = 123 expect(fn).toBeCalledWith(321) expect(fn).toBeCalledWith(123) - expect(fn).toBeCalledTimes(3) + expect(fn).toBeCalledTimes(2) tracker.dispose() }) diff --git a/packages/reactive/src/__tests__/untracked.spec.ts b/packages/reactive/src/__tests__/untracked.spec.ts new file mode 100644 index 00000000000..d6c3b33f6c4 --- /dev/null +++ b/packages/reactive/src/__tests__/untracked.spec.ts @@ -0,0 +1,17 @@ +import { untracked, observable, autorun } from '../' + +test('basic untracked', () => { + const obs = observable({}) + const fn = jest.fn() + autorun(() => { + untracked(() => { + fn(obs.value) + }) + }) + obs.value = 123 + expect(fn).toBeCalledTimes(1) +}) + +test('no params untracked', () => { + untracked() +}) diff --git a/packages/reactive/src/annotations/computed.ts b/packages/reactive/src/annotations/computed.ts index 91a0027debd..15ff00e96f5 100644 --- a/packages/reactive/src/annotations/computed.ts +++ b/packages/reactive/src/annotations/computed.ts @@ -54,17 +54,13 @@ export const computed: IComputed = createAnnotation( store.value = getter?.call?.(context) } function reaction() { - const reactionIndex = ReactionStack.indexOf(reaction) - if (reactionIndex === -1) { + if (ReactionStack.indexOf(reaction) === -1) { try { ReactionStack.push(reaction) compute() } finally { ReactionStack.pop() } - } else { - ReactionStack.splice(reactionIndex, 1) - reaction() } } reaction._name = 'ComputedReaction' diff --git a/packages/reactive/src/autorun.ts b/packages/reactive/src/autorun.ts index dda5af48cf5..0a57104e627 100644 --- a/packages/reactive/src/autorun.ts +++ b/packages/reactive/src/autorun.ts @@ -1,10 +1,11 @@ import { batchEnd, batchStart, + untrackEnd, + untrackStart, disposeBindingReactions, releaseBindingReactions, } from './reaction' -import { untracked } from './untracked' import { isFn } from './checkers' import { ReactionStack } from './environment' import { Reaction, IReactionOptions } from './types' @@ -14,7 +15,7 @@ interface IValue { oldValue?: any } -interface ITracked { +interface IInitialized { current?: boolean } @@ -25,22 +26,22 @@ interface IDirty { export const autorun = (tracker: Reaction, name = 'AutoRun') => { const reaction = () => { if (!isFn(tracker)) return - const reactionIndex = ReactionStack.indexOf(reaction) - if (reactionIndex === -1) { + if (reaction._boundary > 0) return + if (ReactionStack.indexOf(reaction) === -1) { releaseBindingReactions(reaction) try { - ReactionStack.push(reaction) batchStart() + ReactionStack.push(reaction) tracker() } finally { - batchEnd() ReactionStack.pop() + reaction._boundary++ + batchEnd() + reaction._boundary = 0 } - } else { - ReactionStack.splice(reactionIndex, 1) - reaction() } } + reaction._boundary = 0 reaction._name = name reaction() return () => { @@ -58,7 +59,7 @@ export const reaction = ( ...options, } const value: IValue = {} - const tracked: ITracked = {} + const initialized: IInitialized = {} const dirty: IDirty = {} const dirtyCheck = () => { if (isFn(realOptions.equals)) @@ -66,18 +67,40 @@ export const reaction = ( return value.oldValue !== value.currentValue } - return autorun(() => { - value.currentValue = tracker() - dirty.current = dirtyCheck() + const reaction = () => { + if (ReactionStack.indexOf(reaction) === -1) { + releaseBindingReactions(reaction) + try { + ReactionStack.push(reaction) + value.currentValue = tracker() + dirty.current = dirtyCheck() + } finally { + ReactionStack.pop() + } + } + if ( - (dirty.current && tracked.current) || - (!tracked.current && realOptions.fireImmediately) + (dirty.current && initialized.current) || + (!initialized.current && realOptions.fireImmediately) ) { - untracked(() => { + try { + batchStart() + untrackStart() if (isFn(subscriber)) subscriber(value.currentValue) - }) + } finally { + untrackEnd() + batchEnd() + } } + value.oldValue = value.currentValue - tracked.current = true - }, realOptions.name) + initialized.current = true + } + + reaction._name = realOptions.name + reaction() + + return () => { + disposeBindingReactions(reaction) + } } diff --git a/packages/reactive/src/reaction.ts b/packages/reactive/src/reaction.ts index 0f1f50d2514..c3f25ce6000 100644 --- a/packages/reactive/src/reaction.ts +++ b/packages/reactive/src/reaction.ts @@ -22,9 +22,7 @@ const addRawReactionsMap = ( if (reactionsMap) { const reactions = reactionsMap.get(key) if (reactions) { - if (!reactions.has(reaction)) { - reactions.add(reaction) - } + reactions.add(reaction) } else { reactionsMap.set(key, new Set([reaction])) } @@ -42,9 +40,7 @@ const addReactionsMapToReaction = ( ) => { const bindSet = reaction._reactionsSet if (bindSet) { - if (!bindSet.has(reactionsMap)) { - bindSet.add(reactionsMap) - } + bindSet.add(reactionsMap) } else { reaction._reactionsSet = new Set([reactionsMap]) } @@ -74,13 +70,9 @@ const runReactions = (target: any, key: PropertyKey) => { if (reaction._isComputed) { reaction._scheduler(reaction) } else if (isScopeBatching()) { - if (!PendingScopeReactions.has(reaction)) { - PendingScopeReactions.add(reaction) - } + PendingScopeReactions.add(reaction) } else if (isBatching()) { - if (!PendingReactions.has(reaction)) { - PendingReactions.add(reaction) - } + PendingReactions.add(reaction) } else { if (isFn(reaction._scheduler)) { reaction._scheduler(reaction) @@ -112,22 +104,20 @@ export const bindComputedReactions = (reaction: Reaction) => { if (isFn(reaction)) { const current = ReactionStack[ReactionStack.length - 1] if (current) { - const computeds = current._computedsSet - if (computeds) { - if (!computeds.has(reaction)) { - computeds.add(reaction) - } + const computes = current._computesSet + if (computes) { + computes.add(reaction) } else { - current._computedsSet = new Set([reaction]) + current._computesSet = new Set([reaction]) } } } } export const suspendComputedReactions = (reaction: Reaction) => { - const computeds = reaction._computedsSet - if (computeds) { - computeds.forEach((reaction) => { + const computes = reaction._computesSet + if (computes) { + computes.forEach((reaction) => { const reactions = getReactionsFromTargetKey( reaction._context, reaction._property diff --git a/packages/reactive/src/tracker.ts b/packages/reactive/src/tracker.ts index cf119d90636..1202eb5f07f 100644 --- a/packages/reactive/src/tracker.ts +++ b/packages/reactive/src/tracker.ts @@ -16,24 +16,24 @@ export class Tracker { ) { this.track._scheduler = scheduler this.track._name = name + this.track._boundary = 0 } track: Reaction = (tracker: Reaction) => { if (!isFn(tracker)) return this.results - const reactionIndex = ReactionStack.indexOf(this.track) - if (reactionIndex === -1) { + if (this.track._boundary > 0) return + if (ReactionStack.indexOf(this.track) === -1) { releaseBindingReactions(this.track) try { - ReactionStack.push(this.track) batchStart() + ReactionStack.push(this.track) this.results = tracker() } finally { - batchEnd() ReactionStack.pop() + this.track._boundary++ + batchEnd() + this.track._boundary = 0 } - } else { - ReactionStack.splice(reactionIndex, 1) - this.track(tracker) } return this.results } diff --git a/packages/reactive/src/types.ts b/packages/reactive/src/types.ts index 9a8fa73e42e..3e1f4fbde3f 100644 --- a/packages/reactive/src/types.ts +++ b/packages/reactive/src/types.ts @@ -45,12 +45,13 @@ export type ObservableListener = (operation: IOperation) => void export type ObservablePath = Array export type Reaction = ((...args: any[]) => any) & { + _boundary?: number _name?: string _isComputed?: boolean _dirty?: boolean _context?: any _property?: PropertyKey - _computedsSet?: Set + _computesSet?: Set _reactionsSet?: Set _scheduler?: (reaction: Reaction) => void } diff --git a/yarn.lock b/yarn.lock index 27319998e31..108fbf89de0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1167,12 +1167,12 @@ resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz#c3c5ae543c897caa9c2a68630bed355be5f9990f" integrity sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ== -"@designable/core@0.3.21", "@designable/core@^0.3.21": - version "0.3.21" - resolved "https://registry.yarnpkg.com/@designable/core/-/core-0.3.21.tgz#452abaf499360e14fcc260df6ad2b4f6951af38e" - integrity sha512-66PkOLwFcSDhWnwK3oU/tdawnPVGh2QXGjRg9ubJF1o8XQiB2ANWE0z1zIJwd2E0ULxLvtBlVDOUei6IB29TXA== +"@designable/core@0.3.24", "@designable/core@^0.3.24": + version "0.3.24" + resolved "https://registry.yarnpkg.com/@designable/core/-/core-0.3.24.tgz#6a8aa44b4a86c4378986484e6bda73206a7361bb" + integrity sha512-AV4X/3tb60btaJKQL0Gxf/CyViIgalNv0waigrHj1XJYoyPfdcJc/sS7Ae/UawolREo674H/yPjL3StOSfP3QQ== dependencies: - "@designable/shared" "0.3.21" + "@designable/shared" "0.3.24" "@formily/json-schema" "^2.0.0-beta.68" "@formily/path" "^2.0.0-beta.68" "@formily/reactive" "^2.0.0-beta.68" @@ -1188,25 +1188,25 @@ "@formily/reactive" "^2.0.0-beta.50" "@juggle/resize-observer" "^3.3.1" -"@designable/formily@^0.3.21": - version "0.3.21" - resolved "https://registry.yarnpkg.com/@designable/formily/-/formily-0.3.21.tgz#3671856d7a525e7f325dedf880dfe35567f37a38" - integrity sha512-DmqGQo8Zo3psxVOjAscV0M8mx6TPr96L4yPZbIp8p9TXhlDYLe8baQDrxO6PbJNYWTbT9AqIk1h5zHup/avswg== +"@designable/formily@^0.3.24": + version "0.3.24" + resolved "https://registry.yarnpkg.com/@designable/formily/-/formily-0.3.24.tgz#0ab8acfd41a1ce6e100296f2d5dabeef69ad4061" + integrity sha512-/fRaJcUUjR7Me7YmT+NUo/CfvKNnjRF+fLCD69qwyflo35UfgXob/dknW+lk+3rJO5JlqKWgyItRY9rxMyvR6w== dependencies: - "@designable/core" "0.3.21" - "@designable/shared" "0.3.21" + "@designable/core" "0.3.24" + "@designable/shared" "0.3.24" "@formily/core" "^2.0.0-beta.68" "@formily/json-schema" "^2.0.0-beta.68" -"@designable/react-settings-form@^0.3.21": - version "0.3.21" - resolved "https://registry.yarnpkg.com/@designable/react-settings-form/-/react-settings-form-0.3.21.tgz#d1ef052b55a83a4e765f226a9511af0fc9c8e9c1" - integrity sha512-Nq/J8CFV9XPmUtWylHgrb8qCkYhGm307/qfznuOoKm1MYPVSwENx0alJPl0ELdzgK89itx7PGtXrwfwsmonnPg== +"@designable/react-settings-form@^0.3.24": + version "0.3.24" + resolved "https://registry.yarnpkg.com/@designable/react-settings-form/-/react-settings-form-0.3.24.tgz#4b91c7f43725da6fa482aad710fc38f996b561c2" + integrity sha512-GYk5MIxdFWb/7zSEsmfeIUxd+jRFGZ93Gejs+795WIKTc70WEl/7SgKSa/Uhir3XYHA3eyhRfXqmIZ7lTAjkSQ== dependencies: "@babel/parser" "^7.14.7" - "@designable/core" "0.3.21" - "@designable/react" "0.3.21" - "@designable/shared" "0.3.21" + "@designable/core" "0.3.24" + "@designable/react" "0.3.24" + "@designable/shared" "0.3.24" "@formily/antd" "^2.0.0-beta.68" "@formily/core" "^2.0.0-beta.68" "@formily/react" "^2.0.0-beta.68" @@ -1218,13 +1218,13 @@ prettier "^2.3.2" react-color "^2.19.3" -"@designable/react@0.3.21", "@designable/react@^0.3.21": - version "0.3.21" - resolved "https://registry.yarnpkg.com/@designable/react/-/react-0.3.21.tgz#3f41790fe46c026c3b70be530e06a107e79ea7cc" - integrity sha512-hp2q8+U6mkoeMuBcIDjRpkNx7tUZ7SSgdKblD2CRj7AgFbGTCC0I+YtJs92irUAKVeV1SSxO+Zx/qVa2XSu3Dg== +"@designable/react@0.3.24", "@designable/react@^0.3.24": + version "0.3.24" + resolved "https://registry.yarnpkg.com/@designable/react/-/react-0.3.24.tgz#e7732056a77d06ac134daf0535b1c65c9a92370f" + integrity sha512-Y2oDEerDMIO2FIMM5gOyFEhfTFXNKzryY9WIdSZ7QnyUAvxtxFnZ/Q8jgoYEgGIH2WKWh7HvaTtZh/qho7nIKg== dependencies: - "@designable/core" "0.3.21" - "@designable/shared" "0.3.21" + "@designable/core" "0.3.24" + "@designable/shared" "0.3.24" "@formily/reactive" "^2.0.0-beta.68" "@formily/reactive-react" "^2.0.0-beta.68" "@juggle/resize-observer" "^3.3.1" @@ -1236,10 +1236,10 @@ dependencies: requestidlecallback "^0.3.0" -"@designable/shared@0.3.21": - version "0.3.21" - resolved "https://registry.yarnpkg.com/@designable/shared/-/shared-0.3.21.tgz#ed7f29c5dca33ff0a7957e022eecbf077d89212f" - integrity sha512-97oXQsfIU2PGQCIjrPpFB36eifsTveQ5oaFcBNUzDaAbscNX134lCPmukshOorgp2ZQeXod8ftpbmQoQJR+2/A== +"@designable/shared@0.3.24": + version "0.3.24" + resolved "https://registry.yarnpkg.com/@designable/shared/-/shared-0.3.24.tgz#a58fc51afdf5a0bcd36de7bb9a3de1561c5e523b" + integrity sha512-FfMOlsJV1eur/71hWciBPtuisqmdZy6PyqN/DFtlQVA4Z+kcTHTgaPM7j99V+1ufdqHUOHCtRLPp8PdsGL1J4Q== dependencies: requestidlecallback "^0.3.0"