[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 />,讓你的能夠快速的在已有的項目裏使用起來,

具體代碼點此處

線上演示點此處,注:線上演示代碼不完整,最完整的運行此項目

相關文章
相關標籤/搜索