0 概述
0.1 是什么
Formily的Reactive包,官方文档在这里
Reative包其实就是MobX的另外一个实现而已,根据官方的介绍,Reactive的特点是:
- 完全兼容MobX的API,可以无缝替换
- 不支持IE浏览器,包袱更少,性能更好,打包也更少
- 支持React的并发渲染
性能比MobX要好一点,不过,说实话,大部分情况下随便用什么框架都很少卡顿了
0.2 为什么
Reactive其实就是个类MobX的框架,就是以MVVM为思想的框架。怎么了,React提倡的单向数据流不好吗,Redux不好吗,为啥要用类似Vue的MVVM想法。
这其实只是个不同场景的选择问题而已:
Redux框架,View层修改Store数据,只能通过dispatch。这是因为每次View渲染的数据都是不同的数据引用,你在View层把数据改了,Store的数据依然是没改的,这样是无法触发其他组件重新render的。它可以说是严格版本的MVC,从实现角度杜绝了View层乱写业务逻辑代码的问题。Redux的这种严格性,最终使得项目更轻松地堆代码了,不太容易一团槽,因为所有的业务逻辑都被迫填写在dispath的action上面,代码分层干净整洁。对于多人协作的大项目,真的非常好用。但是,Redux的缺点是,Reducer的代码真的是太繁琐了,每次都是写相似的数据修改逻辑。
MVVM框架,以响应式数据的框架,每次View的渲染都是共用同一个数据引用。最爽的是写代码直接改数据上的引用数据,那么所有对应的其他组件都会自动更新,这样显然性能要好得多,而且代码真的直观,完全的命令方式编程,一点啰嗦都没有。它的缺点刚好就是Redux的优点所在,MVVM框架无法阻止开发者在View层写业务逻辑。当代码大幅膨胀的时候,多个组件都能修改这些引用数据,数据变了,但你不知道是哪个组件触发的这种变动,是真的维护头痛。
MobX允许开发者在View层直接修改数据的方式过于粗暴
这是Redux的作者对MobX框架的评价(找不到原文,大概就是如此)。
MobX也吸收了Redux的评价,做出了另外一个框架mobx-state-tree,结合了Redux与MobX的特点。在数据修改逻辑上,允许使用像MobX的方法,直接在原地修改,而不需要像Redux写一堆Reducer。在页面刷新方面,每次View层渲染所用的数据都是不同的引用,不允许开发者直接在View层修改数据。我个人认为,这个项目确实很不错。
当然,在一些特殊的场景后,例如后台管理系统,每个页面都是自己单独的渲染数据,很少需要多个页面共享数据的。而且每个页面由于表单多而且字段多,如果用Redux的方式写代码,Reducer会多到哭。这种情况,仅仅使用MVVM就会相当合适,修改数据仅仅就是一个赋值操作而已。而且一个页面的数据修改逻辑都仅仅集中一个tsx文件中,不会散落在多个文件,也不会出现MVVM框架中View层直接写业务逻辑导致的难以维护的问题。
最后,总结一下:
- MVVM,后台管理系统(不需要跨页面共享数据),简单页面。
- mobx-state-tree,大前端,需要跨页面共享数据且页面很多的场景
- Hook+Immer,我也很喜欢这种的架构,相当于一个加强版的mobx-state-tree。Immer简化数据修改逻辑,并且也能用到Hook的易于复用业务逻辑的特性,对TypeScript的支持也是一流,可维护性也不错。
0.3 有什么
reactive框架的内容主要包括:
- 倾听数据的set操作
- 收集数据的get操作,然后当数据set的时候重新触发
- 当数据set操作的时候,批量触发
- 倾听数据的set操作的语法糖
- 与react的集成
1 倾听set操作
代码在这里
1.1 observable
import { observable, autorun } from '@formily/reactive'
function testObservable1() {
// 将一个对象变化可观察的,就是倾听它的set操作
const obs = observable({
: {
aa: 123,
bb,
}: {
cc: 123,
dd,
}
})autorun(() => {
// 首次的时候会触发,变化的时候也会触发
// 总共触发2次
console.log('normal1', obs.aa.bb)
})
//数据进行set操作的时候,就会触发
.aa.bb = 44
obs
autorun(() => {
// 当值相同的时候,不会重复触发
// 这里只会触发1次
console.log('normal2', obs.cc.dd)
})
.cc.dd = 123
obs
}
function testObservable2() {
const obs = observable({
: {
aa: 123,
bb,
}: {
cc: 123,
dd,
}: {
ee: 123,
ff,
}: {
gg: 123,
hh,
}
})
autorun(() => {
// 整个字段被赋值的话,就会触发
// 所以,这里触发2次
console.log('object1', obs.cc)
})
.cc = { dd: 456 }
obs
autorun(() => {
// 这里会触发2次,虽然值相同
// 但是object的比较是通过引用比较的
console.log('object2', obs.ee)
})
.ee = { ff: 123 }
obs
autorun(() => {
// 只是倾听aa字段的话,那么子字段的变化是不会触发的
// 因为obs.aa的引用没有变化
// 所以这里只触发1次
console.log('object3', obs.aa)
})
console.log('testObservable2 set data')
.aa.bb = 44
obs
autorun(() => {
// 主体变化的时候,子的也要变化
// 所以这里触发2次
console.log('object4', obs.gg.hh)
})
console.log('testObservable2 set data2')
.gg = { hh: 45 }
obs
}
function testObservable3() {
const obs = observable({
: {
aa: ['a'],
bb,
}
})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')
.aa.bb.push('cc')
obs
}
function testObservable4() {
const obs = observable({
: {
aa: ['a'],
bb,
}: '78',
cc
})autorun(() => {
// 倾听其他字段的话当然也不会触发
console.log('other', obs.cc)
})
console.log('testObservable4 set data')
.aa.bb.push('cc')
obs
}
function testObservableShadow() {
const obs = observable.shallow({
: {
aa: 'a',
bb,
}: {
cc: 'a',
dd,
}
})
autorun(() => {
// 这里只会触发1次,因为是浅倾听set操作
console.log('shadow1', obs.aa.bb)
})
console.log('testObservableShadow set data1')
.aa.bb = '123'
obs
autorun(() => {
// 这里会触发2次,aa属于浅倾听的范围
console.log('shadow2', obs.cc)
})
console.log('testObservableShadow set data2')
.cc = { dd: 'c' }
obs
}export default function testObservable() {
testObservable1()
testObservable2()
testObservable3()
testObservable4()
testObservableShadow()
}
可以看到触发的规则为:
- number与string的基础类型,值比较发生变化了会触发
- object与array的复合类型,引用发生变化了会触发,object的字段添减不会触发,array的push和pop也不会触发
- array.length,它属于字段的基础类型变化,所以也会触发
- object与array类型,对于自己引用整个变化的时候,它也会触发子字段的触发
浅倾听shadow,只能处理表面一层的数据
1.2 复杂对象的obserable
import { observable, autorun } from '@formily/reactive'
function testObservable1_object() {
const obs = observable({
: {
aa: 123,
bb,
}
})autorun(() => {
// 触发2次
// 首次
// 自身赋值1次
console.log('normal1_object', obs.aa)
})
// 不会触发,子字段的变化不会影响到父字段的触发
console.log('1. sub assign')
.aa.bb = 44
obs
// 会触发
console.log('2. self assign')
.aa = { bb: 789 }
obs
}
function testObservable2_object() {
const obs = observable({
: {
aa: 123,
bb,
}
})autorun(() => {
// 触发2次
// 首次
// 自身赋值1次
// 赋值如同console,一样是向对象执行get操作
const mk = obs.aa
console.log('normal2_object')
})
// 不会触发,子字段的变化不会影响到父字段的触发
console.log('1. sub assign')
.aa.bb = 44
obs
// 会触发1次
console.log('2. self assign')
.aa = { bb: 789 }
obs
}
function testObservable3_object() {
const obs = observable({
: {},
aaas any
}) autorun(() => {
// 触发1次
// 首次
const mk = obs.aa
console.log('normal3_object')
})
// 不会触发,object的添加property不会触发
console.log('1. self add property')
.aa.bb = 4
obs
// 不会触发,obs.aa.bb的赋值不会触发
console.log('2. self assign')
.aa.bb = 5
obs
// 不会触发,object的移除property不会触发
console.log('3. self remove property')
delete obs.aa.bb
}
function testObservable4_object() {
const obs = observable({
: {},
aaas 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')
.aa.bb = 4
obs
// 不会触发,obs.aa.bb的赋值不会触发
console.log('2. self assign')
.aa.bb = 5
obs
// 会触发,object的移除property不会触发
console.log('3. self remove property')
delete obs.aa.bb
}
function testObservable1_array() {
const obs = observable({
: [] as number[],
aa
})autorun(() => {
// 一共触发了1次
// 首次
const mk = obs.aa
console.log('normal1_array')
})
// 不会触发,相当于object的添加property而已
console.log('1.push')
.aa.push(1)
obs
// 不会触发
console.log('2.assign')
.aa[0] = 3
obs
// 不会触发
console.log('3.push')
.aa.push(4)
obs
// 不会触发,相当于object的移除property而已
console.log('4.pop')
.aa.pop()
obs
// 不会触发
console.log('5.assign')
.aa[0] = 5
obs
}
function testObservable2_array() {
const obs = observable({
: [] as number[],
aa
})autorun(() => {
// 一共触发了5次
// 首次,
// push 的2次
// pop的2次,pop一次,触发2次
console.log('normal2_array', obs.aa.length)
})
// 会触发,因为push影响到了length字段
console.log('1.push')
.aa.push(1)
obs
// 不会触发,因为对某个元素赋值不影响length字段
console.log('2.assign')
.aa[0] = 3
obs
// 会触发,因为push影响到了length字段
console.log('3.push')
.aa.push(4)
obs
// 会触发,因为pop影响到了length字段,这个会触发2次,不知道为什么
console.log('4.pop')
.aa.pop()
obs
// 不会触发,因为对某个元素赋值不影响length字段
console.log('5.assign')
.aa[0] = 5
obs
}
function testObservable3_array() {
const obs = observable({
: [] as any[],
aa
})autorun(() => {
// 一共触发了6次
// 首次,
// push 的2次
// pop的1次
// 赋值的2次
.aa.map((item) => '')
obsconsole.log('normal3_array')
})
// 会触发,因为影响到了map
console.log('1.push')
.aa.push(1)
obs
// 会触发,因为影响到了map
console.log('2.assign')
.aa[0] = 3
obs
// 会触发,因为影响到了map
console.log('3.push')
.aa.push({})
obs
// 不会触发,嵌套元素赋值
console.log('4.inner assign')
.aa[1].kk = 3
obs
// 会触发,因为影响到了map
console.log('5.pop')
.aa.pop()
obs
// 会触发,因为影响到了map
console.log('6.assign')
.aa[0] = 5
obs
}
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对应的闭包函数。
1.3 observable.ref
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)
})
.value = 123
ref }
在js环境中,只有object类型才能侦听数据set操作。对于一个基础类型的数据,无法倾听它的set操作。ref操作就是为了包装它实现的
1.4 observable.box
import { autorun, observable } from '@formily/reactive'
export default function testRef() {
// box类型与ref类型类似
// 不过它将基础类型包装为get()与set()方法而已
const box = observable.box(1)
autorun(() => {
console.log(box.get())
})
.set(123)
box }
box类型也是类似ref类型的一样的功能,它只是换成了用get与set方法来包装基础类型而已
2 收集get操作并自动触发
代码在这里
2.1 autorun
import { autorun, observable } from "@formily/reactive"
//autorun是收集get依赖,然后重新运行,它总是马上执行一次
function testAutoRun1(){
const obs = observable({
:78
aa
})
//autorun会执行两次
//第一次是输出结果,并收集对字段进行get操作的依赖
//第二次是当数据变化时,被set操作收集到,然后找出get操作的autorun方法,重新执行一遍,也会重新计算依赖
const dispose = autorun(() => {
console.log(obs.aa)
})
.aa = 123
obs
//释放autorun,不再自动执行了
dispose()
//这一句的set操作不再触发autorun了
.aa = 789
obs
}
function testAutoRun2(){
const obs = observable({
:1,
aa:3
bb
})
//算上首次触发,一共是3次触发,而不是4次触发
autorun(()=>{
if( obs.aa == 1 || obs.bb == 2){
console.log('true');
else{
}console.log("false");
}
})
//这一句不会触发
//因为收集get操作的时候,只判断到了obs.aa==1就已经提前终止了
//所以autorun的第一次收集,只记录了obs.aa的数据
.bb = 4
obs
//这一句触发了autorun,因为判断不满足,所以会触发obs.bb的数据记录
.aa = 2
obs
//这一句也触发autorun
.bb = 2
obs
}
export default function testAutoRun(){
testAutoRun1()
testAutoRun2()
}
autorun就是在闭包中批量收集对数据get操作的依赖,当数据变化的时候,就会自动重新执行一次闭包,并且重新收集get操作的依赖
2.2 computed
import { autorun, observable } from '@formily/reactive'
// computed与autorun是类似的,
// 它们都是收集get依赖,然后重新运行,它总是马上执行一次,
// 唯一不同的是computed是有一个返回值,返回值是一个ref对象,这个ref对象是observable的
export default function testComputed() {
const obs = observable({
: 11,
aa: 22,
bb
})
// 返回的数据用ref包装
const computed = observable.computed(() => obs.aa + obs.bb)
autorun(() => {
console.log(computed.value)
})
.aa = 33
obs }
computed的想法也是很直观,autorun是数据变化时重新执行闭包,computed是数据变化重新计算派生值
2.3 reaction
import { observable, reaction, autorun } from '@formily/reactive'
// 语法糖,reaction其实就是computed与autorun的混合
function testReaction1() {
const obs = observable({
: 1,
aa: 2,
bb
})
// 触发两次,初始化1次,更新后1次
const dispose = reaction(() => obs.aa + obs.bb, console.log)
.aa = 4
obs
dispose()
}
function testReaction2() {
const obs = observable({
: 1,
aa: 2,
bb
})
const computeValue = observable.computed(() => obs.aa + obs.bb)
// 触发两次,初始化1次,更新后1次
const dispose = autorun(() => {
console.log(computeValue.value)
})
.aa = 4
obs
dispose()
}export default function testReaction() {
testReaction1()
testReaction2()
}
reaction其实就是computed与autorun的组合而已,数据变化的时候,先重新计算派生值,然后拿派生值作为参数运行闭包
2.4 tracker
import { observable, Tracker } from '@formily/reactive'
// Tracker是一个更为底层的方法
// 首次触发需要手动调用track,与函数,来执行
// 当数据变化后,回调自己,开发者可以在回调继续注册Tracker,也可以放弃注册
export default function testTracker() {
const obs = observable({
: 11,
aa
})
const view = () => {
console.log('view go!!!')
console.log(obs.aa)
}
const tracker = new Tracker(() => {
// 收到数据变化的通知
console.log('tracker other')
// 再次执行view,并收集依赖
.track(view)
tracker
})
// 首次执行view,并收集依赖
console.log('tracker first')
.track(view)
tracker
.aa = 22
obs
.dispose()
tracker }
tracker是更为底层的方法,一般都很少用。autorun与computed都是数据变化的时候,自动重新触发和重新收集get操作依赖。而tracker就是仅一次触发,要想下次触发就必须手动调用track方法
2.5 observe
import { observable, observe } from '@formily/reactive'
// observe仅在首次显式收集get依赖,而后每次发生变化,都通知一下,节点变化的情况
// 它可以具体到某个对象的某个字段的触发
export default function testObserve() {
const obs = observable({
: 11,
aa: [1],
bb
})
// 触发3次
// obs.aa更改1次,obs.bb进行push的2次
const dispose = observe(obs, (change) => {
console.log('observe1', change)
})
.aa = 22
obs
// 触发2次
// obs进行push的2次
const dispose2 = observe(obs.bb, (change) => {
console.log('observe2', change)
})
.bb.push(1)
obs.bb.push(2)
obs
dispose()
dispose2()
}
observe的方法更为底层,它会输出数据是如何变化的这个信息,并不会重新收集依赖
3 批量触发
代码在这里
3.1 batch
import { observable, autorun, batch } from '@formily/reactive'
export default function testBatch() {
// 空字段的时候也能倾听
const obs = observable<any>({
: 1,
aa
})
// 触发2次,首次,以及修改1次
autorun(() => {
console.log(obs.aa, obs.bb)
})
// 设置了两次,但是只触发1次,这是batch,批量触发的特性
batch(() => {
.aa = 321
obs.bb = 'dddd'
obs
}) }
batch操作时,只有batch方法执行完成以后,才批量触发一次autorun的通知,这样能提高性能,避免重复触发。
3.2 batch.scope
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次
.scope(() => {
batch.aa = 123
obs
})
// scope里面第2次
.scope(() => {
batch.cc = 'ccccc'
obs
})
// 两句都在外面的batch,它们是第3次触发
.bb = 321
obs.dd = 'dddd'
obs
}) }
batch.scope就是支持嵌套批量的能力而已,就像事务里面嵌套事务
4 倾听set操作的语法糖
代码在这里
reative提供了语法糖的方法,来帮助我们更快地建立字段的observable,ref,box,shallow,方法的batch这些操作而已
4.1 action
import { observable, action, autorun } from '@formily/reactive'
// action是一种语法糖,将方法包装为batch
export default function testAction() {
const obs = observable({
: 1,
aa: 2,
bb
})
// 这里触发2次
// 首次1次
// 被action包装的方法1次
autorun(() => {
console.log(obs.aa, obs.bb)
})
// 传入一个方法,返回一个包装的方法
// 这个方法的内容里面就是batch的
const method = action(() => {
.aa = 123
obs.bb = 321
obs
})
method()
}
action能快速帮助将一个闭包,用batch包围起来
4.2 define
import { define, observable, autorun, action } from '@formily/reactive'
// model是语法糖
export default function testDefine() {
class DomainModel {
= { aa: 1 }
deep
= {}
shallow
// 因为基础类型被box引用了,代码会被变为box.set,box.get,但是这里ts感知不到
= 0
box
// ref引用包装后,字段变为{value}类型,这里也是ts感知不到的
= ''
ref
constructor() {
// define对typescript的支持并不友好
// 左边是字段或者方法名,右边是包装的方法
define(this, {
: observable,
deep: observable.shallow,
shallow: observable.box,
box: observable.ref,
ref: observable.computed,
computed: action,
go
})
}
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)
})
.go(1, 2)
model.go(1, 2) // 重复调用不会重复响应
model.go(3, 4)
model }
define的这个方法挺不好的,建议不要用,对TypeScript的支持不好,而且也不快捷
4.3 model
import { model, autorun } from '@formily/reactive'
// model是一个更好的语法糖
export default function testModel() {
const obs = model({
// 普通属性自动声明 observable
// 它不是针对某个字段包装为observable,而是以整个model为根,包装为observable,注意与define的不同
: 1,
aa: 2,
bb
// 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)
})
// 单独赋值
.aa = 3
obs
// 调用了被batch包装的方法
.update(4, 6)
obs }
model这个语法糖超级好:
- 字段自动用observable包装
- getter与setter方法用computed包装
- 普通方法用action包装
5 react集成
代码在这里
reactive对自己的定位是与UI无关的响应式框架,它将与react集成的事情交给了另外一个包@formily/reactive-react
5.1 基础使用
import React from 'react';
import { observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
const obs = observable({
value: 'Hello world',
});
//对函数组件的包装,只要函数的组件任意一个observable变量变化时,就会自动刷新该函数组件
export default observer(() => {
return (
<div>
<div>
<input
style={{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}}
value={obs.value}
onChange={(e) => {
obs.value = e.target.value;
}}
/>
</div>
<div>{obs.value}</div>
</div>
);
});
做法还是比较简单的,对一个函数组件用observer包装一下就可以了,它就能当组件内的可观察对象变化的时候自动render。相当于普通闭包中的autorun。
5.2 多计数器使用
我们在React Hook经验汇总中探讨过一个多key变化的计数器用例,我们用reactive来重写一遍吧。
import { model } from '@formily/reactive';
export type CounterEnum = 'fish' | 'cat';
let CounterStore = model({
: 0,
fish: 0,
catinc(type: CounterEnum) {
this[type]++;
,
}dec(type: CounterEnum) {
this[type]--;
,
}get(type: CounterEnum) {
return this[type];
,
};
})
//去除字段,在编译层,禁止调用字段
type CounterType<T> = Omit<T, 'fish' | 'cat'>;
function extractMethod<T>(a: T): CounterType<T> {
return (a as unknown) as CounterType<T>;
}
export default extractMethod(CounterStore);
先定义一个Store,用model语法糖真的好简单,代码也不需要遵循数据immutable的原则,只需要原地修改就行了。
import { observable } from '@formily/reactive';
import { observer } from '@formily/reactive-react';
import { useState } from 'react';
import CounterStore, { CounterEnum } from './Store';
type Props = {
name: string;
mode: CounterEnum;
};
export default observer((props: Props) => {
let [mode, setMode] = useState<{ value: CounterEnum }>(() => {
return observable({
value: props.mode,
});
});
let counter = CounterStore.get(mode.value);
console.log('Child Render');
return (
<div>
<h2>{props.name}</h2>
<div>{'当前mode为:' + mode.value + ',当前值为:' + counter}</div>
<button onClick={CounterStore.inc.bind(CounterStore, mode.value)}>
加1
</button>
<button onClick={CounterStore.dec.bind(CounterStore, mode.value)}>
减1
</button>
<button
onClick={() => {
if (mode.value == 'fish') {
mode.value = 'cat';
} else {
mode.value = 'fish';
}
}}
>
{'切换mode'}
</button>
</div>
);
});
然后我们编写每一个组件,ChildButton,代码也非常明显,我们甚至连setMode都不需要更改,全部字段都是原地修改就可以了。使用useState,仅仅是因为每个组件都有自己的一个mode的状态,这些状态是不共用的。
import { observer } from '@formily/reactive-react';
import { observable } from '@formily/reactive';
import ChildButton from './ChildButton';
let data = observable({
open: true,
buttons: [0],
});
export default observer(() => {
console.log('Parent Render');
return (
<div>
<div>
<button
key="add"
onClick={() => {
data.buttons.push(data.buttons.length + 1);
}}
>
添加一个
</button>
<button
key="clear"
onClick={() => {
data.buttons = [];
}}
>
清除
</button>
<button
key="other"
onClick={() => {
data.open = !data.open;
}}
>
状态:{data.open ? '打开' : '关闭'}
</button>
</div>
{data.buttons.map((id) => {
return (
<ChildButton key={id} name={'按钮' + id} mode={'fish'} />
);
})}
</div>
);
});
对于一个组件只有一个实例的数据,我们可以将observable放到组件的外部,而不是在useState中声明。数据修改的逻辑全部都是原地更新,真是简单直观爆了。
如果我们对中间两个mode为cat的按钮,进行自增,会发现所有mode为cat的按钮都会一起自增,数据是跨组件同步的。与此同时,父组件没有render,其他mode为fish的按钮也不会render,性能达到最优的地步了。
6 FAQ
6.1 在autorun里面嵌套了batch依然可以收集依赖
这是reactive的特色功能,batch可以减少因为set操作造成的触发次数。一般情况下,batch操作,放在了autorun里面的话,autorun就不能收集batch操作里面的依赖。但是reative可以,具体看这里
6.2 Core库的componentProps不要传递Obserable数据
在componentProps中不要传递obserable类型的数据,会被自动toJS掉,失去响应能力,具体看这里
7 总结
Reactive的MVVM对于简单页面,以及轻逻辑的页面相当好用,直观,开发快。
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!