目錄回顧
前言
最初的react
react使用者最初接觸接觸react時,一定被洗腦了無數次下面幾句話
- 資料驅動檢視
- 單向資料流
- 元件化
它們體現著react的精髓,最初的時候,我們接觸的最原始的也是最多的觸發react檢視渲染就是
setState
,這個函式開啟了通往react世界的大門,因為有了setState
,我們能夠賦予元件生命,讓它們按照我們開發者的意圖動起來了。
漸漸的我們發現,當我們的單頁面應用元件越來越多的時候,它們各自的狀態形成了一個個孤島,無法相互之間優雅的完成合作,我們越來越需要一個集中式的狀態管理方案,於是facebook提出了flux方案,解決龐大的元件群之間狀態不統一、通訊複雜的問題
狀態管理來了
僅接著社群優秀的flux實現湧現出來,最終沉澱下來形成了龐大使用者群的有redux
,mbox
等,本文不再這裡比較cc與它們之間的具體差異,因為cc
其實也是基於flux實現的方案,但是cc
最大的特點是直接接管了setState
,以此為根基實現整個react-control-center
的核心邏輯,所以cc
是對react
入侵最小且改寫現有程式碼邏輯最靈活的方案,整個cc
核心的簡要實現如下
可以看到上圖裡除了setState
,還有dispatch
、effect
,以及3個點,因為cc觸發有很多種,這裡只提及setState
、dispatch
和effect
這3種能覆蓋使用者99%場景的方法,期待讀完本文的你,能夠愛上cc
。
setState,線上示例程式碼 線上示例程式碼2
一個普通的react元件誕生了,
以下是一個大家見到的最最普通的有狀態元件,檢視裡包含了一個名字顯示和input框輸入,讓使用者輸入新的名字
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,宣告一個store
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函式的解釋
/****
* @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類註冊到不同的模組,下面我們寫另一個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>
)
}
}
複製程式碼
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 });
}
複製程式碼
對模組精確劃分
上面貼圖中,我們看到當我們修改<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函式歸類方式就很靈活了,使用者可按照自己的理解去做歸類
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 };
}
}
複製程式碼
cc支援reducer函式可以是async或者generator函式,其實reducer函式的引數excutionContext可以解構出module
、effect
、xeffect
、state
、moduleState
、globalState
、dispatch
等引數,
我們在reducer函式發起了其他的副作用呼叫
dispatch內部,組合其他dispatch
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函式
effect,最靈活的setState
不想用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`);
複製程式碼
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
、$$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 });
複製程式碼
類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會取消掉它的監聽函式,並刪除對它的引用,防止記憶體洩露
關於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(``);
}
}
}
複製程式碼
關於ccDom
cc預設採用的是反向繼承的方式包裹你的react類,所以在reactDom樹看到的元件非常乾淨,不會有多級包裹
關於頂層函式和store
現在,你可以開啟console,輸入cc.
,可以直接呼叫dispatch
、emit
、setState
等函式,讓你快速驗證你的渲染邏輯,輸入sss,檢視整個cc的狀態樹結構
結語
好了,基本上cc驅動檢視渲染的3個基本函式介紹就到這裡了,cc只是提供了最最基礎驅動檢視渲染的方式,並不強制使用者使用哪一種,使用者可以根據自己的實際情況摸索出最佳實踐
因為cc接管了setState,所以cc可以不需要包裹<Provider />
,讓你的可以快速的在已有的專案裡使用起來,