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

正楷發表於2019-01-12

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變化的目的。

  • 一統天下

隨著redux生態逐漸完善,大家預設的把redux當做了react狀態管理的首選解決方案,所以redux已經在react狀態管理框架裡一統天下,通過github star發現,另一個流行的狀態管理框架mobx頁已經鎖定第二的位置,用另一種思路來給大家展示原來狀態可以這麼管理,狀態管理的格局似乎基本已經洗牌完成,可是作為redux重度使用者的我並不滿足與此,覺得在redux世界裡,通過props層層穿透資料,通過provider包裹整個react app應用,只是解決了狀態的流動問題,而元件的通訊任然非常間接與尷尬,依靠redux來完成不是不可以,只是對比vue,任然覺得少了些什麼......

  • why cc

react-control-center並不是簡單的立足於狀態管理,而是想為你提供更多的有趣玩法,因為現有的狀態管理解決方案已經非常成熟(但是在某些場景未必真的好用),所以cc從一開始設計就讓其api對現有的元件入侵非常之小,你可以在redux專案裡區域性使用cc來把玩cc的狀態管理思路,可以從一個元件開始,慢慢在開始漸進式的修改到其他地方,僅僅使用一個register函式,將你的react類註冊為cc類,那麼從cc類生成的cc例項,將給你帶來以下新的特性、新的概念、新的思路。

1 所有cc例項都擁有 emiton 的能力,無論元件間巢狀關係多複雜,實現元件間通訊將會是如此輕鬆。

實際上例項還擁有更精準的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去觸發^_^

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物件裡。

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需要的引數

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

  • 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為f1、f2、f3、f4、f5, ccClass1通過sharedStateKeys觀察模組M的f1、f2, ccClass2通過sharedStateKeys觀察模組M的f2、f3、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對應的值合併得出來。

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

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使用那麼的爽快......先從示例專案開始體驗吧^_^,期待著你和我有一樣的感受

相關文章