Skip to content

Commit

Permalink
fix(reactive): fix tracker recursive react (#1709)
Browse files Browse the repository at this point in the history
  • Loading branch information
janryWang committed Jul 3, 2021
1 parent 617ce88 commit 3de82d5
Show file tree
Hide file tree
Showing 13 changed files with 232 additions and 94 deletions.
8 changes: 4 additions & 4 deletions designable/antd/package.json
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/advanced/destructor.zh-CN.md
Expand Up @@ -6,7 +6,7 @@

但从前端组件化角度来看,数组结构又是最佳的;

所以哪一边都有其道理,可惜的是,每次都只能前端取消化这样一个不平等条约,不过,有了 Formily,你就完全不需要为这样一个尴尬局面而难受了,**Formily 提供了解构路径的能力,可以帮助用户快速解决这类问题。**,下面可以看看例子
所以哪一边都有其道理,可惜的是,每次都只能前端去消化这样一个不平等条约,不过,有了 Formily,你就完全不需要为这样一个尴尬局面而难受了,**Formily 提供了解构路径的能力,可以帮助用户快速解决这类问题。**,下面可以看看例子

## Markup Schema 案例

Expand Down
4 changes: 2 additions & 2 deletions docs/guide/scenes/more.zh-CN.md
@@ -1,5 +1,5 @@
# 更多场景

因为Formily在表单层面上是一个非常完备的方案,而且还很灵活,支持的场景非常多,但是场景案例,我们无法一一列举。
因为 Formily 在表单层面上是一个非常完备的方案,而且还很灵活,支持的场景非常多,但是场景案例,我们无法一一列举。

所以,还是希望社区能帮助Formily完善更多场景案例!我们会不胜感激!😀
所以,还是希望社区能帮助 Formily 完善更多场景案例!我们会不胜感激!😀
10 changes: 7 additions & 3 deletions packages/json-schema/src/transformer.ts
@@ -1,3 +1,4 @@
/* istanbul ignore file */
import { untracked } from '@formily/reactive'
import {
isBool,
Expand Down Expand Up @@ -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')
}
Expand All @@ -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
},
{}
Expand Down
111 changes: 109 additions & 2 deletions packages/reactive/src/__tests__/autorun.spec.ts
Expand Up @@ -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<any>({ value: 1 })
autorun(() => {
obs.value++
})
expect(obs.value).toEqual(2)
})

test('autorun direct recursive react with if', () => {
const obs1 = observable<any>({})
const obs2 = observable<any>({})
const fn = jest.fn()
Expand All @@ -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<any>({})
const obs2 = observable<any>({})
const obs3 = observable<any>({})
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<any>({})
const bb = observable<any>({})
const cc = observable<any>({})

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<any>({})
const obs2 = observable<any>({})
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')
})
2 changes: 1 addition & 1 deletion packages/reactive/src/__tests__/tracker.spec.ts
Expand Up @@ -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()
})
17 changes: 17 additions & 0 deletions packages/reactive/src/__tests__/untracked.spec.ts
@@ -0,0 +1,17 @@
import { untracked, observable, autorun } from '../'

test('basic untracked', () => {
const obs = observable<any>({})
const fn = jest.fn()
autorun(() => {
untracked(() => {
fn(obs.value)
})
})
obs.value = 123
expect(fn).toBeCalledTimes(1)
})

test('no params untracked', () => {
untracked()
})
6 changes: 1 addition & 5 deletions packages/reactive/src/annotations/computed.ts
Expand Up @@ -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'
Expand Down
61 changes: 42 additions & 19 deletions 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'
Expand All @@ -14,7 +15,7 @@ interface IValue {
oldValue?: any
}

interface ITracked {
interface IInitialized {
current?: boolean
}

Expand All @@ -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 () => {
Expand All @@ -58,26 +59,48 @@ export const reaction = <T>(
...options,
}
const value: IValue = {}
const tracked: ITracked = {}
const initialized: IInitialized = {}
const dirty: IDirty = {}
const dirtyCheck = () => {
if (isFn(realOptions.equals))
return !realOptions.equals(value.oldValue, value.currentValue)
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)
}
}

0 comments on commit 3de82d5

Please sign in to comment.