添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Core库

@formily/core 提供了核心能力,比如创建表单实例,监听表单各个生命周期等等,该仓库主要管理form实例、管理各字段状态,管理校验状态,以及上述三者之间的关系

Reactive库

@formily/reactive 提供了响应式状态管理,类似于mobx,更加方便的提供响应式数据修改组件。 状态修改后,主动刷新组件需要借助 @formily/reactive-react 实现,类似于 UI = Observer(Fn(state))

React库

@formily/react 来接入内核数据,用来实现最终的表单交互效果

概述

目前使用React组件的方式接入formily框架,尚未接触到通过JSON Scheme的方式创建表单,主要通过组件方式创建表单。借助formily官方提供的UI桥接层库 @formily/react 提供的胶水组件Field,ArrayField,ObjectField实现表单快速搭建,记录总结在使用中遇到问题与解决方案,记录API与使用方法便于更快查询

创建表单

createForm

values Object initialValues 表单默认值 Object pattern 表单交互模式 “editable” | “disabled” | “readOnly” | “readPretty” “editable” display “visible” | “hidden” | “none” “visible hidden UI 隐藏 Boolean FALSE visible 显示/隐藏(数据隐藏) Boolean editable 是否可编辑 Boolean disabled Boolean FALSE readOnly Boolean FALSE readPretty 是否是优雅阅读态 Boolean FALSE effects 副作用逻辑,用于实现各种联动逻辑 (form:Form)=>void validateFirst 是否只校验第一个非法规则 Boolean FALSE

display => visible表示表单正常展示,hidden表示隐藏表单但隐藏字段,none表示隐藏表单与字段 同理hidden、visible意义相同

effects 中可以配置FormEffectHooks与FieldEffectHooks

FormEffectHooks

用于配置表单的生命周期,主要是表单挂载,表单值的改变,表单提交,表单校验相关。

FieldEffectHooks

监听指定路径的表单字段的 挂载、校验、值变化等操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React, { ReactChild } from "react";
import {
createForm,
Form,
onFieldChange,
onFormInit,
onFormMount,
onFormValuesChange
} from "@formily/core";
import { FormProvider, Field, FormConsumer } from "@formily/react";
import { Input } from "antd";

const form = createForm({
effects: () => {
onFormInit((form) => {
console.log("表单初始化 : ", form.values);
});
onFormMount((form) => {
console.log("表单挂载 : ", form.values);
});
onFormValuesChange((form) => {
console.log("form values : ", form.values);
});
onFieldChange("input1", (field) => {
// 监听input1的value变化 修改input的值
console.log("input1 field.values : ", field.value);
const input = field.query("input");
const inputField = input.take();
inputField.setValue(field.value);
});
}
});

export default () => {
return (
<FormProvider form={form}>
<Field
name="input"
component={[Input, { placeholder: "Please Input" }]}
/>
<Field
name="input1"
component={[Input, { placeholder: "Please Input" }]}
/>
<Field
name="input2"
component={[Input, { placeholder: "Please Input" }]}
/>
<FormConsumer>
{(form: Form) => {
return JSON.stringify(form.values) as ReactChild;
}}
</FormConsumer>
</FormProvider>
);
};

自定义组件

借助@formily/react能力,自定义支持formily的组件。主要使用connect与mapProps实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from "react";
import { Form, FormItemProps } from "antd";
import { connect, mapProps } from "@formily/react";
import { isDataField } from '@formily/core';

const Item = Form.Item;

export const FormItem: FormItemProps = connect<FormItemProps>(
Item,
// props: FormItemProps, field: GeneralField
// 映射props,实现Field与组件的关联
// 可以借助field 获取当前状态如获取失败field.selfErrors
mapProps((props, field) => {
// 判断一个对象是否为 Field/ArrayField/ObjectField 对象
if(!isDataField(field)) return;
console.log(field.selfErrors);
return {
...props,
help: field.selfErrors?.length ? field.selfErrors : undefined,
};
})
);

@formily/react 组件与Hook

3.1 Field ArrayField ObjectField

用于创建一个字段模型,可以传入以下参数

initialized 字段是否已被初始化 Boolean FALSE mounted 字段是否已挂载 Boolean FALSE unmounted 字段是否已卸载 Boolean FALSE address 字段节点路径 FormPath 字段数据路径 FormPath title FieldMessage description FieldMessage loading 字段加载状态 Boolean FALSE validating 字段是否正在校验 Boolean FALSE modified 字段子树是否被手动修改过 Boolean FALSE selfModified 字段自身是否被手动修改过 Boolean FALSE active 字段是否处于激活态 Boolean FALSE visited 字段是否被浏览过 Boolean FALSE inputValue 字段输入值 inputValues 字段输入值集合 Array dataSource 字段数据源 Array validator 字段校验器 FieldValidator decorator 字段装饰器 Any[] component Any[] feedbacks 字段反馈信息 IFieldFeedback [] parent GeneralField errors 字段汇总(包含子节点)错误消息 IFormFeedback [] warnings 字段汇总(包含子节点)警告消息 IFormFeedback [] successes 字段汇总(包含子节点)成功消息 IFormFeedback [] valid 字段是否合法(包含子节点) Boolean invalid 字段是否非法(包含子节点) Boolean FALSE value initialValue 字段默认值 display 字段展示状态 FieldDisplayTypes “visible” pattern 字段交互模式 FieldPatternTypes “editable” required 字段是否必填 Boolean FALSE hidden 字段是否隐藏 Boolean FALSE visible 字段是否显示 Boolean disabled 字段是否禁用 Boolean FALSE readOnly 字段是否只读 Boolean FALSE readPretty 字段是否为阅读态 Boolean FALSE editable 字段是可编辑 Boolean validateStatus 字段校验状态 FieldValidateStatus content 字段内容,一般作为子节点 字段扩展属性 Object selfErrors 字段自身错误消息 FieldMessage [] selfWarnings 字段自身警告消息 FieldMessage [] selfSuccesses 字段自身成功消息 FieldMessage [] selfValid 字段自身是否合法 Boolean selfInvalid 字段自身是否非法 Boolean FALSE indexes 字段数字索引集合 Number index 字段数字索引,取 indexes 最后一个 Number reactions 配置参数(字段响应器) FieldReaction[] | FieldReaction

其中ArrayField提供了 移动、删除、添加 等方法,ObjectField提供了 添加、删除 操作

方法1:通过useField创建,使用该方法创建的组件如若需响应数据变化需要用observer包裹

formily工具方法 用于判断是否是指定对象类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import { FieldValidator, ArrayField as ArrayFieldType } from '@formily/core';
import { useField, observer } from '@formily/react';
import { Form } from 'antd';

// 创建数组表单字段
const ArrayComponent = observer(() => {
const field = useField<ArrayFieldType>()
return (
<>
<div>
{field.value?.map((item, index) => (
<div key={index} style={{ display: 'flex-block', marginBottom: 10 }}>
<Field name={index} component={[Input]} />
</div>
))}
</div>
<Button
onClick={() => {
field.push('')
}}
>
Add
</Button>
</>
)
})
const FormItem = observer(({ children }) => {
const field = useField()
return (
<Form.Item
label={field.title}
help={field.selfErrors?.length ? field.selfErrors : undefined}
extra={field.description}
validateStatus={field.validateStatus}
>
{children}
</Form.Item>
)
})

export default () => {
return <>
<Field
name="name"
title="姓名"
required
decorator={[FormItem]}
component={[Input, { placeholder: 'Please Input' }]}
/>
<ArrayField
name="habby"
title="爱好"
decorator={[FormItem]}
component={[ArrayComponent]}
>
</>
}

方法2:通过Field、ArrayField、ObjectField组件方式创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<Field 
name={`${basePath}.xxx`} // 字段名称 也就是path
title="标签" // 字段对应的label
component={[ // 使用的组件 [组件名,{组件props}]
Input,
{
filled: true,
showCount: true,
placeholder: '请输入标签(2-7个字)',
maxCount: 14
}
]}
validator={{ // 校验器
triggerType: 'onInput',
validator: (val: string) => {
if (getLength(val) > 14 || getLength(val) < 4) {
return '请输入标签(2-7个字)'
}
return ''
}
}}
decorator={[ // 字段装饰器,通常是包裹功能组件的外层样式组件如 FormItem等,与component参数一致
FormItem,
{
required: true,
marginBottom: 24,
}
]}
/>

3.2 FormProvider

类似于antd组件库中的Form组件,用于下发表单上下文给字段组件,负责整个表单状态

3.3 FormConsumer

存在provider那么必然会有consumer,它的职责就是可以获取到实时的form表单数据

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react'
import { createForm } from '@formily/core'
import { FormProvider, FormConsumer, Field } from '@formily/react'
import { Input } from 'antd'

const form = createForm()

export default () => (
<FormProvider form={form}>
<Field name="input" component={[Input]} />
<FormConsumer>{(form) => form.values.input}</FormConsumer>
</FormProvider>
)

3.4 useFormEffects

该方法可以在组件使用,为form注入一些effect操作,同样可以将这些effect操作放到createForm的effects方法中, 这个hook没有办法监听onFormInit,执行到这个组件的时候那么Form表单必然是初始化好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React from 'react'
import { createForm, onFieldReact } from '@formily/core'
import { FormProvider, Field, useFormEffects } from '@formily/react'
import { Input, Form } from 'antd'

const form = createForm({
effects() {
onFieldReact('custom.aa', (field) => {
field.value = field.query('input').get('value')
})
},
})

const Custom = () => {
useFormEffects(() => {
onFieldReact('custom.bb', (field) => {
field.value = field.query('.aa').get('value')
})
})
return (
<div>
<Field name="aa" decorator={[Form.Item]} component={[Input]} />
<Field name="bb" decorator={[Form.Item]} component={[Input]} />
</div>
)
}

export default () => (
<FormProvider form={form}>
<Field name="input" decorator={[Form.Item]} component={[Input]} />
<Field name="custom" decorator={[Form.Item]} component={[Custom]} />
</FormProvider>
)

@formily/reactive

在普通场景里,我们可能只需要有Field就可以了,通过Field传入的reactions去做组件的关联效果。某些场景下希望,非表单组件可以控制表单组件展示内容或显隐逻辑,此时就需要一个状态来处理这个功能,但是useState无法影响表单数据样式展示,影响组件变化需要使用Observer 进行包裹

4.1 监听值变化

4.1.1 observable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import { observable, autorun } from '@formily/reactive'
function testObservable1() {
// 将一个对象变化可观察的,就是倾听它的set操作
const obs = observable({
aa: {
bb: 123,
},
cc: {
dd: 123,
},
})
autorun(() => {
// 首次的时候会触发,变化的时候也会触发
// 总共触发2次
console.log('normal1', obs.aa.bb)
})

//数据进行set操作的时候,就会触发
obs.aa.bb = 44
autorun(() => {
// 当值相同的时候,不会重复触发
// 这里只会触发1次
console.log('normal2', obs.cc.dd)
})

obs.cc.dd = 123
}

function testObservable2() {
const obs = observable({
aa: {
bb: 123,
},
cc: {
dd: 123,
},
ee: {
ff: 123,
},
gg: {
hh: 123,
},
})

autorun(() => {
// 整个字段被赋值的话,就会触发// 所以,这里触发2次
console.log('object1', obs.cc)
})

obs.cc = { dd: 456 }

autorun(() => {
// 这里会触发2次,虽然值相同// 但是object的比较是通过引用比较的
console.log('object2', obs.ee)
})

obs.ee = { ff: 123 }

autorun(() => {
// 只是倾听aa字段的话,那么子字段的变化是不会触发的
// 因为obs.aa的引用没有变化
// 所以这里只触发1次
console.log('object3', obs.aa)
})

console.log('testObservable2 set data')
obs.aa.bb = 44
autorun(() => {
// 主体变化的时候,子的也要变化
// 所以这里触发2次
console.log('object4', obs.gg.hh)
})

console.log('testObservable2 set data2')
obs.gg = { hh: 45 }
}

function testObservable3() {
const obs = observable({
aa: {
bb: ['a'],
},
})
autorun(() => {
// 只倾听bb字段的话,变化的时候也不会触发
// 因为obs.aa.bb的引用没变化
console.log('array1', obs.aa.bb)
})

autorun(() => {
// length字段会autorun的时候触发
// 因为obs.aa.bb的length字段发生变化了
console.log('array2', obs.aa.bb.length)
})

autorun(() => {
// 即使原来的不存在,也能触发
// 这里会触发2次,因为的确obs.aa.bb[1]的值变了
console.log('array3', obs.aa.bb[1])
})

console.log('testObservable3 set data')
obs.aa.bb.push('cc')
}

function testObservable4() {
const obs = observable({
aa: {
bb: ['a'],
},
cc: '78',
})
autorun(() => {
// 倾听其他字段的话当然也不会触发
console.log('other', obs.cc)
})

console.log('testObservable4 set data')
obs.aa.bb.push('cc')
}

function testObservableShadow() {
const obs = observable.shallow({
aa: {
bb: 'a',
},
cc: {
dd: 'a',
},
})

autorun(() => {
// 这里只会触发1次,因为是浅倾听set操作
console.log('shadow1', obs.aa.bb)
})

console.log('testObservableShadow set data1')
obs.aa.bb = '123'autorun(() => {
// 这里会触发2次,aa属于浅倾听的范围
console.log('shadow2', obs.cc)
})

console.log('testObservableShadow set data2')
obs.cc = { dd: 'c' }
}
export default function testObservable() {
testObservable1()
testObservable2()
testObservable3()
testObservable4()
testObservableShadow()
}

可以看到触发的规则为:

  • number与string的基础类型,值比较发生变化了会触发
  • object与array的复合类型,引用发生变化了会触发,object的字段添减不会触发,array的push和pop也不会触发
  • array.length,它属于字段的基础类型变化,所以也会触发
  • object与array类型,对于自己引用整个变化的时候,它也会触发子字段的触发
  • 浅倾听shadow,只能处理表面一层的数据

    4.1.2 复杂对象的obserable

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    import { observable, autorun } from '@formily/reactive'
    function testObservable1_object() {
    const obs = observable({
    aa: {
    bb: 123,
    },
    })
    autorun(() => {
    // 触发2次
    // 首次
    // 自身赋值1次
    console.log('normal1_object', obs.aa)
    })

    // 不会触发,子字段的变化不会影响到父字段的触发
    console.log('1. sub assign')
    obs.aa.bb = 44
    // 会触发
    console.log('2. self assign')
    obs.aa = { bb: 789 }
    }

    function testObservable2_object() {
    const obs = observable({
    aa: {
    bb: 123,
    },
    })
    autorun(() => {
    // 触发2次
    // 首次
    // 自身赋值1次
    // 赋值如同console,一样是向对象执行get操作
    const mk = obs.aa
    console.log('normal2_object')
    })

    // 不会触发,子字段的变化不会影响到父字段的触发
    console.log('1. sub assign')
    obs.aa.bb = 44
    // 会触发1次
    console.log('2. self assign')
    obs.aa = { bb: 789 }
    }

    function testObservable3_object() {
    const obs = observable({
    aa: {},
    })
    autorun(() => {
    // 触发1次// 首次
    const mk = obs.aa
    console.log('normal3_object')
    })

    // 不会触发,object的添加property不会触发
    console.log('1. self add property')
    obs.aa.bb = 4
    // 不会触发,obs.aa.bb的赋值不会触发
    console.log('2. self assign')
    obs.aa.bb = 5
    // 不会触发,object的移除property不会触发
    console.log('3. self remove property')
    delete obs.aa.bb
    }

    function testObservable4_object() {
    const obs = observable({
    aa: {},
    }) as any
    autorun(() => {
    // 触发3次
    // 首次
    // addProperty时
    // removeProperty时
    for (const i in obs.aa) {
    console.log('nothing')
    }
    console.log('normal4_object')
    })

    // 会触发,object的添加property会触发,对象是遍历时
    console.log('1. self add property')
    obs.aa.bb = 4
    // 不会触发,obs.aa.bb的赋值不会触发
    console.log('2. self assign')
    obs.aa.bb = 5
    // 会触发,object的移除property不会触发
    console.log('3. self remove property')
    delete obs.aa.bb
    }

    function testObservable1_array() {
    const obs = observable({
    aa: [] as number[],
    })
    autorun(() => {
    // 一共触发了1次
    // 首次
    const mk = obs.aa
    console.log('normal1_array')
    })

    // 不会触发,相当于object的添加property而已
    console.log('1.push')
    obs.aa.push(1)

    // 不会触发
    console.log('2.assign')
    obs.aa[0] = 3
    // 不会触发
    console.log('3.push')
    obs.aa.push(4)

    // 不会触发,相当于object的移除property而已
    console.log('4.pop')
    obs.aa.pop()

    // 不会触发
    console.log('5.assign')
    obs.aa[0] = 5
    }

    function testObservable2_array() {
    const obs = observable({
    aa: [] as number[],
    })
    autorun(() => {
    // 一共触发了5次
    // 首次,
    // push 的2次
    // pop的2次,pop一次,触发2次
    console.log('normal2_array', obs.aa.length)
    })

    // 会触发,因为push影响到了length字段
    console.log('1.push')
    obs.aa.push(1)

    // 不会触发,因为对某个元素赋值不影响length字段
    console.log('2.assign')
    obs.aa[0] = 3
    // 会触发,因为push影响到了length字段
    console.log('3.push')
    obs.aa.push(4)

    // 会触发,因为pop影响到了length字段,这个会触发2次,不知道为什么
    console.log('4.pop')
    obs.aa.pop()

    // 不会触发,因为对某个元素赋值不影响length字段
    console.log('5.assign')
    obs.aa[0] = 5
    }

    function testObservable3_array() {
    const obs = observable({
    aa: [] as any[],
    })
    autorun(() => {
    // 一共触发了6次
    // 首次,
    // push 的2次
    // pop的1次
    // 赋值的2次
    obs.aa.map((item) => '')
    console.log('normal3_array')
    })

    // 会触发,因为影响到了map
    console.log('1.push')
    obs.aa.push(1)

    // 会触发,因为影响到了map
    console.log('2.assign')
    obs.aa[0] = 3
    // 会触发,因为影响到了map
    console.log('3.push')
    obs.aa.push({})

    // 不会触发,嵌套元素赋值
    console.log('4.inner assign')
    obs.aa[1].kk = 3
    // 会触发,因为影响到了map
    console.log('5.pop')
    obs.aa.pop()

    // 会触发,因为影响到了map
    console.log('6.assign')
    obs.aa[0] = 5
    }

    export default function testObservableCaseTwo() {
    testObservable1_object()
    testObservable2_object()
    testObservable3_object()
    testObservable4_object()
    testObservable1_array()
    testObservable2_array()
    testObservable3_array()
    }

    对于数组与对象类型的触发,他们的规则是:

  • 如果只是对数组或对象整个进行get操作(console,或者赋值到其他变量),那么只有整个对象都被set的时候才会被触发。
  • 对数组或对象进行遍历或长度操作,例如for,map或者length行为,那么执行对象的addProperty或者push,pop都会有通知
  • 数组的map操作特别一点,但其实它的回调闭包里面包含了元素的get操作,所以对元素的set操作会得到触发。这条规则其实就是第一条规则而已。
  • 注意, 对于子字段的变化,父字段不会收到通知。反过来,父字段整个变化的时候,子字段总是可以收到通知
  • 至此,我们大概能推测到Observable的实现是:

  • 包装对象对属性的get与set操作,当get操作触发的时候,将当前闭包函数到subscribe保存起来。当同一个对象的set操作发生时,拉取对应属性的闭包函数,然后publish对应的闭包函数, 并触发子对象的通知
  • 包装对象对方法的操作,for,map,length,当这些方法触发的时候,将当前闭包函数到subscribe保存起来。当同一个对象的addProperty,removeProperty,push,pop触发的时候,publish对应的闭包函数。
  • 4.1.3 observable.ref

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { autorun, observable } from '@formily/reactive'
    export default function testRef() {
    // ref就是为了弥补基础类型无法倾听set与get的问题// ref将基础类型包装一个object,{value:xxx}里面
    const ref = observable.ref(1)

    autorun(() => {
    console.log(ref.value)
    })

    ref.value = 123
    }

    在js环境中,只有object类型才能侦听数据set操作。对于一个基础类型的数据,无法倾听它的set操作。ref操作就是为了包装它实现的

    4.1.4 observable.box

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { autorun, observable } from '@formily/reactive'
    export default function testRef() {
    // box类型与ref类型类似// 不过它将基础类型包装为get()与set()方法而已
    const box = observable.box(1)

    autorun(() => {
    console.log(box.get())
    })

    box.set(123)
    }

    box类型也是类似ref类型的一样的功能,它只是换成了用get与set方法来包装基础类型而已

    4.2 收集get操作并自动触发

    4.2.1 autorun

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    import { autorun, observable } from "@formily/reactive"
    //autorun是收集get依赖,然后重新运行,它总是马上执行一次
    function testAutoRun1(){
    const obs = observable({
    aa:78
    })

    //autorun会执行两次
    //第一次是输出结果,并收集对字段进行get操作的依赖
    //第二次是当数据变化时,被set操作收集到,然后找出get操作的autorun方法,重新执行一遍,也会重新计算依赖
    const stop = autorun(() => {
    console.log(obs.aa)
    })

    obs.aa = 123
    //执行autorun的返回函数 则对应的autorun不会再继续监听值变化
    stop();

    //不会触发autorun
    obs.aa = 789
    }

    function testAutoRun2(){
    const obs = observable({
    aa:1,
    bb:3
    })

    //算上首次触发,一共是3次触发,而不是4次触发
    autorun(()=>{
    if( obs.aa == 1 || obs.bb == 2){
    console.log('true');
    }else{
    console.log("false");
    }
    })

    //这一句不会触发
    //因为收集get操作的时候,只判断到了obs.aa==1就已经提前终止了
    //所以autorun的第一次收集,只记录了obs.aa的数据
    obs.bb = 4//这一句触发了autorun,因为判断不满足,所以会触发obs.bb的数据记录
    obs.aa = 2//这一句也触发autorun
    obs.bb = 2
    }

    export default function testAutoRun(){
    testAutoRun1()
    testAutoRun2()
    }

    autorun就是在闭包中批量收集对数据get操作的依赖,当数据变化的时候,就会自动重新执行一次闭包,并且 重新收集get操作的依赖

    4.2.2 computed

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import { autorun, observable } from '@formily/reactive'
    // computed与autorun是类似的,
    // 它们都是收集get依赖,然后重新运行,它总是马上执行一次,
    // 唯一不同的是computed是有一个返回值,返回值是一个ref对象,这个ref对象是observable的
    export default function testComputed() {
    const obs = observable({
    aa: 11,
    bb: 22,
    })

    // 返回的数据用ref包装
    const computed = observable.computed(() => obs.aa + obs.bb)

    autorun(() => {
    console.log(computed.value)
    })

    obs.aa = 33
    }

    computed的想法也是很直观,autorun是数据变化时重新执行闭包,computed是数据变化重新计算派生值

    4.2.3 reaction

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    import { observable, reaction, autorun } from '@formily/reactive'
    // 语法糖,reaction其实就是computed与autorun的混合
    function testReaction1() {
    const obs = observable({
    aa: 1,
    bb: 2,
    })

    // 触发两次,初始化1次,更新后1次
    const dispose = reaction(() => obs.aa + obs.bb, console.log)

    obs.aa = 4
    dispose()
    }

    function testReaction2() {
    const obs = observable({
    aa: 1,
    bb: 2,
    })

    const computeValue = observable.computed(() => obs.aa + obs.bb)

    // 触发两次,初始化1次,更新后1次
    const dispose = autorun(() => {
    console.log(computeValue.value)
    })

    obs.aa = 4
    dispose()
    }
    export default function testReaction() {
    testReaction1()
    testReaction2()
    }

    reaction其实就是computed与autorun的组合而已,数据变化的时候,先重新计算派生值,然后拿派生值作为参数运行闭包

    4.2.4 tracker

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import { observable, Tracker } from '@formily/reactive'
    // Tracker是一个更为底层的方法
    // 首次触发需要手动调用track,与函数,来执行
    // 当数据变化后,回调自己,开发者可以在回调继续注册Tracker,也可以放弃注册
    export default function testTracker() {
    const obs = observable({
    aa: 11,
    })

    const view = () => {
    console.log('view go!!!')
    console.log(obs.aa)
    }

    const tracker = new Tracker(() => {
    // 收到数据变化的通知
    console.log('tracker other')

    // 再次执行view,并收集依赖
    tracker.track(view)
    })

    // 首次执行view,并收集依赖
    console.log('tracker first')
    tracker.track(view)

    obs.aa = 22

    tracker.dispose()
    }

    tracker是更为底层的方法,一般都很少用。autorun与computed都是数据变化的时候,自动重新触发和重新收集get操作依赖。而tracker就是仅一次触发,要想下次触发就必须手动调用track方法

    4.2.5 observe

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import { observable, observe } from '@formily/reactive'
    // observe仅在首次显式收集get依赖,而后每次发生变化,都通知一下,节点变化的情况
    // 它可以具体到某个对象的某个字段的触发
    export default function testObserve() {
    const obs = observable({
    aa: 11,
    bb: [1],
    })

    // 触发3次
    // obs.aa更改1次,obs.bb进行push的2次
    const dispose = observe(obs, (change) => {
    console.log('observe1', change)
    })

    obs.aa = 22
    // 触发2次
    // obs进行push的2次
    const dispose2 = observe(obs.bb, (change) => {
    console.log('observe2', change)
    })

    obs.bb.push(1)
    obs.bb.push(2)

    dispose()
    dispose2()
    }

    observe的方法更为底层,它会输出数据是如何变化的这个信息,并不会重新收集依赖

    4.3 批量触发

    4.3.1 batch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import { observable, autorun, batch } from '@formily/reactive'
    export default function testBatch() {
    // 空字段的时候也能倾听
    const obs = observable<any>({
    aa: 1,
    })

    // 触发2次,首次,以及修改1次
    autorun(() => {
    console.log(obs.aa, obs.bb)
    })

    // 设置了两次,但是只触发1次,这是batch,批量触发的特性
    batch(() => {
    obs.aa = 321
    obs.bb = 'dddd'
    })
    }

    batch操作时,只有batch方法执行完成以后,才批量触发一次autorun的通知,这样能提高性能,避免重复触发。

    4.3.2 batch.scope

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import { batch, observable, autorun } from '@formily/reactive'
    export default function testBatchScope() {
    const obs = observable<any>({})

    // 共4次触发
    // 首次,以及后续的4次修改
    autorun(() => {
    console.log(obs.aa, obs.bb, obs.cc, obs.dd)
    })

    // 这里触发3次
    batch(() => {
    // scope里面第1次
    batch.scope(() => {
    obs.aa = 123
    })

    // scope里面第2次
    batch.scope(() => {
    obs.cc = 'ccccc'
    })

    // 两句都在外面的batch,它们是第3次触发
    obs.bb = 321
    obs.dd = 'dddd'
    })
    }

    batch.scope就是支持嵌套批量的能力而已,就像事务里面嵌套事务

    4.4 倾听值变化的语法糖

    reative提供了语法糖的方法,来帮助我们更快地建立字段的observable,ref,box,shallow,方法的batch这些操作而已

    4.4.1 action

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import { observable, action, autorun } from '@formily/reactive'
    // action是一种语法糖,将方法包装为batch
    export default function testAction() {
    const obs = observable({
    aa: 1,
    bb: 2,
    })

    // 这里触发2次
    // 首次1次
    // 被action包装的方法1次
    autorun(() => {
    console.log(obs.aa, obs.bb)
    })

    // 传入一个方法,返回一个包装的方法
    // 这个方法的内容里面就是batch的
    const method = action(() => {
    obs.aa = 123
    obs.bb = 321
    })

    method()
    }

    action能快速帮助将一个闭包,用batch包围起来

    4.4.2 define

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    import { define, observable, autorun, action } from '@formily/reactive'
    export default function testDefine() {
    class DomainModel {
    deep = { aa: 1 }

    shallow = {}

    // 因为基础类型被box引用了,代码会被变为box.set,box.get,但是这里ts感知不到
    box = 0
    // ref引用包装后,字段变为{value}类型,这里也是ts感知不到的
    ref = ''
    constructor() {
    // define对typescript的支持并不友好
    // 左边是字段或者方法名,右边是包装的方法
    define(this, {
    deep: observable,
    shallow: observable.shallow,
    box: observable.box,
    ref: observable.ref,
    computed: observable.computed,
    go: action,
    })
    }

    get computed() {
    return this.deep.aa + this.box.get()
    }

    go(aa, box) {
    this.deep.aa = aa
    this.box.set(box)
    }
    }

    const model = new DomainModel()

    autorun(() => {
    console.log(model.computed)
    })

    model.go(1, 2)
    model.go(1, 2) // 重复调用不会重复响应
    model.go(3, 4)
    }

    define的这个方法挺不好的,建议不要用,对TypeScript的支持不好,而且也不快捷

    4.4.3 model

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import { model, autorun } from '@formily/reactive'
    // model是一个更好的语法糖
    export default function testModel() {
    const obs = model({
    // 普通属性自动声明 observable
    // 它不是针对某个字段包装为observable,而是以整个model为根,包装为observable,注意与define的不同
    aa: 1,
    bb: 2,

    // getter/setter 属性自动声明 computed
    get cc() {
    return this.aa + this.bb
    },

    // 函数自动声明 action,也就是被batch包围了
    update(aa: number, bb: number) {
    this.aa = aa
    this.bb = bb
    },
    })

    // 这段触发3次
    // 首次渲染
    // 第2次是单独赋值obs.aa
    // 第3次是执行被batch包围的update方法
    autorun(() => {
    console.log(obs.cc)
    })

    // 单独赋值
    obs.aa = 3// 调用了被batch包装的方法
    obs.update(4, 6)
    }

    model这个语法糖超级好:

  • 字段自动用observable包装
  • getter与setter方法用computed包装
  • 普通方法用action包装
  • 4.5 结合react使用

    利用@formily/reactive-react 或 @formily/react 都可,使用导出的Observer组件包裹函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import { Field, Observer } from '@formily/react';
    import { observable } from '@formily/reactive';
    const state = observable({
    name: 1
    })
    export default () => {
    return <Observer>
    {
    () => <>
    { state.name }
    <Button onClick={() => (state.name += 1)}>+1</Button>
    </>
    }
    <Observer>

    }

    路径

    FormPath 是 Formily 2.0 中一个能力,相比于 Formily 1.x 中的 cool-path ,它提供了更加清晰、易用、强大的路径管理能力。

    解构表达式

  • 解构表达式会作为点路径的某个节点,我们可以把它看做一个普通字符串节点,只是在数据操作时会生效,所以在匹配语法中只需要把解构表达式作为普通节点节点来匹配即可
  • 在 setIn 中使用解构路径,数据会被解构
  • 在 getIn 中使用解构路径,数据会被重组
  • 1
    2
    3
    4
    5
    6
    7
    8
    import { FormPath } from '@formily/core'

    const target = {}

    FormPath.setIn(target, 'parent.[aa,bb]', [11, 22])
    console.log(target) //{parent:{aa:11,bb:22}}
    console.log(FormPath.getIn(target, 'parent.[aa,bb]')) //[11,22]
    console.log(FormPath.parse('parent.[aa,bb]').toString()) //parent.[aa,bb]
  • n 个点代表往前 n-1 步,中括号中可以用下标计算表达式:[+]代表当前下标+1,[-]代表当前下标-1,[+n]代表当前下标+n,[-n]代表当前下标-n
  • 1
    console.log(FormPath.parse('..[+10].dd', 'aa.1.cc').toString()) //aa.11.dd

    全匹配/局部匹配/分组匹配/反向匹配

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { FormPath } from '@formily/core'
    // 全匹配
    console.log(FormPath.parse('*').match('aa.bb')) //true
    // 局部匹配
    console.log(FormPath.parse('aa.*.cc').match('aa.bb.cc')) //true
    // 分组匹配
    console.log(
    FormPath.parse('aa.*(bb,kk,dd,ee.*(oo,gg).gg).cc').match('aa.bb.cc')
    ) //true
    // 反向匹配
    console.log(FormPath.parse('*(!aa,bb,cc)').match('kk')) //true

    field的query可以基于当前字段查询相邻字段,也就是说field的query可以基于同一父path去查询,而Form的query则无限制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 此时路径下存在 {people:{info: {name: 1, age: 2}, hobby:1}}

    ...
    <Field
    name="info.age"
    component={[
    Input
    ]}
    reactions={[
    (field) => {
    // 相对路径查询
    // 此时查询 info下的name字段的值
    console.log(field.query('.name').value())
    // 查询hobby
    console.log(field.query('..hobby').value())
    }
    ]}
    >
    </Field>
    ...

    校验功能

    校验类型参考

    简单示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <Field
    required
    validator={{
    triggerType: 'onBlur', // 触发方式
    required: true,
    validator: (value) => { // 返回字符串表示存在错误
    return ""
    }
    }}
    ></Field>
    formily2学习笔记