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