Formily的Reactive的经验汇总

2021-07-13 fishedee 前端

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: {
            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,只能处理表面一层的数据

1.2 复杂对象的obserable

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: {},
    }) as any
    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对应的闭包函数。

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)
    })

    ref.value = 123
}

在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())
    })

    box.set(123)
}

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

2 收集get操作并自动触发

代码在这里

2.1 autorun

import { autorun, observable } from "@formily/reactive"

//autorun是收集get依赖,然后重新运行,它总是马上执行一次
function testAutoRun1(){
    const obs = observable({
        aa:78
    })

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

    obs.aa = 123

    //释放autorun,不再自动执行了
    dispose()

    //这一句的set操作不再触发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操作的依赖

2.2 computed

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是数据变化重新计算派生值

2.3 reaction

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的组合而已,数据变化的时候,先重新计算派生值,然后拿派生值作为参数运行闭包

2.4 tracker

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方法

2.5 observe

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的方法更为底层,它会输出数据是如何变化的这个信息,并不会重新收集依赖

3 批量触发

代码在这里

3.1 batch

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的通知,这样能提高性能,避免重复触发。

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次
        batch.scope(() => {
            obs.aa = 123
        })

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

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

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({
        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.2 define

import { define, observable, autorun, action } from '@formily/reactive'

// model是语法糖
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.3 model

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包装

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({
    fish: 0,
    cat: 0,
    inc(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对于简单页面,以及轻逻辑的页面相当好用,直观,开发快。

相关文章