博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
react-control-center 对话 redux(家族),后生何以挑战前辈?
阅读量:6292 次
发布时间:2019-06-22

本文共 12876 字,大约阅读时间需要 42 分钟。

没有对比就没有伤害,react-control-center vs redux

以下会把react-control-center 简称为cc

对比项/项目 redux react-control-center 结果
git start 43k 43 没错,少了一个k,彻底完败
开源时间 2015年 2018年12月 早出生4年
作者 Dan Abramov 无名小辈 惨无人道的完败
架构实现 flux flux 平手
插件生态 redux-dev-tool等 无,未来提供 cc需要时间追赶?
中间件机制 提供 提供 平手
可对接的UI框架 react、vue、angular或其他 专注于react redux可以用他的基础库桥接其他UI框架,cc仅仅专注于react,此项无结果
代码组织 严格按照action、reducer的思路写出很多模板代码,所以无数redux wrapper出来了,让你更优雅的写redux,写副作用 内置了几个核心api,简单且强大,让reducer和action合为一体,甚至你都不需要感知到reducer的存在(如果你讨厌写dispatch&reducer,cc提供invoke) 不做评论,请各位看官看完在下结论

各位看官看到这里,肯定感慨良多,如果借用三体里的比喻,cc是地球文明的话,redux就是三体文明,或者再提升到歌者文明.......

简直就是托马斯回旋翻滚360度转体720度然后脸部着地的完败,但?真的是这样吗,还请你此刻不要把鼠标挪到关闭按钮上,先把下文读完,看看cc用什么来挑战redux,甚至整个redux家族。


回顾下,redux给予了我们什么

一个全局统一的状态树

redux内部维护着一个big object,官方称之为state tree或者store,这一棵参天大树携带的数据作为整个单页面应用的数据源,利用内置的中间件功能,结合提供的redux-dev-tool以及immutableJs提供的数据不可变特性,每次修改数据都会生成一个新的state记录在redux-dev-tool里,让我们在开发模式下实现了状态可追溯。

规范的数据修改方式

redux世界里约束了我们一定要通过派发一个action对象去修改state,生成action的函数称之为actionCreateor,修改state的函数称为reducer

题外话,关于reducer为什么称为reducer,我们引用下官网的原话:

It's called a reducer because it's the type of function you would pass to Array.prototype.reduce(reducer, ?initialValue)
翻译出来大概就是:之所以将函数叫为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue)里的回调函数reducer属于相同的类型。

所以不管命名怎样,我们已经达成了共识,action就是一个用type描述要使用什么reducer函数以及用payload描述传递什么数据给reducer函数的普通json对象,reducer函数负责把最新的状态传递给store,store负责把发生了变化的状态下发到各个关心这些变化的子组件上。

整理一下,三大核心概念actionreducerstore,会有如下关系

action (type,paylaod)  |______reducer(new state)            |______store                     |______render UI复制代码

cc给予了我们什么

一个模块化的的单一状态树

cc一开始就推荐用户按模块切分自己的状态,然后启动时将这些模块话的state交给cc,cc将它们合并出一个单一的状态树,当然针对这一点很多redux wrapper也做了改进,如dva提供了namespace让你的状态拥有自己的命名空间。

更灵活的修改数据的api

注册到cccc class,如果你仅仅像传统的方式一样使用setState去改变数据来驱动视图渲染,那么看起来和普通react class真的是没有什么不同之处的,但是cc class自生上下文携带了几个很重要的信息,即module表示属于哪个模块,sharedStateKeys表示共享这个模块的哪些key的状态,既然是共享,就意味着当前cc实例改变了这个key的值,cc会把它广播到其他同样属于这个模块并共享这个keycc class的实例,当然了,其他cc实例改变了这个key的值也会广播到当前实例并触发其渲染,cc内核的工作流程大致如下图所示:

可以看到此种模式下,
cc彻底解决了
redux里几个问题

  • action命名膨胀,redux里提倡的reducer是纯函数,每次返回的一定是一个全新的state,因为redux需要只是利用浅比较的方式知道状态有没有发生变化,所以我们通常会看到如下代码
// code in counter-actions.jsexport function inc(){    return {
type:'INC_COUNT'}}export function dec(){ return {
type:'INC_COUNT'}}// code in counter-reducer.jsexport default function reducer(initState, action){ const {
type, payload} = action; switch(type){ case 'INC_COUNT': return {...state, count:initState.count+1}; case 'DEC_COUNT': return {...state, count:initState.count-1}; default: return initState; }}复制代码

cc是接管了react最原始的setState函数做扩展,就像react.setState(partialState, callback)描述的一样,所以对于cc来说,真的只需要一个片段state就够了,cc通过分析用户提交的partialState,足以知道用户的此次操作改变了哪些状态,所以我们的Counter可以写为

//也可以简写为@cc.r('Counter',{m:'counter', s:'*'})@cc.register('Counter',{module:'counter', sharedStateKeys:'*'})class Counter extends Component{    inc = ()=> { this.setState({count:this.state.count+1}) }    dec = ()=> { this.setState({count:this.state.count-1}) }    render(){        const {count} = this.state;        return (            
{count}
); }}复制代码

如果你的App实例化了多个Counter,他们将共享count

render(){    return (        
);}复制代码

实际上你可能发现一个问题,redux严格约定的action type本意是用来追溯是什么动作改变了state!可是真的想想,仅靠action type能够知道什么动作改变了state就够了吗?在一个大型的复杂项目里,通常你是需要知道具体到那个UI改变了状态,但是你会发现有很多UI都会派发同一个action type,这要怎么追,为每一个动作都命名一个不一样的action type但是其实操作的数据和修改的动作是一摸一样的?

cc里你只要为组件标记一个ccKey就够了,你可以写一个简答的中间件函数打印,cc会告诉你此次修改的所有细节,后期提供的cc-dev-tool会结合immutableJs来构建一个可追溯的状态历史

cc.startup(    {        //...        middlewares: [            function myMiddleware1(context, next){                //ccKey, fnName, module, calledBy, state等                console.log(context);                next();            }        ]    })复制代码
  • 副作用代码难以编写和复用,尽管有redux-saga之类的来解决此类问题,可是我们重新审视一下cc的设计,天生的对副作用的代码书写是友好的。 然后抛弃setState,使用dispatch来改变状态,在cc class内部可以使用this.$$dispatch(action:Action|String, payload?:any)来完成,注意一点哦,因为上面说到了,对于cc来说只需要提交一个partialState就够了,所以实际上actionCreatorreducer被精简为一体了,在ccreducer函数负责接到状态,然后返回一个新的partialState就够了。
//code in Counter classinc() => this.$$dispatch({
type:'inc'});dec()=> this.$$dispatch('dec');//我们启动cc配置的reducer如下cc.startup({ //... reducer:{ inc({state, payload, dispatch}){ return {count: state.count+1}; }, dec({state}){ return {count: state.count+1}; } }})复制代码

说好的副作用书写友好在哪里呢?我们留意的可以看到reducer函数参数列表里还解构出其他东东,比如dispatch,来来来,让我们提个需求,新增一个按钮,点击这个按钮时,先加10,然后过2秒钟自动减5,然后再过3秒直接变成100,因为在cc的recuder里是不强制一定要返回一个新的partialState的,不返回只是不会触发渲染而已,但是解构出来的dispatch是一个组合复用其他reducer函数的哦,让我们清爽的实现这个需求。

async function sleep(ms=1000){    return new Promise((resolve)=>setTimeout(resolve, ms));}//reducer修改如下reducer:{    inc({state, payload:count=1, dispatch, effect}){        return {count: state.count+count};    },    dec({state, payload:count=1}){        return {count: state.count-count};    },    async funnyInc({await}){        await dispatch('inc', 10);        await sleep(5);        await dispatch('dec', 5);    }}//Counter里funnyInc() => this.$$dispatch('funnyInc');//render里//甚至你可以使用$$domDispatch,来减少这样没有必要的函数定义复制代码

现在你可以放心喝一口茶了,看到界面上会如你所想的工作,组合现有的reducer函数是一件多么轻松惬意的事情,注意哦dispatch返回一个Promise,某些场景时机你可能不需要await,这个就取决于具体业务了。

我们进一步思考下中间件函数打印出的东西里有ccKey, fnName, module, calledBy, state等,以及图中提到的effect,我们所有做的事情只是返回一个新的partialState,一定需要走dispatchreducer这种模式吗?当然是否,cc提供effect(moduleName:String, userFunction:Function, ...args)就是让你直接调用自己的业务函数,返回一个新的partialState就好了,那么我们funnyInc可以改写为:

async function sleep(ms=1000){    return new Promise((resolve)=>setTimeout(resolve, ms));}async function inc(prevCount, count=1){    return {count: prevCount+1};}async function dec(prevCount, dec=1){     return {count: prevCount-1};}async function myFunnyInc({effect, dispatch, state}, count){    await effect('count', inc, state.count, 10);    await sleep(5);    await effect('count', dec, state.count, 5);}//Counter里, 注意此处用的$$xeffect,用户自定义的函数参数列表第一位会是cc注入的ExecutionObject,里面可以解构出相关其他句柄和数据funnyInc() => this.$$xeffect('count', myFunnyInc);复制代码

如果用户留意的话,发现上面$$xeffect调用的用户自定义函数的第一位参数里也解构出了dispatch,如果你再往上看,会发现reducer方法里也解构出了effectxeffect,如你想所想,他们可以混合使用,你可以在reducer里用effect,也可以在effect调用的函数使用dispatch,能够完美的工作起来,事实上你可能再想.......这样穿插的调用,还怎么保证状态可追踪?你可能忘了,任何调用cc都会知道上下文,由那个cc实例最初发起调用,使用了什么方式setStatedispatcheffect或者其他,如果是dispatchtype是什么,如果是effect,调用的自定义函数名字是什么等,真正让你从源头知道是从那里开始,走了一个怎样的流程,改变了那些状态,是不是够你追溯了呢?

  • 更加优雅的组件间通信,让我们仔细想想,redux真正的算是解决了组件间通信吗?基于状态去做?让我们看看cc里是怎么实现的
@cc.r('Counter',{m:'counter', s:'*'})class Counter extends Component{    componentDidMount(){        const id = this.props.id;        this.$$on('cool',(p1, p2)=>{            //做你任意想做的事吧            alert(p1+p2+id);        })        this.$$onIdentity('cool', id, (p1, p2)=>{            alert(p1+p2+id);        })    }}//App render里      
复制代码

当你点击emit按钮时,5个<Counter/>都会收到事件然后弹出显示,当你点击emitIdentity按钮时,只有id为1的那个<Counter/>会弹出提示,是不是更直白和优雅?

事实上可能有细心的读者注意到每次组件componentDidMount都会触发$$on,会不会造成内存泄露,需不需要人工off?尽管cc提供了api让你可以使用this.$$off(eventName:String),但是这里cc在这里已经在每次组件卸载时off掉这写监听了,不需要你再去componentWillUnmount里实现了。

  • 计算属性呢?reduxmapStateProps里可以让用户重新计算注入到组件里的值,让我们看看cc怎么样更直白的实现
@cc.r('Counter',{m:'counter', s:'*'})class Foo extends Component{    $$computed(){        return {            count(count){                return count*100;            }        }    }    render(){        const {count} = this.$$refComputed;        return 
scaled count {count}
}}复制代码

实际上你还可在模块里定义computed,这样计算出来的值是这个module下的所有组件都可以获取到的了,不过在render里是通过this.$$moduleComputed取到。

cc.startup(){    //...    computed:{        counter:{//为counter模块的count定义计算函数            count(count){                return count*100;            }        }    }}复制代码

注意,计算函数只有在对应的key值发生变化时才重新触发计算,否则值是一直被缓存住的。

一切从state获取是不是违背原则

读者可能已经注意到了,在cc里,store的数据都是注入在state里了,实际上cc实例的state由cc通过register时标记的modulesharedStateKeysglobalStateKeys的值合成出来的,所有cc组件都天生的能够观察cc内置模块$$global的状态变化,所有cc组件如果不设定module都会默认为属于cc的内置模块$$default,如下图所示,告诉你cc实例state怎么产生的

假设我们的
conter模块和
$$global模块的
state现在如下

cc.startup({    isModuleModel:true,    store:{        counter:{            count:8,        }        $$global:{            info:'i am global'        }    }})复制代码

我们新建一个Bar

//等同于写cc.register('Bar',{m:'counter', sharedStateKeys:'*', globalStateKeys:'*'})@cc.r('Bar',{m:'counter', s:'*', g:'*'})class Foo extends Component{    render(){       console.log(this.state);// {count:8, info:'i am global'}    }}复制代码

注意我们没有书写constructorcc为我们合成出了state,让我们稍作修改

@cc.r('Bar',{m:'counter', s:'*', g:'*'})class Bar extends Component{    constructor(props, context){        super(props, context);        this.state = {myPrivateKey:'666'}    }    render(){       console.log(this.state);       // {count:8, info:'i am global', myPrivateKey:'666'}    }}复制代码

如果我们在constructorcount赋值100,打印出来的state里的count还是8,因为这个值被ccstore恢复回来了,你写的值被覆盖了,这一点要注意

console.log(this.state); // {count:8, info:'i am global', myPrivateKey:'666'}复制代码

除非你注册时,没有申明任何共享的sharedStateKeys,尽管这个cc class属于counter,但是将不会收counter模块里任何key变化的影响哦

@cc.r('Bar',{m:'counter'})复制代码

说到这里,依然还是正面回答标题里提出的疑问:一切从state获取是不是违背原则。因为我们从一开始就被告知,state是自己管理管理的状态,props上派发下来的状态才是需要共享的状态,我们仔细思考一下,在cc里你只要定义的keystorekey不重复,就不发生共享关系,或者你register时刻意设定某些想关心的key,也可以让你的key成为私有的state

counter store: {key:1,key2:2, key3:3}@cc.r('Bar',{m:'counter',s:['key1','key2']})class Bar extends Component{    constructor(props, context){        this.state = {key3:888888};    }    render(){        console.log(this.state);        //{key:1,key2:2, key3:888888}    }}复制代码

打印结果会看到{key:1,key2:2, key3:888888}

尽管counter模块里有key1 key2 key3,但是你注册时只共享了key1,key2,所以key3还是你私有的state ,如果你调用setState({key1:666,key2:888,key3:999})时,
{key1:666,key2:888,key3:999}会赋值给自己,然后cc提取出{key1:666,key2:888}广播出去。

不想用state来承载store的数据可以吗

如果你不喜欢用state来获取store的数据,只想干干净净的用state来做自己组件的状态管理,cc同样提供$$propState来获取store上的数据,上图里用户看到最后一步有一个broadcastPropState,完成此项工作。

我们重写Bar

//@cc.register('Bar',{stateToPropMapping:{
'counter/count':'count'}})@cc.r('Bar',{pm:{
'counter/count':'count'}})class Bar extends Component{ constructor(props, context){ this.state = {key3:888888}; } render(){ console.log(this.$$propState); //{count:8} }}复制代码

stateToPropMapping复杂完成把模块上的某些key映射到$$propStatekey,大家可能留意到,stateToPropMappingkey是带模块名的,值作为$$propStatekey可以被重命名,是因为这样做cc class可以观察任意多个模块的任意key的变化了

// 假设我们的counter模块里还有其他key如 info:'x',// 还有另外一个模块chart : {count:19, list:[]} const pm = {    'counter/count':'count',    'counter/info':'info',      'chart/count':'chart_count'}@cc.r('Bar',{pm}) console.log(this.$$propState); // {count:8, info:'x', chart_count:19}复制代码

当你在别的地方修改chartcount值的为10000时候,Barrender会被触发渲染,你会看到chart_count变为10000

console.log(this.$$propState); // {count:8, info:'x', chart_count:10000}复制代码

如果你讨厌会所有key起别名,但是又担心命名冲突,可以写为:

// 假设我们的counter模块里还有其他key如 info:'x',// 还有另外一个模块chart : {count:19, list:[]} const pm = {    'counter/*':'',     'chart/*':''}@cc.r('Bar',{pm, isPropStateModuleMode:true})// 也可以直接写为@cc.connect('Bar', pm) console.log(this.$$propState); // {counter:{count:8, info:'x'}, chart:{chart_count:19}}复制代码

当然这里要注意,这样写你其实关心这两个模块所有key变化了,根据实际场景来做判断需不需要标记*,实际上register是可以一起写sharedStateKeysstateToPropMapping的,这样的话组件即从this.state拿到store罪行的数据,也能从this.$$propState上拿到store最新的数据

关于无状态组件怎么复用cc里现有的业务逻辑?CcFragment给你答案

19年facebookreact赋能hooks后,都觉得以后慢慢的不需要class组件了,直接使用function组件能搞定一切?各种useStateuseEffectuseContext已经被标准化,看起来function组件能够慢慢替代class组件了,可是我们仔细想想,我们期望状态集中管理,状态变化可以被精确追踪,hooks必然还需要一段很长的路走,我们看看cc给出对无状态组件怎么复用reducer给出的答案

import {CcFragment} from ''const MyPanel = ()=>{    return (        
{ ({propState, setState})=>(
setState('counter', {count:200})}>{propState.counter.count}
) }
{ ({propState, dispatch})=>(
dispatch('foo/changeName', 'newName')}>{propState.foo.name}
) }
);}复制代码

让我们调戏UI

现在你可以打开console,输入cc回车,你会发现cc已经将api绑定到了window.cc下了,你可以输入cc.setState(moduleName, newPartialState)直接触发渲染,当然前提是有相关的UI已经挂载到界面上,要不然只是改变了store,视图并没有说明变化,除此之外,其他的cc.emitcc.dispatch等使用方法和你在cc class是一样的使用体验,让你可以快速验证一些你的渲染逻辑哦。

输入sss回车,可以查看cc最新的整个状态树。


结语

综上所诉,cc挑战前辈的资本,在于只是提供了最基础的api,却可以让你用更轻松的方式分离你的业务逻辑和视图渲染逻辑,以及更优雅的方式复用你的函数,因为对于对于cc来说,它们更像是一个个newPartialStateCreator,厌倦了redux的你,能不能在cc里找到你想要的答案呢?

转载于:https://juejin.im/post/5c8479316fb9a049ba42635c

你可能感兴趣的文章
说说SDN和云平台对接
查看>>
物联网给中国智造插上翅膀
查看>>
51Testing专访史亮:测试人员在国外
查看>>
“黑科技”安防界遍地开花 公安实战如何应用?
查看>>
《C++编程规范:101条规则、准则与最佳实践》——2.9 确保资源为对象所拥有。使用显式的RAII和智能指针...
查看>>
《Web异步与实时交互——iframe AJAX WebSocket开发实战》—— 2.1 简介
查看>>
《SOA达人迷》目录—导读
查看>>
Apache Kylin权威指南1.5 Apache Kylin的主要特点
查看>>
Java IO: 其他字节流(上)
查看>>
Java中的锁
查看>>
节省60%费用!巧用阿里云归档存储降低基因测序成本
查看>>
《Adobe Dreamweaver CS6中文版经典教程》——1.7 创建自定义的快捷键
查看>>
linux学习笔记三: secureCRT小键盘输入数字键的时候,出现字母的解决方法:
查看>>
beego打印请求http内容
查看>>
手机自动化测试:Appium源码分析之跟踪代码分析二
查看>>
老李推荐:第8章7节《MonkeyRunner源码剖析》MonkeyRunner启动运行过程-小结
查看>>
Java语言概述
查看>>
支持 web sftp的Jumpserver 1.4.2 发布
查看>>
企业环境下MySQL5.5调优
查看>>
【阿里云MVP Meetup 第四期】产业中的“图像识别”分享与探索,干货来袭!
查看>>