Skip to content

Commit 92945b0

Browse files
authoredMay 24, 2022
feat(react): support dynamic scope (#3143)
* feat(react): support dynamic scope * test(shared/reactive): add some tests
1 parent e1a2a65 commit 92945b0

File tree

14 files changed

+252
-72
lines changed

14 files changed

+252
-72
lines changed
 

‎packages/antd/src/form-button-group/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
* 4. 吸底布局
66
*/
77
import React, { useRef, useLayoutEffect, useState } from 'react'
8-
import StickyBox, { StickyBoxCompProps } from 'react-sticky-box'
98
import { ReactFC } from '@formily/react'
109
import { Space } from 'antd'
1110
import { SpaceProps } from 'antd/lib/space'
1211
import { BaseItem, IFormItemProps } from '../form-item'
1312
import { usePrefixCls } from '../__builtins__'
13+
import StickyBox from 'react-sticky-box'
1414
import cls from 'classnames'
15-
interface IStickyProps extends StickyBoxCompProps {
15+
interface IStickyProps extends React.ComponentProps<typeof StickyBox> {
1616
align?: React.CSSProperties['textAlign']
1717
}
1818

‎packages/json-schema/src/transformer.ts

+20-17
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isFn,
88
isPlainObj,
99
reduce,
10+
lazyMerge,
1011
} from '@formily/shared'
1112
import { Schema } from './schema'
1213
import {
@@ -99,31 +100,35 @@ const setSchemaFieldState = (
99100
if (target) {
100101
if (request.state) {
101102
field.form.setFieldState(target, (state) =>
102-
patchCompile(state, request.state, {
103-
...scope,
104-
$target: state,
105-
})
103+
patchCompile(
104+
state,
105+
request.state,
106+
lazyMerge(scope, {
107+
$target: state,
108+
})
109+
)
106110
)
107111
}
108112
if (request.schema) {
109113
field.form.setFieldState(target, (state) =>
110114
patchSchemaCompile(
111115
state,
112116
request.schema,
113-
{
114-
...scope,
117+
lazyMerge(scope, {
115118
$target: state,
116-
},
119+
}),
117120
demand
118121
)
119122
)
120123
}
121124
if (isStr(runner) && runner) {
122125
field.form.setFieldState(target, (state) => {
123-
shallowCompile(`{{function(){${runner}}}}`, {
124-
...scope,
125-
$target: state,
126-
})()
126+
shallowCompile(
127+
`{{function(){${runner}}}}`,
128+
lazyMerge(scope, {
129+
$target: state,
130+
})
131+
)()
127132
})
128133
}
129134
} else {
@@ -153,16 +158,15 @@ const getBaseScope = (
153158
const $self = field
154159
const $form = field.form
155160
const $values = field.form.values
156-
return {
157-
...options.scope,
161+
return lazyMerge(options.scope, {
158162
$form,
159163
$self,
160164
$observable,
161165
$effect,
162166
$memo,
163167
$props,
164168
$values,
165-
}
169+
})
166170
}
167171

168172
const getBaseReactions =
@@ -194,12 +198,11 @@ const getUserReactions = (
194198
const run = () => {
195199
const $deps = getDependencies(field, reaction.dependencies)
196200
const $dependencies = $deps
197-
const scope = {
198-
...baseScope,
201+
const scope = lazyMerge(baseScope, {
199202
$target: null,
200203
$deps,
201204
$dependencies,
202-
}
205+
})
203206
const compiledWhen = shallowCompile(when, scope)
204207
const condition = when ? compiledWhen : true
205208
const request = condition ? fulfill : otherwise

‎packages/next/src/form-button-group/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
* 4. 吸底布局
66
*/
77
import React, { useRef, useLayoutEffect, useState } from 'react'
8-
import StickyBox, { StickyBoxCompProps } from 'react-sticky-box'
8+
import StickyBox from 'react-sticky-box'
99
import { ReactFC } from '@formily/react'
1010
import { Space, ISpaceProps } from '../space'
1111
import { BaseItem, IFormItemProps } from '../form-item'
1212
import { usePrefixCls } from '../__builtins__'
1313
import cls from 'classnames'
14-
interface IStickyProps extends StickyBoxCompProps {
14+
interface IStickyProps extends React.ComponentProps<typeof StickyBox> {
1515
align?: React.CSSProperties['textAlign']
1616
}
1717

‎packages/react/src/components/ExpressionScope.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import React, { useContext } from 'react'
2+
import { lazyMerge } from '@formily/shared'
23
import { SchemaExpressionScopeContext } from '../shared'
34
import { IExpressionScopeProps, ReactFC } from '../types'
45

56
export const ExpressionScope: ReactFC<IExpressionScopeProps> = (props) => {
67
const scope = useContext(SchemaExpressionScopeContext)
78
return (
8-
<SchemaExpressionScopeContext.Provider value={{ ...scope, ...props.value }}>
9+
<SchemaExpressionScopeContext.Provider
10+
value={lazyMerge(scope, props.value)}
11+
>
912
{props.children}
1013
</SchemaExpressionScopeContext.Provider>
1114
)

‎packages/react/src/components/ReactiveField.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React, { Fragment, useContext } from 'react'
22
import { toJS } from '@formily/reactive'
33
import { observer } from '@formily/reactive-react'
4-
import { isFn } from '@formily/shared'
4+
import { FormPath, isFn } from '@formily/shared'
55
import { isVoidField, GeneralField, Form } from '@formily/core'
6-
import { SchemaOptionsContext } from '../shared'
6+
import { SchemaComponentsContext } from '../shared'
77
import { RenderPropsChildren } from '../types'
88
interface IReactiveFieldProps {
99
field: GeneralField
@@ -31,7 +31,7 @@ const renderChildren = (
3131
) => (isFn(children) ? children(field, form) : children)
3232

3333
const ReactiveInternal: React.FC<IReactiveFieldProps> = (props) => {
34-
const options = useContext(SchemaOptionsContext)
34+
const components = useContext(SchemaComponentsContext)
3535
if (!props.field) {
3636
return <Fragment>{renderChildren(props.children)}</Fragment>
3737
}
@@ -47,7 +47,7 @@ const ReactiveInternal: React.FC<IReactiveFieldProps> = (props) => {
4747
return <Fragment>{children}</Fragment>
4848
}
4949
const finalComponent =
50-
options?.getComponent(field.decoratorType) ?? field.decoratorType
50+
FormPath.getIn(components, field.decoratorType) ?? field.decoratorType
5151

5252
return React.createElement(
5353
finalComponent,
@@ -84,7 +84,7 @@ const ReactiveInternal: React.FC<IReactiveFieldProps> = (props) => {
8484
? field.pattern === 'readOnly'
8585
: undefined
8686
const finalComponent =
87-
options?.getComponent(field.componentType) ?? field.componentType
87+
FormPath.getIn(components, field.componentType) ?? field.componentType
8888
return React.createElement(
8989
finalComponent,
9090
{

‎packages/react/src/components/RecursionField.tsx

+3-16
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
import React, { Fragment, useContext, useRef, useMemo } from 'react'
1+
import React, { Fragment, useContext, useMemo } from 'react'
22
import { isFn, isValid } from '@formily/shared'
33
import { GeneralField } from '@formily/core'
44
import { Schema } from '@formily/json-schema'
5-
import {
6-
SchemaContext,
7-
SchemaOptionsContext,
8-
SchemaExpressionScopeContext,
9-
} from '../shared'
5+
import { SchemaContext, SchemaExpressionScopeContext } from '../shared'
106
import { IRecursionFieldProps, ReactFC } from '../types'
117
import { useField } from '../hooks'
128
import { ObjectField } from './ObjectField'
@@ -15,18 +11,9 @@ import { Field } from './Field'
1511
import { VoidField } from './VoidField'
1612

1713
const useFieldProps = (schema: Schema) => {
18-
const options = useContext(SchemaOptionsContext)
1914
const scope = useContext(SchemaExpressionScopeContext)
20-
const scopeRef = useRef<any>()
21-
scopeRef.current = scope
2215
return schema.toFieldProps({
23-
...options,
24-
get scope() {
25-
return {
26-
...options.scope,
27-
...scopeRef.current,
28-
}
29-
},
16+
scope,
3017
}) as any
3118
}
3219

‎packages/react/src/components/SchemaField.tsx

+12-19
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
SchemaMarkupContext,
77
SchemaExpressionScopeContext,
88
SchemaOptionsContext,
9+
SchemaComponentsContext,
910
} from '../shared'
1011
import {
1112
ReactComponentPath,
@@ -16,7 +17,7 @@ import {
1617
ISchemaMarkupFieldProps,
1718
ISchemaTypeFieldProps,
1819
} from '../types'
19-
import { FormPath } from '@formily/shared'
20+
import { lazyMerge } from '@formily/shared'
2021
const env = {
2122
nonameId: 0,
2223
}
@@ -53,25 +54,17 @@ export function createSchemaField<Components extends SchemaReactComponents>(
5354
}
5455

5556
return (
56-
<SchemaOptionsContext.Provider
57-
value={{
58-
...options,
59-
getComponent(name) {
60-
const propsComponent = FormPath.getIn(props.components, name)
61-
if (propsComponent) return propsComponent
62-
return FormPath.getIn(options.components, name)
63-
},
64-
}}
65-
>
66-
<SchemaExpressionScopeContext.Provider
67-
value={{
68-
...options.scope,
69-
...props.scope,
70-
}}
57+
<SchemaOptionsContext.Provider value={options}>
58+
<SchemaComponentsContext.Provider
59+
value={lazyMerge(options.components, props.components)}
7160
>
72-
{renderMarkup()}
73-
{renderChildren()}
74-
</SchemaExpressionScopeContext.Provider>
61+
<SchemaExpressionScopeContext.Provider
62+
value={lazyMerge(options.scope, props.scope)}
63+
>
64+
{renderMarkup()}
65+
{renderChildren()}
66+
</SchemaExpressionScopeContext.Provider>
67+
</SchemaComponentsContext.Provider>
7568
</SchemaOptionsContext.Provider>
7669
)
7770
}

‎packages/react/src/shared/context.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import React, { createContext } from 'react'
22
import { Form, GeneralField } from '@formily/core'
33
import { Schema } from '@formily/json-schema'
4-
import { ISchemaFieldOptionContext } from '../types'
4+
import {
5+
ISchemaFieldReactFactoryOptions,
6+
SchemaReactComponents,
7+
} from '../types'
58

69
const createContextCleaner = <T>(...contexts: React.Context<T>[]) => {
710
return ({ children }) => {
@@ -16,13 +19,16 @@ export const FieldContext = createContext<GeneralField>(null)
1619
export const SchemaMarkupContext = createContext<Schema>(null)
1720
export const SchemaContext = createContext<Schema>(null)
1821
export const SchemaExpressionScopeContext = createContext<any>(null)
22+
export const SchemaComponentsContext =
23+
createContext<SchemaReactComponents>(null)
1924
export const SchemaOptionsContext =
20-
createContext<ISchemaFieldOptionContext>(null)
25+
createContext<ISchemaFieldReactFactoryOptions>(null)
2126

2227
export const ContextCleaner = createContextCleaner(
2328
FieldContext,
2429
SchemaMarkupContext,
2530
SchemaContext,
2631
SchemaExpressionScopeContext,
32+
SchemaComponentsContext,
2733
SchemaOptionsContext
2834
)

‎packages/react/src/types.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,7 @@ export interface ISchemaFieldReactFactoryOptions<
7171
}
7272

7373
export interface ISchemaFieldOptionContext {
74-
getComponent: (name: string) => JSXComponent
75-
scope?: any
74+
components: SchemaReactComponents
7675
}
7776

7877
export interface ISchemaFieldProps<

‎packages/reactive/src/__tests__/annotations.spec.ts

+55-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { observable, action, model } from '../'
1+
import { observable, action, model, define } from '../'
22
import { autorun, reaction } from '../autorun'
33
import { observe } from '../observe'
44
import { isObservable } from '../externals'
@@ -292,3 +292,57 @@ test('computed no track get', () => {
292292
expect(compu.value).toBe(123)
293293
})
294294
})
295+
296+
test('computed cache descriptor', () => {
297+
class A {
298+
_value = 0
299+
constructor() {
300+
define(this, {
301+
_value: observable.ref,
302+
value: observable.computed,
303+
})
304+
}
305+
306+
get value() {
307+
return this._value
308+
}
309+
}
310+
const obs1 = new A()
311+
const obs2 = new A()
312+
const handler1 = jest.fn()
313+
const handler2 = jest.fn()
314+
autorun(() => {
315+
handler1(obs1.value)
316+
})
317+
autorun(() => {
318+
handler2(obs2.value)
319+
})
320+
expect(handler1).toBeCalledTimes(1)
321+
expect(handler2).toBeCalledTimes(1)
322+
obs1._value = 123
323+
obs2._value = 123
324+
expect(handler1).toBeCalledTimes(2)
325+
expect(handler2).toBeCalledTimes(2)
326+
})
327+
328+
test('computed normal object', () => {
329+
const obs = define(
330+
{
331+
_value: 0,
332+
get value() {
333+
return this._value
334+
},
335+
},
336+
{
337+
_value: observable.ref,
338+
value: observable.computed,
339+
}
340+
)
341+
const handler = jest.fn()
342+
autorun(() => {
343+
handler(obs.value)
344+
})
345+
expect(handler).toBeCalledTimes(1)
346+
obs._value = 123
347+
expect(handler).toBeCalledTimes(2)
348+
})

‎packages/reactive/src/__tests__/define.spec.ts

+20
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,26 @@ describe('makeObservable', () => {
138138
expect(handler).toBeCalledTimes(2)
139139
expect(target.cc).toEqual(44)
140140
})
141+
test('unexpect target', () => {
142+
const testFn = jest.fn()
143+
const testArr = []
144+
const obs1 = define(4 as any, {
145+
value: observable.computed,
146+
})
147+
const obs2 = define('123' as any, {
148+
value: observable.computed,
149+
})
150+
const obs3 = define(testFn as any, {
151+
value: observable.computed,
152+
})
153+
const obs4 = define(testArr as any, {
154+
value: observable.computed,
155+
})
156+
expect(obs1).toBe(4)
157+
expect(obs2).toBe('123')
158+
expect(obs3).toBe(testFn)
159+
expect(obs4).toBe(testArr)
160+
})
141161
})
142162

143163
test('define model', () => {

‎packages/reactive/src/annotations/computed.ts

+26-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,26 @@ const getDescriptor = Object.getOwnPropertyDescriptor
2525

2626
const getProto = Object.getPrototypeOf
2727

28+
const ClassDescriptorMap = new WeakMap()
29+
30+
function getPropertyDescriptor(obj: any, key: PropertyKey) {
31+
if (!obj) return
32+
return getDescriptor(obj, key) || getPropertyDescriptor(getProto(obj), key)
33+
}
34+
35+
function getPropertyDescriptorCache(obj: any, key: PropertyKey) {
36+
const constructor = obj.constructor
37+
if (constructor === Object || constructor === Array)
38+
return getPropertyDescriptor(obj, key)
39+
const cache = ClassDescriptorMap.get(constructor) || {}
40+
const descriptor = cache[key]
41+
if (descriptor) return descriptor
42+
const newDesc = getPropertyDescriptor(obj, key)
43+
ClassDescriptorMap.set(constructor, cache)
44+
cache[key] = newDesc
45+
return newDesc
46+
}
47+
2848
function getGetterAndSetter(target: any, key: PropertyKey, value: any) {
2949
if (!target) {
3050
if (value) {
@@ -36,9 +56,11 @@ function getGetterAndSetter(target: any, key: PropertyKey, value: any) {
3656
}
3757
return []
3858
}
39-
const descriptor = getDescriptor(target, key)
40-
if (descriptor) return [descriptor.get, descriptor.set]
41-
return getGetterAndSetter(getProto(target), key, value)
59+
const descriptor = getPropertyDescriptorCache(target, key)
60+
if (descriptor) {
61+
return [descriptor.get, descriptor.set]
62+
}
63+
return []
4264
}
4365

4466
export const computed: IComputed = createAnnotation(
@@ -49,7 +71,7 @@ export const computed: IComputed = createAnnotation(
4971

5072
const context = target ? target : store
5173
const property = target ? key : 'value'
52-
const [getter, setter] = getGetterAndSetter(context, property, value)
74+
const [getter, setter] = getGetterAndSetter(target, property, value)
5375

5476
function compute() {
5577
store.value = getter?.call(context)

‎packages/shared/src/__tests__/index.spec.ts

+58-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { globalThisPolyfill } from '../global'
1818
import { isValid, isEmpty } from '../isEmpty'
1919
import { stringLength } from '../string'
2020
import { Subscribable } from '../subscribable'
21-
import { merge } from '../merge'
21+
import { lazyMerge, merge } from '../merge'
2222
import { instOf } from '../instanceof'
2323
import { isFn, isHTMLElement, isNumberLike, isReactElement } from '../checkers'
2424
import { defaults } from '../defaults'
@@ -777,6 +777,63 @@ describe('merge', () => {
777777
test('merge unmatch', () => {
778778
expect(merge({ aa: 123 }, [111])).toEqual([111])
779779
})
780+
781+
test('lazy merge', () => {
782+
const merge1 = lazyMerge<any>(1, 2)
783+
expect(merge1).toBe(2)
784+
const merge2 = lazyMerge<any>('123', '321')
785+
expect(merge2).toBe('321')
786+
const merge3 = lazyMerge<any>(1, undefined)
787+
expect(merge3).toBe(1)
788+
const merge4 = lazyMerge<any>('123', undefined)
789+
expect(merge4).toBe('123')
790+
const merge5 = lazyMerge<any>(undefined, '123')
791+
expect(merge5).toBe('123')
792+
const merge6 = lazyMerge([1, 2, 3], [3, 4])
793+
expect(merge6[0]).toBe(3)
794+
expect(merge6[1]).toBe(4)
795+
expect(merge6[2]).toBe(3)
796+
const merge7 = lazyMerge<any>(
797+
{
798+
get x() {
799+
return 'x'
800+
},
801+
},
802+
{
803+
get y() {
804+
return 'y'
805+
},
806+
}
807+
)
808+
expect(merge7.x).toBe('x')
809+
expect(merge7.y).toBe('y')
810+
const effects = {
811+
a: 1,
812+
b: 2,
813+
}
814+
const merge8 = lazyMerge<any>(
815+
{
816+
get x() {
817+
return effects.a
818+
},
819+
},
820+
{
821+
get y() {
822+
return effects.b
823+
},
824+
}
825+
)
826+
expect(merge8.x).toBe(1)
827+
expect(merge8.y).toBe(2)
828+
effects.a = 123
829+
effects.b = 321
830+
expect(merge8.x).toBe(123)
831+
expect(merge8.y).toBe(321)
832+
expect(Object.keys(merge8)).toEqual(['x', 'y'])
833+
expect('x' in merge8).toBe(true)
834+
expect('y' in merge8).toBe(true)
835+
expect('z' in merge8).toBe(false)
836+
})
780837
})
781838

782839
describe('globalThis', () => {

‎packages/shared/src/merge.ts

+36
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,40 @@ function deepmerge(target: any, source: any, options?: Options) {
151151
}
152152
}
153153

154+
export const lazyMerge = <T extends object>(target: T, source: T): T => {
155+
if (!isValid(source)) return target
156+
if (!isValid(target)) return source
157+
if (typeof target !== 'object') return source
158+
if (typeof source !== 'object') return target
159+
return new Proxy(
160+
{},
161+
{
162+
get(_, key) {
163+
if (key in source) return source[key]
164+
return target[key]
165+
},
166+
ownKeys() {
167+
const keys = Object.keys(target)
168+
for (let key in source) {
169+
if (!(key in target)) {
170+
keys.push(key)
171+
}
172+
}
173+
return keys
174+
},
175+
getOwnPropertyDescriptor() {
176+
return {
177+
enumerable: true,
178+
configurable: true,
179+
writable: false,
180+
}
181+
},
182+
has(_, key: string) {
183+
if (key in source || key in target) return true
184+
return false
185+
},
186+
}
187+
) as any
188+
}
189+
154190
export const merge = deepmerge

0 commit comments

Comments
 (0)
Please sign in to comment.