目录回顾
react 用户最初接触接触 react 时,一定被洗脑了无数次下面几句话
它们体现着 react 的精髓,最初的时候,我们接触的最原始的也是最多的触发 react 视图渲染就是
setState
,这个函数打开了通往 react 世界的大门,因为有了setState
,我们能够赋予组件生命,让它们按照我们开发者的意图动起来了。
渐渐的我们发现,当我们的单页面应用组件越来越多的时候,它们各自的状态形成了一个个孤岛,无法相互之间优雅的完成合作,我们越来越需要一个集中式的状态管理方案,于是 facebook 提出了 flux 方案,解决庞大的组件群之间状态不统一、通信复杂的问题
仅接着社区优秀的 flux 实现涌现出来,最终沉淀下来形成了庞大用户群的有redux
,mbox
等,本文不再这里比较 cc 与它们之间的具体差异,因为cc
其实也是基于 flux 实现的方案,但是cc
最大的特点是直接接管了setState
,以此为根基实现整个react-control-center
的核心逻辑,所以cc
是对react
入侵最小且改写现有代码逻辑最灵活的方案,整个cc
内核的简要实现如下
可以看到上图里除了setState
,还有dispatch
、effect
,以及 3 个点,因为 cc 触发有很多种,这里只提及setState
、dispatch
和effect
这 3 种能覆盖用户 99%场景的方法,期待读完本文的你,能够爱上cc
。
以下是一个大家见到的最最普通的有状态组件,视图里包含了一个名字显示和 input 框输入,让用户输入新的名字
class Hello extends React.Component {
constructor(props) {
super(props);
this.state = { name:'' };
}
changeName = (e)=>{
this.setState({name:e.currentTarget.value});
}
render() {
const {name} = this.state;
return (
<div className="hello-box">
<div>{this.props.title}</div>
<input value={name} onChange={this.changeName} />hello cc, I am {name}
</div>
)
}
}
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="app-box">
<Hello title="normal instance"/>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('app'));
事实上声明一个 cc 组件非常容易,将你的 react 组件注册到 cc,其他就交给 cc 吧,这里我们先在程序的第一行启动 cc,声明一个store
cc.startup({
store:{name:'zzk'}
});
使用cc.register
注册Hello
为 CC 类
const CCHello = cc.register('Hello',{sharedStateKeys:'*'})(Hello);
然后让我们渲染出CCHello 吧
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="app-box">
<Hello title="normal instance"/>
<CCHello title="cc instance1"/>
<CCHello title="cc instance2"/>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('app'));
上面动态图中我们可以看到几点<CCHello />
与<Hello />
表现不一样的地方
- 初次添加一个
<CCHello />
的时候,input 框里直接出现了 zzk 字符串- 添加了 3 个
<CCHello />
后,对其中输入名字后,另外两个也同步渲染了
为什么 CC 组件会如此表现呢,接下来我们聊聊register
register
,普通组件通往 cc 世界的桥梁我们先看看 register 函数签名解释,因为 register 函数式如此重要,所以我尽可能的解释清楚每一个参数的意义,但是如果你暂时不想了解细节,可以直接略过这段解释,不妨碍你阅读后面的内容哦^_^,了解跟多关于 register 函数的解释
/****
* @param {string} ccClassKey cc 类的名称,你可以使用多个 cc 类名注册同一个 react 类,但是不能用同一个 cc 类名注册多个 react 类
* ' - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -'
* @param {object} registerOption 注册的可选参数
* ' - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -'
* @param {string} [registerOption.module] 声明当前 cc 类属于哪个模块,默认是`$$default`模块
* ' - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -'
* @param {Array<string>|string} [registerOption.sharedStateKeys]
* 定义当前 cc 类共享所属模块的哪些 key 值,默认空数组,写为`*`表示观察并共享所属模块的所有 key 值变化
* ' - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -'
* @param {Array<string>|string} [registerOption.globalStateKeys]
* 定义当前 cc 类共享 globa 模块的哪些 key 值,默认空数组,写为`*`表示观察并共享 globa 模块的所有 key 值变化
* ============ !!!!!! ============
* 注意 key 命名重复问题,因为一个 cc 实例的 state 是由 global state、模块 state、自身 state 合成而来,
* 所以 cc 不允许 sharedStateKeys 和 globalStateKeys 有重复的元素
* ' - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -'
* @param {object} [registerOption.stateToPropMapping] { (moduleName/keyName)/(alias), ...}
* 定义将模块的 state 绑定到 cc 实例的$$propState 上,默认'{}'
* ' - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -'
* @param {object} [registerOption.isPropStateModuleMode]
* 默认是 false,表示 stateToPropMapping 导出的 state 在$$propState 是否需要模块化展示
* ' - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -'
* @param {string} [registerOption.reducerModule]
* 定义当前 cc 类的 reducer 模块,默认和'registerOption.module'相等
* ' - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -'
* @param {string} [registerOption.extendInputClass]
* 是否直接继承传入的 react 类,默认是 true,cc 默认使用反向继承的策略来包裹你传入的 react 类,这以为你在 cc 实例可以通过'this.'直接呼叫任意 cc 实例方法,如果可以设置'registerOption.extendInputClass'为 false,cc 将会使用属性代理策略来包裹你传入的 react 类,在这种策略下,所有的 cc 实例方法只能通过'this.props.'来获取。
* 跟多的细节可以参考 cc 化的 antd-pro 项目的此组件 https://github.com/fantasticsoul/rcc-antd-pro/blob/master/src/routes/Forms/BasicForm.js
* ' - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -'
* @param {string} [registerOption.isSingle] 该 cc 类是否只能实例化一次,默认是 false
* 如果你只允许当前 cc 类被实例化一次,这意味着至多只有一个该 cc 类的实例能存在
* 你可以设置'registerOption.isSingle'为 true,这有点类似 java 编码里的单例模式了^_^
* ' - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - -'
* @param {string} [registerOption.asyncLifecycleHook] 是否是 cc 类的生命周期函数异步化,默认是 false
* 我们可以在 cc 类里定义这些生命周期函数'$$beforeSetState'、'$$afterSetState'、'$$beforeBroadcastState',
* 他们默认是同步运行的,如果你设置'registerOption.isSingle'为 true,
* cc 将会提供给这些生命周期函数 next 句柄放在他们参数列表的第二位,
* * ============ !!!!!! ============
* 你必须调用 next,否则当前 cc 实例的渲染动作将会被永远阻塞,不会触发新的渲染
* ```
* $$beforeSetState(executeContext, next){
* //例如这里如果忘了写'next()'调用 next, 将会阻塞该 cc 实例的'reactSetState'和'broadcastState'等操作~_~
* }
* ```
*/
通过register
函数我们来解释上面遗留的两个现象的由来
- 初次添加一个
<CCHello />
的时候,input 框里直接出现了 zzk 字符串.因为我们注册
Hello
为CCHello
的时候,语句如下
const CCHello = cc.register('Hello',{sharedStateKeys:'*'})(Hello);
没有声明任何模块,所以CCHello
属于$$default
模块,定义了sharedStateKeys
为*
,
表示观察和共享$$default
模块的整个状态,所以在starup
里定义的store
的name
就被同步到CCHello
了
- 添加了 3 个
<CCHello />
后,对其中输入名字后,另外两个也同步渲染了因为对其中一个
<CCHello />
输入名字时,
其他两个<CCHello/>
他们也属于'$$default'模块,也共享和观察name
的变化,
所以其实任意一个<CCHello />
的输入,cc 都会将状态广播到其他两个<CCHello />
前面文章我们介绍cc.startup
时说起推荐用户使用多模块话启动cc
,所以我们稍稍改造一下starup
启动参数,让我们的不仅仅只是使用 cc 的内置模块$$default
和$$global
。
定义两个新的模块foo
和bar
,可以把他们的 state 定义成一样的。
cc.startup({
isModuleMode:true,
store:{
$$default:{
name:'zzk of $$default',
info:'cc',
},
foo:{
name:'zzk of foo',
info:'cc',
},
bar:{
name:'zzk of bar',
info:'cc',
}
}
});
以Hello
类为输入新注册 2 个 cc 类HelloFoo
和HelloBar
,然后渲染他们看看效果吧
const CCHello = cc.register('Hello',{sharedStateKeys:'*'})(Hello);
const HelloFoo = cc.register('HelloFoo',{module:'foo',sharedStateKeys:'*'})(Hello);
const HelloBar= cc.register('HelloBar',{module:'bar',sharedStateKeys:'*'})(Hello);
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="app-box">
<Hello title="normal instance"/>
<CCHello title="cc instance1 of module $$default"/>
<CCHello title="cc instance1 of module $$default"/>
<br />
<HelloFoo title="cc instance3 of module foo"/>
<HelloFoo title="cc instance3 of module foo"/>
<br />
<HelloBar title="cc instance3 of module bar"/>
<HelloBar title="cc instance3 of module bar"/>
</div>
)
}
}
以上我们演示了用同一个 react 类注册为观察着不同模块 state 的 cc 类,可以发现尽管视图是一样的,但是他们的状态在模块化的模式下被相互隔离开了,这也是为什么推荐用模块化方式启动 cc,因为业务的划分远远不是两个内置模块就能表达的
上面我们演示了用同一个 react 类注册到不同的模块,下面我们写另一个 react 类Wow
来观察$$default
模块
class Wow extends React.Component {
constructor(props) {
super(props);
this.state = { name:'' };
}
render() {
const {name} = this.state;
return (
<div className="wow-box">
wow {name} <input value={name} onChange={(e)=>this.setState({name:e.currentTarget.value})} />
</div>
)
}
}
我们知道,视图渲染代码和业务代码混在一起,对于代码的重构或者维护是多么的不友好,所以尽管 cc 提供setState
来改变状态,但是我们依然推荐dispatch
方式来使用 cc,让业务逻辑和视图渲染逻辑彻底分离
我们在启动 cc 时,为 foo 模块定义一个和 foo 同名的 reducer 配置在启动参数里
reducer:{
foo:{
changeName({payload:name}){
return {name};
}
}
}
现在让我们修改Hello
类用dispatch
去修改 state 吧,可以声明派发 foo 模块的 reducer 去生成新的 state 并修改 foo,当 state 模块和 reducer 模块重名时,可以用简写方式
changeName = (e)=>{
const name = e.currentTarget.value;
//this.setState({name});
this.$$dispatch('foo/changeName', payload:name);
//等价与 this.$$dispatch('foo/foo/changeName', payload:name);
//等价于 this.$$dispatch({ module: 'foo', reducerModule:'foo',type: 'changeName', payload: name });
}
上面贴图中,我们看到当我们修改<HelloFoo/>
实例里的 input 的框的时候,<HelloFoo/>
如我们预期那样发生了变化,但是我们在<HelloBar/>
或者<CCHello/>
里输入字符串时,他们没有变化,却触发了<HelloFoo/>
发生,这是为什么呢?
我们回过头来看看Hello
类里的this.$$dispatch
函数,指定了状态模块是foo
,所以这里就出问题了
让我们去掉this.$$dispatch
里的状态模块,修改为总是用foo
这个 reducerModule 模块的函数去生成新的 state,但是不指明具体的目标状态模块,这样 cc 实例在发起$$this.dispatch
调用时就会默认去修改当 cc 类所属的状态模块
changeName = (e)=>{
const name = e.currentTarget.value;
//this.setState({name});
//不指定 module,只指定 reducerModule,cc 实例调用时会去修改自己默认的所属状态模块的状态
this.$$dispatch({reducerModule:'foo',type: 'changeName', payload: name });
}
上图的演示效果正如我们的预期效果,三个注册到不同的模块的 cc 组件使用了同一个 recuder 模块的方法去更新状态。 让我们这里总结下 cc 查找 reducer 模块的规律
- 不指定 state 模块和 reducer 模块时,cc 发起
$$dispatch
调用的默认寻找的目标 state 模块和目标 reducer 模块就是当前 cc 类所属的目标 state 模块和目标 reducer 模块- 只指定 state 模块不指定 reducer 模块时,默认寻找的目标 state 模块和目标 reducer 模块都是指定的 state 模块
- 不指定 state 模块,只指定 reducer 模块时,默认寻找的目标 state 模块是当前 cc 类所属的目标 state 模块,寻找的 reducer 模块就是指定的 reducer 模块
- 两者都指定的时候,cc 严格按照用户的指定值去查询 reducer 函数和修改指定目标的 state 模块
cc 这里灵活的把 recuder 模块这个概念也抽象出来,为了方便用户按照自己的习惯归类各个修改状态函数。
大多数时候,用户习惯把 state module 的命名和 reducer module 的命名保持一致,但是 cc 允许你定义一些额外的 recuder module,这样具体的 reducer 函数归类方式就很灵活了,用户可按照自己的理解去做归类
我们知道,react 更新状态时,一定会有副作用产生,这里我们加一个需求,更新 foo 模块的 name 时,通知 bar 模块也更新 name 字段,同时上传一个 name 到后端,拿后端返回的结果更新到$$default
模块的 name 字段里,让我们小小改造一下 changeName 函数
async function mockUploadNameToBackend(name) {
return 'name uploaded'
}
changeName: async function ({ module, dispatch, payload: name }) {
if (module === 'foo') {
await dispatch('bar/foo/changeName', name);
const result = await mockUploadNameToBackend(name);
await dispatch('$$default/foo/changeName', result);
return { name };
} else {
return { name };
}
}
cc 支持 reducer 函数可以是 async 或者 generator 函数,其实 reducer 函数的参数 excutionContext 可以解构出module
、effect
、xeffect
、state
、moduleState
、globalState
、dispatch
等参数,
我们在 reducer 函数发起了其他的副作用调用
cc 并不强制要求所有的 reducer 函数返回一个新的 state,所以我们可以利用 dispatch 发起调用组合其他的 dispatch
基于上面的需求,我们再给自己来下一个这样的需求,当 foo 模块的实例输入的是666
的时候,把``foo、
bar的所有实例的那么重置为
恭喜你中奖 500 万了,我们保留原来的 changeName,新增一个函数
changeNameWithAward和
awardYou,然后组件里调用
changeNameWithAward`
awardYou: function ({dispatch}) {
const award = '恭喜你中奖 500 万';
Promise.all(
[
dispatch('foo/changeName', award),
dispatch('bar/foo/changeName', award)
]
);
},
changeNameWithAward: async function ({ module, dispatch, payload: name }) {
console.log('changeNameWithAward', module, name);
if (module === 'foo' && name === '666') {
dispatch('foo/awardYou');
} else {
console.log('changeName');
dispatch(`${module}/foo/changeName`, name);
}
}
我们可以看到awardYou
里并没有返回新的 state,而是并行调用 changeName。
cc 基于这样的组合 dispatch 理念可以让你跟灵活的组织代码和重用已有的 reducer 函数
dispatch
和reducer
组合拳?试试effect
effect
其实和dispatch
是一样的作用,生成新的 state,只不过不需要指定 reducerModule 和 type 让 cc 从 reducer 定义里找到对应的函数执行逻辑,而是直接把函数交给 effect 去执行
让我们在Hello
组件里稍稍改造一下,当 name 为 888 的时候,不调用$$dispatch
而是调用$$effect
function myChangeName(name, prefix) {
return { name: `${prefix}${name}` };
}
changeName = (e) => {
const name = e.currentTarget.value;
// this.setState({name});
// this.$$dispatch('foo/changeName', name);
if(name==='888'){
const currentModule = this.cc.ccState.module;
//add prefix 888
this.$$effect(currentModule, myChangeName, name, '8');
}else{
this.$$dispatch({reducerModule:'foo',type: 'changeNameWithAward', payload: name });
}
}
effect 必须指定具体的模块,如果想自动默认使用当前实例的所属模块可以写为
this.$invoke(myChangeName, name, '8');
上面我们演示 recuder 函数时有提到 executionContext 里可以解构出effect
,所以用户可以在 reducher 函数里一样的使用 effect
awardYou:function ({dispatch, effect}) {
const award = '恭喜你中奖 500 万';
await Promise.all([
dispatch('foo/changeName', award),
dispatch('bar/foo/changeName', award)
]);
await effect('bar',function(info){
return {info}
},'wow cool');
}
想用在 effect 内部使用dispatch
,需要使用 cc 提供的xeffect
函数,默认把用户自定义函数的第一位参数占用了,传递 executionContext 给第一位参数
async function myChangeName({dispatch, effect}, name, prefix) {
//call effect or dispatch as you expected
return { name: `${prefix}${name}` };
}
changeName = (e) => {
const name = e.currentTarget.value;
this.$$xeffect(currentModule, myChangeName, name, '8');
}
该参数大多时候用户都不需要用到,cc 可以为setState
、$$dispatch
、effect
都可以设置延迟时间,单位是毫秒,侧面印证 cc 是的状态过程存在,这里我们设置当输入是222
时,3 秒延迟广播状态, (备注,不设定时,cc 默认是-1,表示不延迟广播)
this.setState({name});
---> 可以修改为如下代码,备注,第二位参数是 react.setState 的 callback,cc 做了保留
this.setState({name}, null, 3000);
this.$$effect(currentModule, myChangeName, name, 'eee');
---> 可以修改为如下代码,备注,$$xeffect 对应的延迟函数式$$lazyXeffect
this.$$lazyEffect(currentModule, myChangeName, 3000, name, 'eee');
this.$$dispatch({ reducerModule: 'foo', type: 'changeNameWithAward', payload: name });
---> 可以修改为如下代码,备注,$$xeffect 对应的延迟函数式$$lazyXeffect
this.$$dispatch({ lazyMs:3000, reducerModule: 'foo', type: 'changeNameWithAward', payload: name });
cc 允许用户对 cc 类实例定义$$on
、$$onIdentity
,以及调用$$emit
、$$emitIdentity
、$$off
我们继续对上面的需求做扩展,当用户输入999
时,发射一个普通事件999
,输入9999
时,发射一个认证事件名字为9999
证书为9999
,我们继续改造Hello
类,在 componentDidMount 里开始监听
componentDidMount(){
this.$$on('999',(from, wording)=>{
console.log(`%c${from}, ${wording}`,'color:red;border:1px solid red' );
});
if(this.props.ccKey=='9999'){
this.$$onIdentity('9999','9999',(from, wording)=>{
console.log(`%conIdentity triggered,${from}, ${wording}`,'color:red;border:1px solid red' );
});
}
}
changeName = (e) => {
// ......
if(name === '999'){
this.$$emit('999', this.cc.ccState.ccUniqueKey, 'hello');
}else if(name === '9999'){
this.$$emitIdentity('9999', '9999', this.cc.ccState.ccUniqueKey, 'hello');
}
}
注意哦,你不需要在 computeWillUnmount 里去$$off 事件,这些 cc 都已经替你去做了,当一个 cc 实例销毁时,cc 会取消掉它的监听函数,并删除对它的引用,防止内存泄露
我们可以对 cc 类定义$$computed 方法,对某个 key 或者多个 key 的值定义 computed 函数,只有当这些 key 的值发生变化时,cc 会触发计算这些 key 对应的 computed 函数,并将其缓存起来
我们在 cc 类定义的 computed 描述对象计算出的值,可以从this.$$refComputed
里取出计算结果,而我们在启动时为模块的 state 定义的 computed 描述对象计算出的值,可以从this.$$moduleComputed
里取出计算结果,特别地,如果我们为$$global
模块定义了 computed 描述对象,可以从this.$$globalComputed
里取出计算结果
现在我们为类定义 computed 方法,将输入的值反转,代码如下
$$computed() {
return {
name(name) {
return name.split('').reverse().join('');
}
}
}
cc 默认采用的是反向继承的方式包裹你的 react 类,所以在 reactDom 树看到的组件非常干净,不会有多级包裹
现在,你可以打开 console,输入cc.
,可以直接呼叫dispatch
、emit
、setState
等函数,让你快速验证你的渲染逻辑,输入 sss,查看整个 cc 的状态树结构
好了,基本上 cc 驱动视图渲染的 3 个基本函数介绍就到这里了,cc 只是提供了最最基础驱动视图渲染的方式,并不强制用户使用哪一种,用户可以根据自己的实际情况摸索出最佳实践
因为 cc 接管了 setState,所以 cc 可以不需要包裹<Provider />
,让你的可以快速的在已有的项目里使用起来,