目錄回顧vue
react用戶最初接觸接觸react時,必定被洗腦了無數次下面幾句話java
它們體現着react的精髓,最初的時候,咱們接觸的最原始的也是最多的觸發react視圖渲染就是
setState
,這個函數打開了通往react世界的大門,由於有了setState
,咱們可以賦予組件生命,讓它們按照咱們開發者的意圖動起來了。
漸漸的咱們發現,當咱們的單頁面應用組件愈來愈多的時候,它們各自的狀態造成了一個個孤島,沒法相互之間優雅的完成合做,咱們愈來愈須要一個集中式的狀態管理方案,因而facebook提出了flux方案,解決龐大的組件羣之間狀態不統1、通訊複雜的問題react
僅接着社區優秀的flux實現涌現出來,最終沉澱下來造成了龐大用戶羣的有redux
,mbox
等,本文再也不這裏比較cc與它們之間的具體差別,由於cc
其實也是基於flux實現的方案,可是cc
最大的特色是直接接管了setState
,以此爲根基實現整個react-control-center
的核心邏輯,因此cc
是對react
入侵最小且改寫現有代碼邏輯最靈活的方案,整個cc
內核的簡要實現以下git
setState
,還有
dispatch
、
effect
,以及3個點,由於cc觸發有不少種,這裏只說起
setState
、
dispatch
和
effect
這3種能覆蓋用戶99%場景的方法,期待讀完本文的你,可以愛上
cc
。
如下是一個你們見到的最最普通的有狀態組件,視圖裏包含了一個名字顯示和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組件很是容易,將你的react組件註冊到cc,其餘就交給cc吧,這裏咱們先在程序的第一行啓動cc,聲明一個store
redux
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函數的解釋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字符串.由於咱們註冊
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 />
,讓你的能夠快速的在已有的項目裏使用起來,