[react-control-center tutorial 3] 数据驱动视图的灵魂setState

目录回顾vue


前言

最初的react

react用户最初接触接触react时,必定被洗脑了无数次下面几句话java

  • 数据驱动视图
  • 单向数据流
  • 组件化

它们体现着react的精髓,最初的时候,咱们接触的最原始的也是最多的触发react视图渲染就是setState,这个函数打开了通往react世界的大门,由于有了setState,咱们可以赋予组件生命,让它们按照咱们开发者的意图动起来了。
渐渐的咱们发现,当咱们的单页面应用组件愈来愈多的时候,它们各自的状态造成了一个个孤岛,没法相互之间优雅的完成合做,咱们愈来愈须要一个集中式的状态管理方案,因而facebook提出了flux方案,解决庞大的组件群之间状态不统1、通讯复杂的问题react

状态管理来了

仅接着社区优秀的flux实现涌现出来,最终沉淀下来造成了庞大用户群的有reduxmbox等,本文再也不这里比较cc与它们之间的具体差别,由于cc其实也是基于flux实现的方案,可是cc最大的特色是直接接管了setState,以此为根基实现整个react-control-center的核心逻辑,因此cc是对react入侵最小且改写现有代码逻辑最灵活的方案,整个cc内核的简要实现以下git

能够看到上图里除了 setState,还有 dispatcheffect,以及3个点,由于cc触发有不少种,这里只说起 setStatedispatcheffect这3种能覆盖用户99%场景的方法,期待读完本文的你,可以爱上 cc


setState,在线示例代码 在线示例代码2

一个普通的react组件诞生了,

如下是一个你们见到的最最普通的有状态组件,视图里包含了一个名字显示和input框输入,让用户输入新的名字github

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组件

事实上声明一个cc组件很是容易,将你的react组件注册到cc,其余就交给cc吧,这里咱们先在程序的第一行启动cc,声明一个storeredux

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
上面动态图中咱们能够看到几点 <CCHello /><Hello />表现不同的地方

  • 初次添加一个<CCHello />的时候,input框里直接出现了zzk字符串
  • 添加了3个<CCHello />后,对其中输入名字后,另外两个也同步渲染了

为何CC组件会如此表现呢,接下来咱们聊聊register缓存

register,普通组件通往cc世界的桥梁

咱们先看看register函数签名解释,由于register函数式如此重要,因此我尽量的解释清楚每个参数的意义,可是若是你暂时不想了解细节,能够直接略过这段解释,不妨碍你阅读后面的内容哦^_^,了解跟多关于register函数的解释bash

/****
 * @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字符串.

由于咱们注册HelloCCHello的时候,语句以下
const CCHello = cc.register('Hello',{sharedStateKeys:'*'})(Hello);
没有声明任何模块,因此CCHello属于$$default模块,定义了sharedStateKeys*
表示观察和共享$$default模块的整个状态,因此在starup里定义的storename就被同步到CCHello

  • 添加了3个<CCHello />后,对其中输入名字后,另外两个也同步渲染了

由于对其中一个<CCHello />输入名字时,
其余两个<CCHello/>他们也属于'$$default'模块,也共享和观察name的变化,
因此其实任意一个<CCHello />的输入,cc都会将状态广播到其余两个<CCHello />

多模块话组织状态树

前面文章咱们介绍cc.startup时提及推荐用户使用多模块话启动cc,因此咱们稍稍改造一下starup启动参数,让咱们的不只仅只是使用cc的内置模块$$default$$global。 定义两个新的模块foobar,能够把他们的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类HelloFooHelloBar,而后渲染他们看看效果吧

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>
    )
  }
}
复制代码

多个模块的Hello
以上咱们演示了用同一个react类注册为观察着不一样模块state的cc类,能够发现尽管视图是同样的,可是他们的状态在模块化的模式下被相互隔离开了,这也是为何推荐用模块化方式启动cc,由于业务的划分远远不是两个内置模块就能表达的

让一个模块被被另外的react类注册

上面咱们演示了用同一个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>
    )
  }
}
复制代码

Wow来了


dispatch,更灵活的setState

在线示例代码

让业务逻辑和视图渲染逻辑完全分离

咱们知道,视图渲染代码和业务代码混在一块儿,对于代码的重构或者维护是多么的不友好,因此尽管cc提供setState来改变状态,可是咱们依然推荐dispatch方式来使用cc,让业务逻辑和视图渲染逻辑完全分离

定义reducer

咱们在启动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 });
  }
复制代码

Wow来了

对模块精确划分

上面贴图中,咱们看到当咱们修改<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 });
  }
复制代码

Wow来了
上图的演示效果正如咱们的预期效果,三个注册到不一样的模块的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函数归类方式就很灵活了,用户可按照本身的理解去作归类

dispatch,发起反作用调用

咱们知道,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 };
      }
    }
复制代码

dispatch
cc支持reducer函数能够是async或者generator函数,其实reducer函数的参数excutionContext能够解构出 moduleeffectxeffectstatemoduleStateglobalStatedispatch等参数, 咱们在reducer函数发起了其余的反作用调用

dispatch内部,组合其余dispatch

cc并不强制要求全部的reducer函数返回一个新的state,因此咱们能够利用dispatch发起调用组合其余的dispatch
基于上面的需求,咱们再给本身来下一个这样的需求,当foo模块的实例输入的是666的时候,把``foobar的全部实例的那么重置为恭喜你中奖500万了,咱们保留原来的changeName,新增一个函数changeNameWithAwardawardYou,而后组件里调用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);
      }
    }
复制代码

dispatch2
咱们能够看到 awardYou里并无返回新的state,而是并行调用changeName。 cc基于这样的组合dispatch理念可让你跟灵活的组织代码和重用已有的reducer函数

effect,最灵活的setState

不想用dispatchreducer组合拳?试试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 });  
    }
  }
复制代码

dispatch2
effect必须指定具体的模块,若是想自动默认使用当前实例的所属模块能够写为

this.$invoke(myChangeName, name, '8');
复制代码

dispatch使用effect?一样能够

上面咱们演示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呢?一样能够

想用在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$$dispatcheffect均可以设置延迟时间,单位是毫秒,侧面印证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 });
复制代码

dispatch2


类vue

关于emit

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会取消掉它的监听函数,并删除对它的引用,防止内存泄露

emit

关于computed

咱们能够对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('');
    }
  }
}
复制代码

computed

关于ccDom

cc默认采用的是反向继承的方式包裹你的react类,因此在reactDom树看到的组件很是干净,不会有多级包裹

ccdom

关于顶层函数和store

如今,你能够打开console,输入cc.,能够直接呼叫dispatchemitsetState等函数,让你快速验证你的渲染逻辑,输入sss,查看整个cc的状态树结构


结语

好了,基本上cc驱动视图渲染的3个基本函数介绍就到这里了,cc只是提供了最最基础驱动视图渲染的方式,并不强制用户使用哪种,用户能够根据本身的实际状况摸索出最佳实践
由于cc接管了setState,因此cc能够不须要包裹<Provider />,让你的能够快速的在已有的项目里使用起来,

具体代码点此处

线上演示点此处,注:线上演示代码不完整,最完整的运行此项目

相关文章
相关标签/搜索