diff --git a/docs/index.md b/docs/index.md index 9e2db9bc1a8..646580a3f6c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,132 +1 @@ ---- -title: Formily - Alibaba unified front-end form solution -order: 10 -hero: - title: Alibaba Formily - desc: Alibaba Unified Front-end Form Solution - actions: - - text: Introduction - link: /guide - - text: Quick start - link: /guide/quick-start -features: - - icon: https://img.alicdn.com/imgextra/i2/O1CN016i72sH1c5wh1kyy9U_!!6000000003550-55-tps-800-800.svg - title: Easier to Use - desc: Out of the box, rich cases - - icon: https://img.alicdn.com/imgextra/i1/O1CN01bHdrZJ1rEOESvXEi5_!!6000000005599-55-tps-800-800.svg - title: More Efficient - desc: Fool writing, ultra-high performance - - icon: https://img.alicdn.com/imgextra/i3/O1CN01xlETZk1G0WSQT6Xii_!!6000000000560-55-tps-800-800.svg - title: More Professional - desc: Complete, flexible and elegant -footer: Open-source MIT Licensed | Copyright © 2019-present
Powered by self ---- - -```tsx -/** - * inline: true - */ -import React from 'react' -import { Section } from './site/Section' -import './site/styles.less' - -export default () => ( -
- -
-) -``` - -```tsx -/** - * inline: true - */ -import React from 'react' -import { Section } from './site/Section' -import './site/styles.less' - -export default () => ( -
- - - -
-) -``` - -```tsx -/** - * inline: true - */ -import React from 'react' -import { Section } from './site/Section' -import './site/styles.less' - -export default () => ( -
- - - -
-) -``` - -```tsx -/** - * inline: true - */ -import React from 'react' -import { Section } from './site/Section' -import { Contributors } from './site/Contributors' -import './site/styles.less' - -export default () => ( -
- -
-) -``` - -```tsx -/** - * inline: true - */ -import React from 'react' -import { Section } from './site/Section' -import { QrCode, QrCodeGroup } from './site/QrCode' -import './site/styles.less' - -export default () => ( -
- - - -
-) -``` + diff --git a/docs/index.tsx b/docs/index.tsx new file mode 100644 index 00000000000..22e56f15428 --- /dev/null +++ b/docs/index.tsx @@ -0,0 +1,98 @@ +import React from 'react' + +import { autorun, batch, reaction, observable } from '@formily/reactive' + +const fieldA = observable({ + value: '', + visible: true, +}) + +const fieldB = observable({ + value: '', + visible: true, + cache: '', +}) + +const fieldC = observable({ + value: '', + visible: true, + cache: '', +}) + +// ===== fieldB reaction ===== +reaction( + () => fieldB.value, + () => { + if (fieldB.value && fieldB.visible === false) { + fieldB.cache = fieldB.value + // 删除 fieldB.value 时,会重新 runReaction + delete fieldB.value + } + } +) + +reaction( + () => fieldB.visible, + () => { + if (fieldB.visible === true) { + // debugger + console.log('fieldB.cache: ', fieldB.cache) + // 执行到这里时,不会执行 fieldB.value 的 autorun,因为在上面 delete fieldB.value 时,已经执行了 + fieldB.value = fieldB.cache + } + } +) + +// ===== fieldC reaction ===== +reaction( + () => fieldC.value, + () => { + if (fieldC.value && fieldC.visible === false) { + fieldC.cache = fieldC.value + delete fieldC.value + } + } +) + +reaction( + () => fieldC.visible, + () => { + if (fieldC.visible === true) { + fieldC.value = fieldC.cache + } + } +) + +// ===== schema 渲染 ===== +autorun(() => { + fieldB.visible = fieldA.value === 'fieldA' +}, 'A') + +autorun(() => { + fieldC.visible = fieldB.value === 'fieldB' +}, 'B') + +// fieldB.value = 'fieldB' +// fieldC.value = 'fieldC' +// fieldA.value = 'fieldA' + +batch(() => { + fieldB.value = 'fieldB' + fieldC.value = 'fieldC' + // debugger + fieldA.value = 'fieldA' + // window.xxx = true +}) + +console.log( + 'fieldA.visible:', + fieldA.visible, + 'fieldB.visible:', + fieldB.visible, + 'fieldC.visible:', + fieldC.visible +) + +const App = () =>
123123
+ +export default App diff --git a/packages/reactive/src/__tests__/autorun.spec.ts b/packages/reactive/src/__tests__/autorun.spec.ts index 2e74766b38a..f1aaef9cf2d 100644 --- a/packages/reactive/src/__tests__/autorun.spec.ts +++ b/packages/reactive/src/__tests__/autorun.spec.ts @@ -24,6 +24,12 @@ test('autorun', () => { expect(handler).toBeCalledTimes(2) }) +test('autorun first argument is not a function', () => { + autorun({} as any) + autorun(1 as any) + autorun('1' as any) +}) + test('reaction', () => { const obs = observable({ aa: { @@ -741,3 +747,113 @@ test('reaction recollect dependencies', () => { expect(fn2).toBeCalledTimes(2) expect(trigger2).toBeCalledTimes(2) }) + +test('multiple source update', () => { + const obs = observable({}) + + const fn1 = jest.fn() + const fn2 = jest.fn() + + autorun(() => { + const A = obs.A + const B = obs.B + if (A !== undefined && B !== undefined) { + obs.C = A / B + fn1() + } + }) + + autorun(() => { + const C = obs.C + const B = obs.B + if (C !== undefined && B !== undefined) { + obs.D = C * B + fn2() + } + }) + + obs.A = 1 + obs.B = 2 + + expect(fn1).toBeCalledTimes(1) + expect(fn2).toBeCalledTimes(1) +}) + +test('same source in nest update', () => { + const obs = observable({}) + + const fn1 = jest.fn() + + autorun(() => { + const B = obs.B + obs.B = 'B' + fn1() + return B + }) + + obs.B = 'B2' + + expect(fn1).toBeCalledTimes(2) +}) + +test('repeat execute autorun cause by deep indirect dependency', () => { + const obs: any = observable({ aa: 1, bb: 1, cc: 1 }) + const fn = jest.fn() + const fn2 = jest.fn() + const fn3 = jest.fn() + autorun(() => fn((obs.aa = obs.bb + obs.cc))) + autorun(() => fn2((obs.bb = obs.aa + obs.cc))) + autorun(() => fn3((obs.cc = obs.aa + obs.bb))) + + expect(fn).toBeCalledTimes(4) + expect(fn2).toBeCalledTimes(4) + expect(fn3).toBeCalledTimes(3) +}) + +test('batch execute autorun cause by deep indirect dependency', () => { + const obs: any = observable({ aa: 1, bb: 1, cc: 1 }) + const fn = jest.fn() + const fn2 = jest.fn() + const fn3 = jest.fn() + autorun(() => fn((obs.aa = obs.bb + obs.cc))) + autorun(() => fn2((obs.bb = obs.aa + obs.cc))) + autorun(() => fn3((obs.cc = obs.aa + obs.bb))) + + expect(fn).toBeCalledTimes(4) + expect(fn2).toBeCalledTimes(4) + expect(fn3).toBeCalledTimes(3) + + fn.mockClear() + fn2.mockClear() + fn3.mockClear() + + batch(() => { + obs.aa = 100 + obs.bb = 100 + obs.cc = 100 + }) + + expect(fn).toBeCalledTimes(2) + expect(fn2).toBeCalledTimes(2) + expect(fn3).toBeCalledTimes(2) +}) + +test('multiple update should trigger only one', () => { + const obs = observable({ aa: 1, bb: 1 }) + + autorun(() => { + obs.aa = obs.bb + 1 + obs.bb = obs.aa + 1 + }) + + expect(obs.aa).toBe(2) + expect(obs.bb).toBe(3) + + autorun(() => { + obs.aa = obs.bb + 1 + obs.bb = obs.aa + 1 + }) + + expect(obs.aa).toBe(6) + expect(obs.bb).toBe(7) +}) diff --git a/packages/reactive/src/__tests__/tracker.spec.ts b/packages/reactive/src/__tests__/tracker.spec.ts index c0da949ea81..ad4d02363f6 100644 --- a/packages/reactive/src/__tests__/tracker.spec.ts +++ b/packages/reactive/src/__tests__/tracker.spec.ts @@ -1,4 +1,4 @@ -import { Tracker, observable } from '../' +import { Tracker, observable, batch } from '../' test('base tracker', () => { const obs = observable({}) @@ -18,6 +18,13 @@ test('base tracker', () => { tracker.dispose() }) +test('track argument is not a function', () => { + const scheduler = () => {} + const tracker = new Tracker(scheduler) + + tracker.track({}) +}) + test('nested tracker', () => { const obs = observable({}) const fn = jest.fn() @@ -91,3 +98,154 @@ test('shared scheduler with multi tracker(mock react strict mode)', () => { expect(scheduler1).toBeCalledTimes(1) expect(scheduler2).toBeCalledTimes(0) }) + +test('multiple source update', () => { + const obs = observable({}) + + const fn1 = jest.fn() + const fn2 = jest.fn() + + const view1 = () => { + const A = obs.A + const B = obs.B + if (A !== undefined && B !== undefined) { + obs.C = A / B + fn1() + } + } + const scheduler1 = () => { + tracker1.track(view1) + } + + const tracker1 = new Tracker(scheduler1) + + const view2 = () => { + const C = obs.C + const B = obs.B + if (C !== undefined && B !== undefined) { + obs.D = C * B + fn2() + } + } + const scheduler2 = () => { + tracker2.track(view2) + } + + const tracker2 = new Tracker(scheduler2) + + tracker1.track(view1) + tracker2.track(view2) + + obs.A = 1 + obs.B = 2 + + expect(fn1).toBeCalledTimes(1) + expect(fn2).toBeCalledTimes(1) + + tracker1.dispose() + tracker2.dispose() +}) + +test('repeat execute tracker cause by deep indirect dependency', () => { + const obs: any = observable({ aa: 1, bb: 1, cc: 1 }) + + const fn1 = jest.fn() + const fn2 = jest.fn() + const fn3 = jest.fn() + + const view1 = () => { + fn1((obs.aa = obs.bb + obs.cc)) + } + const scheduler1 = () => { + tracker1.track(view1) + } + + const tracker1 = new Tracker(scheduler1) + + const view2 = () => { + fn2((obs.bb = obs.aa + obs.cc)) + } + const scheduler2 = () => { + tracker2.track(view2) + } + + const tracker2 = new Tracker(scheduler2) + + const view3 = () => { + fn3((obs.cc = obs.aa + obs.bb)) + } + const scheduler3 = () => { + tracker3.track(view3) + } + + const tracker3 = new Tracker(scheduler3) + + tracker1.track(view1) + tracker2.track(view2) + tracker3.track(view3) + + expect(fn1).toBeCalledTimes(4) + expect(fn2).toBeCalledTimes(4) + expect(fn3).toBeCalledTimes(3) + + tracker1.dispose() + tracker2.dispose() + tracker3.dispose() +}) + +test('batch execute tracker cause by deep indirect dependency', () => { + const obs: any = observable({ aa: 1, bb: 1, cc: 1 }) + + const fn1 = jest.fn() + const fn2 = jest.fn() + const fn3 = jest.fn() + + const view1 = () => { + fn1((obs.aa = obs.bb + obs.cc)) + } + const scheduler1 = () => { + tracker1.track(view1) + } + + const tracker1 = new Tracker(scheduler1) + + const view2 = () => { + fn2((obs.bb = obs.aa + obs.cc)) + } + const scheduler2 = () => { + tracker2.track(view2) + } + + const tracker2 = new Tracker(scheduler2) + + const view3 = () => { + fn3((obs.cc = obs.aa + obs.bb)) + } + const scheduler3 = () => { + tracker3.track(view3) + } + + const tracker3 = new Tracker(scheduler3) + + tracker1.track(view1) + tracker2.track(view2) + tracker3.track(view3) + + expect(fn1).toBeCalledTimes(4) + expect(fn2).toBeCalledTimes(4) + expect(fn3).toBeCalledTimes(3) + + fn1.mockClear() + fn2.mockClear() + fn3.mockClear() + + batch(() => { + obs.aa = 100 + obs.bb = 100 + obs.cc = 100 + }) + + expect(fn1).toBeCalledTimes(2) + expect(fn2).toBeCalledTimes(2) + expect(fn3).toBeCalledTimes(2) +}) diff --git a/packages/reactive/src/autorun.ts b/packages/reactive/src/autorun.ts index f2bbd97e4c1..adc63113880 100644 --- a/packages/reactive/src/autorun.ts +++ b/packages/reactive/src/autorun.ts @@ -19,7 +19,10 @@ interface IValue { export const autorun = (tracker: Reaction, name = 'AutoRun') => { const reaction: Reaction = () => { if (!isFn(tracker)) return - if (reaction._boundary > 0) return + + const updateKey = reaction._boundary.get(reaction._updateTarget) + if (updateKey && updateKey.has(reaction._updateKey)) return + if (ReactionStack.indexOf(reaction) === -1) { releaseBindingReactions(reaction) try { @@ -28,9 +31,22 @@ export const autorun = (tracker: Reaction, name = 'AutoRun') => { tracker() } finally { ReactionStack.pop() - reaction._boundary++ + + const key = reaction._updateKey + const target = reaction._updateTarget + if (key) { + const keys = reaction._boundary.get(target) || new Set([]) + keys.add(key) + reaction._boundary.set(target, keys) + } + batchEnd() - reaction._boundary = 0 + + const keys = reaction._boundary.get(target) + if (keys) { + keys.delete(key) + } + reaction._memos.cursor = 0 reaction._effects.cursor = 0 } @@ -46,10 +62,12 @@ export const autorun = (tracker: Reaction, name = 'AutoRun') => { cursor: 0, } } - reaction._boundary = 0 + + reaction._boundary = new Map() reaction._name = name cleanRefs() reaction() + return () => { disposeBindingReactions(reaction) disposeEffects(reaction) diff --git a/packages/reactive/src/reaction.ts b/packages/reactive/src/reaction.ts index 38444eae094..56d70256a1b 100644 --- a/packages/reactive/src/reaction.ts +++ b/packages/reactive/src/reaction.ts @@ -74,6 +74,8 @@ const runReactions = (target: any, key: PropertyKey) => { UntrackCount.value = 0 for (let i = 0, len = reactions.length; i < len; i++) { const reaction = reactions[i] + reaction._updateTarget = target + reaction._updateKey = key if (reaction._isComputed) { reaction._scheduler(reaction) } else if (isScopeBatching()) { diff --git a/packages/reactive/src/tracker.ts b/packages/reactive/src/tracker.ts index f6e2392e666..27ffc9b2f5b 100644 --- a/packages/reactive/src/tracker.ts +++ b/packages/reactive/src/tracker.ts @@ -15,16 +15,19 @@ export class Tracker { name = 'TrackerReaction' ) { this.track._scheduler = (callback) => { - if (this.track._boundary === 0) this.dispose() + if (this.track._boundary.size === 0) this.dispose() if (isFn(callback)) scheduler(callback) } this.track._name = name - this.track._boundary = 0 + this.track._boundary = new Map() } track: Reaction = (tracker: Reaction) => { if (!isFn(tracker)) return this.results - if (this.track._boundary > 0) return + + const updateKey = this.track._boundary.get(this.track._updateTarget) + if (updateKey && updateKey.has(this.track._updateKey)) return + if (ReactionStack.indexOf(this.track) === -1) { releaseBindingReactions(this.track) try { @@ -33,9 +36,21 @@ export class Tracker { this.results = tracker() } finally { ReactionStack.pop() - this.track._boundary++ + + const key = this.track._updateKey + const target = this.track._updateTarget + if (key) { + const keys = this.track._boundary.get(target) || new Set([]) + keys.add(key) + this.track._boundary.set(target, keys) + } + batchEnd() - this.track._boundary = 0 + + const keys = this.track._boundary.get(target) + if (keys) { + keys.delete(key) + } } } return this.results diff --git a/packages/reactive/src/types.ts b/packages/reactive/src/types.ts index f0fcca41185..16d5b9797af 100644 --- a/packages/reactive/src/types.ts +++ b/packages/reactive/src/types.ts @@ -61,12 +61,14 @@ export type Dispose = () => void export type Effect = () => void | Dispose export type Reaction = ((...args: any[]) => any) & { - _boundary?: number + _boundary?: Map> _name?: string _isComputed?: boolean _dirty?: boolean _context?: any _disposed?: boolean + _updateKey?: string + _updateTarget?: any _property?: PropertyKey _computesSet?: ArraySet _reactionsSet?: ArraySet