react-control-center,再一次顛覆你對狀態管理的認識

C_C welcom to cc world

quick-start demo: github.com/fantasticso…

簡介

  • 硝煙四起

衆所周知,react自己只是很是優雅的解決了視圖層渲染工做,可是隨着應用愈來愈大,龐大的react組件羣體之間狀態相互其實並非孤立的,須要一個方案管理把這些狀態集中管理起來,從而將model和view的邊界劃分得更加清楚,針對於此,facebook官方對於react狀態管理給了一個flux架構並有一套本身的實現,可是社區裏並不知足於此,基於對flux的理解各個第三方作着給出了的本身的解決方案,狀態管理框架的戰爭今後拉開序幕,隨着redux橫空出世,你們默默接受了redux的 dispatch action、hit reducer、comibine new state、render new view的理念,在redux世界裏,組件須要關心的狀態變化都由props注入進來,connect做爲中間的橋樑將react組件與redux關聯起來,經過mapStateToProps將redux裏定義好的state映射到組件的props上,以此達到讓react組件訂閱它須要關心的state變化的目的。vue

  • 一統天下

隨着redux生態逐漸完善,你們默認的把redux當作了react狀態管理的首選解決方案,因此redux已經在react狀態管理框架裏一統天下,經過github star發現,另外一個流行的狀態管理框架mobx頁已經鎖定第二的位置,用另外一種思路來給你們展現原來狀態能夠這麼管理,狀態管理的格局彷佛基本已經洗牌完成,但是做爲redux重度使用者的我並不知足與此,以爲在redux世界裏,經過props層層穿透數據,經過provider包裹整個react app應用,只是解決了狀態的流動問題,而組件的通訊任然很是間接與尷尬,依靠redux來完成不是不能夠,只是對比vue,任然以爲少了些什麼......react

  • why cc

react-control-center並非簡單的立足於狀態管理,而是想爲你提供更多的有趣玩法,由於現有的狀態管理解決方案已經很是成熟(可是在某些場景未必真的好用),因此cc從一開始設計就讓其api對現有的組件入侵很是之小,你能夠在redux項目裏局部使用cc來把玩cc的狀態管理思路,能夠從一個組件開始,慢慢在開始漸進式的修改到其餘地方,僅僅使用一個register函數,將你的react類註冊爲cc類,那麼從cc類生成的cc實例,將給你帶來如下新的特性、新的概念、新的思路。git

1 全部cc實例都擁有 emiton 的能力,不管組件間嵌套關係多複雜,實現組件間通訊將會是如此輕鬆。github

實際上實例還擁有更精準的emitIdentity, emitWith, onIdentity方法,讓用戶基於更精準的力度去發射或者接收。
emitIdentity(eventName:string, identity:string, ...args), 第一位參數是事件名,第二位參數是認證串,剩餘參數是on的handler函數的實際接收參數,當不少相同組件(如以CcClass:BookItem生成了多個CcInstance)訂閱了同一個事件,可是你只但願通知其中一個觸發handler調用時,emitIdentity就能派上用場了。
onIdentity(eventName:string, identity:string, hancler:function),監聽emitIdentity發射的事件。
emitWith(eventName:string, option?:{module?:string, ccClassKey?:string, identity?:string})從一個更精準的角度來發射事件,尋找指定模塊下,指定cc類名的,指定identity的監控函數去觸發執行,具體過程這裏先略過,先看下面關於模塊和cc類的介紹,在回過頭來理解這裏更容易。
off(eventName:string, option?:{module?:string, ccClassKey?:string, identity?:string}),取消監聽。
這些函數在cc的頂層api裏都有暴露,當你的cc app運行起來以後,你能夠打開console,輸入cc並回車,你會發現這些函數已經所有綁定在window.cc對象下了,你能夠直接調用他們來完成快速驗證哦,而非經過ccInstance去觸發^_^json

import cc from 'react-control-center';
import React,{Component, Fragment} from 'react';

@cc.register('Foo')
class Foo extends Component{
    componentDidMount(){
        this.$$on('fooSignal',(signal, from)=>{
            this.setState({signal, from});
        });
        //cc是不容許一個cc實例裏對同一個事件名監聽屢次的,這裏fooSignal監聽了兩次,cc會默認使用最新的監聽函數,因此上面個監聽變成了無效的監聽
        this.$$on('fooSignal',(signal, from)=>{
            this.setState({signal, from:`--${from}--`});
        });
        this.$$on('fooSignalWithIdentity', 'xxx_id_wow',()=>{
            this.setState({signal, from});
        })
    }
}
@cc.register('Bar')
class Bar extends Component{
    render(){
        <div>
            <button onClick={()=>this.$$emit('fooSignal', 'hello', 'Bar')}>emit</button>
            <button onClick={()=>this.$$emit('fooSignal', 'xxx_id_wow', hello', 'Bar')}>emitIdentity</button> <button onClick={()=>this.$$off('fooSignal')}>off event fooSignal</button> </div> } } 複製代碼

2 全部cc實例均可以針對本身的state的任意key定義 computed 函數,cc會在key的值發生變化自動計算新的computed值並緩存起來,在實例裏定義的computed會收集到實例的refComputed對象裏。redux

import cc from 'react-control-center';
import React,{Component, Fragment} from 'react';

@cc.register('Foo')
class Foo extends Component{
    constructor(props, context){
        super(props, context);
        this.state = {woo:'woo cc!'};
    }
    $$computed(){
        return {
            wow(wow){
                return `computed wow ${wow}`;
            }
        }
    }
    componentDidMount(){
        this.$$on('fooSignal',(signal, from)=>{
            this.setState({signal, from});
        });
    }
    changeWow = (e)=>{
        this.setState({wow: e.currentTarget.value});
    }
    render(){
        return (
            <div>
                <span>{this.state.wow}</span>
                <span>{this.$$refComputed.wow}</span>
                <input value={this.state.wow} onChange={this.changeWow}/>
            </div>
        );
    }
}
複製代碼

3 註冊爲cc類的時候,爲該cc類設定了一個該cc類所屬的 模塊 ,並經過sharedStateKeys聲明關心該模塊裏哪些key(能夠是任意的key,也能夠是這個模塊的全部key)的變化,則由改cc類產生的cc實例共同監聽着這些key對應值的變化,任何一個cc實例改變了這些sharedStateKeys裏的值,其餘cc實例都能感知到它的變化並自動被cc觸發渲染。後端

import cc from 'react-control-center';
import React,{Component, Fragment} from 'react';

class Foo extends Component{
    render(){
        return <div>any jsx fragment here</div>
    }
}

//將Foo註冊爲一個共享FooOfM1模塊全部key變化的cc類FooOfM1
const FooOfM1 = cc.register('FooOfM1', {module:'M1', sharedStateKeys:'all'})(Foo);
//將Foo註冊爲一個共享FooOfM2模塊key1和key2變化的cc類FooOfM2
const FooOfM2 = cc.register('FooOfM2', {module:'M2',sharedStateKeys:['key1','key2']})(Foo);
//將Foo註冊爲一個共享FooOfM2模塊key1和key2變化,且共享global模塊g1變化的cc類FooOfM2G
const FooOfM2G = cc.register('FooOfM2', {module:'M2',sharedStateKeys:['key1','key2','key3'],globalStateKeys:['g1']})(Foo);
//不設定任何參數,只寫cc類名,cc會把Foo註冊爲一個屬於default模塊的cc類
const JustWantToOwnCcAbility = cc.register('JustWantToOwnCcAbility')(Foo);

//cc同時也爲register提供簡寫函數
//const FooOfM2G = cc.r('FooOfM2',{m:'M2',s:['key1','key2','key3'],g:['g1']})(Foo})
複製代碼

4 注意在3裏咱們提到一個概念 模塊,對於cc來講一個完整的模塊包括如下屬性:state、reducer、init、computed,這些參數都是調用cc.startup時注入,注意,cc雖然不須要用戶像redux那樣要給頂層App組件包裹一層<Provider/>可是要求用戶在app入口文件的第一句話那裏觸發cc.startup 讓整個cc運行起來,store、reducer、init、computed就是cc.startup須要的參數
api

  • store是一個object對象,store裏的各個key就表示模塊名,對應的值就是各個模塊對應的state,一個cc實例除了setState方法可以觸發修改state,還能夠經過dispatch方法派發action對象去修改state,此時具體的數據合成邏輯就體如今下面要說的reducer裏了
  • recuder是一個object對象,recuder裏的各個key表示reducer的模塊名,一般用戶能夠定義和state的模塊名保持一致,可是能夠定義另外的模塊名,因此這裏的模塊指的是reducerModuel,不強求用戶定義時和stateModule保持一致,stateModule對應的值一個普通的json對象,key爲函數名,值爲處理函數,即處理舊state併合成新state的方法,cc支持函數爲普通函數、生成器函數、async函數。

上面提到了dispatch函數須要傳遞一個action對象,一個標準的action必須包含type、payload 2個屬性,表示cc要去查recuder裏某個模塊下type映射函數去修改某個模塊的state,具體是什麼模塊的type映射函數和什麼模塊對應的state,參見action剩餘的兩個可缺省的屬性module和reducerModule的設定規則,注意,這裏再一次提到了reducerModule,如下規則就體現了爲何cc容許reducer模塊名能夠自由定義:
不指定module和reducerModule的話,cc去查reducer裏當前cc實例所屬模塊下的type映射函數去修改當前cc實例所屬模塊的state。
指定了module,而不指定reducerModule的話,cc去查reducer裏module下的type映射函數去修改module模塊的state。
不指定module,指定reducerModule的話,cc去查reducer裏reducerModule下type映射函數去修改當前觸發dispatch函數的cc實例所屬的module模塊的state。 指定了module,同時也指定了reducerModule的話,cc去查reducer裏reducerModule下type映射函數去修改module模塊的state。
之因此這樣設計是由於考慮到讓用戶能夠自由選擇reducer的模塊描述方式,由於對於cc來講,dispatch派發的action只是爲了準確找到reducer裏的處理函數,而reducer的模塊定義並不須要強制和state保持一致給了用戶更多的選擇去劃分reducer的領域緩存

  • init是一個object對象,key是模塊名,嚴格對應stateModule,值是一個函數,若是用戶爲某個模塊定義了init函數表示用戶但願有機會再次初始化某個模塊的state,一般是異步請求後端來的數據從新賦值給模塊對應的state
  • computed是一個object對象,key是模塊名,嚴格對應stateModule,值是一個moduleComputedObject,moduleComputedObject的key指的就是某個module的某個key,value就是爲這個key定義的計算函數,函數的第一爲參數就是key的原始值,cc實例裏經過moduleComputed對象取到計算後的新值,特別地,爲global模塊定義的moduleComputedObject對象,在cc實例裏經過globalComputed對象取到計算後的新值
//code in index.js
import api from '@foo/bar/api';

cc.startup({
    isModuleMode:true,//表示cc以模塊化方式啓動,默認是false,cc鼓勵用戶使用模塊化管理狀態,更容易劃分領域的邊界
    store:{
        $$global{//$$global是cc的內置模塊,用戶若是沒有顯式的定義,cc會自動注入一個,只不過是一個不包含任何key的對象
            themeColor:'pink',
        },
        m1:{
            name:'zzk',
            age:30,
            books:[],
            error:'',
        },
        m2:{
            wow:'wow',
            signal:'haha',
        }
    },
    reducer:{
        m1:{
            //state表示調用dispatch的cc實例對應的state,moduleState只描述的是cc實例所屬的模塊的state,更多的解釋看下面的4 5 6 7 8這些點。
            //特別的注意,若是該方法是由於某個reducer的函數裏調用的dispatch函數而被觸發調用的,此時的state始終指的是最初的那個在cc實例裏觸發dispatch時那個cc實例的state,而moduleState始終指向的是指定的module的的state!!!
            changeName:function({payload,state,moduleState,dispatch}){
                const newName = payload;
                dispatch({module:'m2',type:'changeSignal',payload:'wow!dispatch in reducer function block'});
                return {name:newName};
            },
            //支持生成器函數
            changeAge:function*({payload,state,moduleState,dispatch}){
                const newAge = payload;
                const result = yield api.verifyAge(newAge);
                if(result.error)return({error:result.error});
                else return {name:newName};
            },
            //支持async
            changeAge:async function({payload:{pageIndex,pageSize}}){
                const books = yield api.getBooks(pageIndex, pageSize);
                return {books};
            }
        },
        m2:{
            changeSignal:function({payload:signal,dispatch}){
                //注意m1/changeName裏指定了修改m2模塊的數據,其實這裏能夠一次性return {signal, wow:'just show reducerModule'}來修改數據,
                //可是故意的調用dispatch找whatever/generateWow來觸發修改m2的wow值,是爲了演示顯示的指定reducerModule的做用
                dispatch({module:'m2',reducerModule:'whatever',type:'generateWow',payload:'just show reducerModule'})
                return {signal};
            }
        },
        whatever:{//一個刻意和stateModule沒有保持一致的reducerModule
            generateWow:function({payload:wow}){
               return {wow};
            }
        },
        $$global:{//爲global模塊指定reducer函數
            changeThemeColor:function({payload:themeColor}){
                return {themeColor}
            }
        }
    },
    init:{
        $$global:setState=>{//爲global模塊指定state的初始化函數
            api.getThemeColor().then(themeColor=>{
                setState({themeColor})
            }).catch(err=>console.log(err))
        }
    },
    computed:{
        m1:{
            name(name){//reverse name
                return name.split('').reverse().join('');
            }
        }
    }
})

複製代碼

4 注意第3點裏,註冊一個react類到某個模塊裏成爲cc類時,sharedStateKeys能夠是這個模塊裏的任意key,由於cc容許註冊不一樣的react類到同一個模塊,例如模塊M裏擁有5個key爲f一、f二、f三、f四、f5, ccClass1經過sharedStateKeys觀察模塊M的f一、f2, ccClass2經過sharedStateKeys觀察模塊M的f二、f三、f4,當ccClass1的某個實例改變了f2的值,那麼ccClass1的其餘實例和ccClass2的全部實例都能感知到f2的變化並被cc觸發渲染。
5 cc有一個內建的global模塊,全部的ccClass都天生的擁有觀察global模塊key值變化的能力,註冊成爲cc類時經過globalStateKeys觀察模塊global裏的任意key。
6 全部cc實例上能夠經過prop ccOption設定storedStateKeys,表示該實例上的這些key是須要被cc存儲的,這樣在該cc實例銷燬而後再次掛載回來的時候,cc能夠把這些key的值恢復回來。
7 一個cc實例的state的key除了上面所提到的global、 shared、stored這三種類型,剩下的一種key就是默認的temporary類型了,這種key對應的值隨着組件銷燬就丟失了,再次掛載cc實例時會讀取state裏的默認值。
8 結合4 5 6 7來看,cc實例裏的state是由cc類上申明的sharedStateKeys、globalStateKeys,和cc實例裏ccOption申明的storedStateKeys對應的值,再加上剩下的默認的temporaryStateKeys對應的值合併得出來。
bash

9 和react實例同樣,觸發cc實例render方法,依然是經典的setState方法,以及上面提到的dispatch定位reducer方法去修改,除了這兩種cc還有更多自由的選擇,如invoke,effect,xeffect容許用戶直接調用本身定義的函數去修改state,同reducer函數同樣,能夠是普通函數、generator函數、async函數。
這樣的方式讓用戶有了更多的選擇去觸發修改state,cc並不強制用戶使用哪種方式,讓用戶本身摸索和組合更多的最佳實踐

invoke必定是修改當前cc實例的state,只須要傳入第一位參數爲具體的用戶自定義執行函數,剩餘的其餘參數都是執行函數須要的參數。
effect容許用戶修改其餘模塊的state,第一位參數是moduleName,第二位參數爲具體的用戶自定義執行函數,剩餘的其餘參數都是執行函數須要的參數。
xeffect和effect同樣,容許用戶修改其餘模塊的state,第一位參數是moduleName,第二位參數爲具體的用戶自定義執行函數,剩餘的其餘參數都是執行函數須要的參數,和effect不同的地方是xeffect調用的執行函數的參數列表,第一位是cc注入的ExecuteContext對象,裏面包含了module, state, moduleState, xeffect,剩下的參數纔對應的是是用戶調用xeffect是除第一第二位參數之外的其餘參數

import React,{Component, Fragment} from 'react';
import cc from 'react-control-center';

function* loginFn(changedBy, p1, p2 ,p3){
    return {changedBy, p1:p1+'--tail', p2:'head--'+p2 ,p3}
}

function* forInvoke(changedBy, p1, p2 ,p3){
    const result = yield loginFn(changedBy, p1, p2 ,p3);
    return result;
}
function* forEffect(changedBy, p1, p2 ,p3){
    const result = yield loginFn(changedBy, p1, p2 ,p3);
    return result;
}
function* forXeffect({module, state, moduleState, xeffect}, changedBy, p1, p2 ,p3){
    const result = yield loginFn(changedBy, p1, p2 ,p3);
    return result;
}

@cc.register('Foo')
class Foo extends Component{
     constructor(props, context){
        super(props, context);
        this.state = {changedBy:'none', p1:'', p2:'' ,p3:''};
    }
    render(){
        const {changedBy, p1, p2, p3} = this.state;
        //注,該cc類模塊沒有顯式的聲明模塊,會被cc當作$$default模塊的cc類
        return (
            <Fragment>
                <div>changedBy {changedBy}</div>
                <div>p1 {p1} p2 {p2} p3 {p3}</div>
                <button onClick={()=>this.$$invoke(forInvoke, 11,22,33)}>invoke</button>
                <button onClick={()=>this.$$effect('$$default',forEffect, 11,22,33)}>effect</button>
                <button onClick={()=>this.$$xeffect('$$default',forXeffect, 11,22,33)}>xeffect</button>
            </Fragment>
        );
    }
}
複製代碼

10 cc定位的容器型組件的狀態管理,一般狀況一些組件和model很是的有業務關係或者從屬關係,咱們會把這些react類註冊爲某個moudle的cc類,觀察這個模塊中的狀態變化,可是有些組件例如一個Workpace類的確須要觀察不少模塊的狀態變化,不算是某個模塊對應的視圖組件,此時除了用上面說的sharedToGlobalMapping功能,將須要觀察各個模塊的部分狀態映射到global裏,而後註冊Workpace時爲其設定globalStateKeys,就能達到觀察多個模塊的狀態變化的目的以外,cc還提供另外一種思路,註冊Workpace時設定stateToPropMapping,就能夠觀察恩義模塊的任意key的值變化,和sharedToGlobalMapping不一樣之處在於,stateToPropMapping要從this.$$propState裏取值,sharedToGlobalMapping是從this.state取值,固然stateToPropMapping不須要模塊主動的將某些key映射到global裏,就能達到跨模塊觀察狀態變化的目錄,cc鼓勵用戶精確對狀態歸類,並探索最佳組合和最佳實踐

// these code written in https://github.com/fantasticsoul/rcc-simple-demo/tree/master/src/cc-use-case/WatchMultiModule
// you can update the rcc-simple-demo lastest version, and run it, then switch tab watch-multi-module, you will see what happen
import React from 'react';
import cc from 'react-control-center';

class WatchMultiModule extends React.Component {
  render() {
    console.log('%c@@@ WatchMultiModule', 'color:green; border:1px solid green;');
    console.log(`type cc.setState('todo',{todoList:[{id:Date.now()+'_1',type:'todo',content:'nono'},{id:Date.now()+'_2',type:'todo',content:'nono'}]}) in console`);

    const { gbc, alias_content, counter_result, todoList } = this.$$propState;
    return (
      <div style={{width:'100%',height:'600px', border:'1px solid darkred'}}>
        <div>open your console</div>
        <div>type and then enter to see what happen <span style={{paddingLeft:'28px',color:'red'}}>cc.setState&#40;'counter',&#123;result &#58; 888&#125; &#41;</span></div>
        <div>type and then enter to see what happen <span style={{paddingLeft:'28px',color:'red'}}>cc.setGlobalState&#40; &#123;content:'wowowo'&#125; &#41;;</span></div>
        <div>{gbc}</div>
        <div>{alias_content}</div>
        <div>{counter_result}</div>
        <div>{todoList.length}</div>
      </div>
    );
  }
}

const stateToPropMapping = {
  '$$global/borderColor': 'gbc',
  '$$global/content': 'alias_content',
  'counter/result': 'counter_result',
  'todo/todoList': 'todoList',
};

//two way to declare watching multi module cc class
export default cc.connect('WatchMultiModule', stateToPropMapping)(WatchMultiModule);
//export default cc.register('WatchMultiModule', {stateToPropMapping})(WatchMultiModule);
複製代碼

github地址:github.com/fantasticso…


gitee地址:gitee.com/nick_zhong/…


quick-start demo: github.com/fantasticso…

期待你們試用並給出修改意見,真心但願可以親愛你的可以感覺的cc的魅力和強大,由於做爲redux使用者的我(3年了快),不論是用了原生的redux仍是dva封裝後的redux,我的都以爲沒有cc使用那麼的爽快......先從示例項目開始體驗吧^_^,期待着你和我有同樣的感覺

相關文章
相關標籤/搜索