前言
最近瀏覽到vue
開發者尤雨溪以前的採訪文章,感觸頗深,其中有一段問答大概是這樣:
採訪:是什麼驅使你開發 Vue.js 的?
答:我想,我可以只把我喜歡的部分從 Angular 中提出來,建立一個非常輕巧的庫,不需要那些額外的邏輯。我也很好奇 Angular 的原始碼到底是怎麼設計的。我最開始只是想著手提取 Angular 裡面很小的功能,如宣告式資料繫結。Vue 大概就是這麼開始的。 用過一段時間之後,我感覺我做的東西還有點前途,因為我自己就很喜歡用。於是我花了更多的時間把它封裝好,取了一個名字叫做 Vue.js,我記得那時還是 2013 年。後來我想『我花了這麼多時間,不能只有我一個人用,我應該和別人分享,他們也會感覺到 Vue 的好處,他們也會喜歡上 Vue 的。』
尤大的確非常直接,因為我自己喜歡用,所以我想分享給多的人,想讓更多的人喜歡...
,這是每一個開源作者由衷的體驗,從開源日期來說,react-control-center
的確非常非常的短,有一些朋友在成為種子使用者之前,都會問我一個問題,
為什麼有了redux, 或者說dva、rematch等更好的redux wrapper,以及mobx這樣強大的狀態管理框架,還要寫一個react-control-center呢?這樣一個輪子是不是有一點多餘
在回答這個問題之前,我想了下,尤大的那一段採訪回答的確非常符合我的心境,首先呢,我們的專案也在大量的過使用redux
或者dva
,我自己私底下也瞭解過mobx
,可是切換為react-control-center
的確讓我們的程式碼更加簡潔和更容易維護與擴充套件,而且比redux
多了很多非常好玩的特性,因為react-control-center
是基於react
的setState
做了增強,所以不存在黑魔法,只是讓你更優雅的呼叫setState
而已哦,接下來我聊一聊變化偵測,再結合setState
你一定會明白,或許我們不需要redux
這種方式,而是迴歸react
本質去做狀態管理,一樣可以高效而簡單,但是卻可以更加強大和有趣。
變化偵測
pull & push
變化偵測這個詞在尤大的採訪中提過不少次,我們同時也能看到尤大提到了變化偵測分為兩種pull
和push
,這裡我結合我對尤大的理解的解讀和從我自己的視覺來談一談pull
和push
,本質上來說,這是兩種不同的驅動方式來驅動資料和檢視保持同步,只不過前者pull
對於UI框架來說被動觸發,react
裡暴露一個setState
入口來讓開發人工的提交要改變的資料,這樣react
才知道資料變化了,push
對於UI框架來說主動觸發,對於vue
來說,你為元件宣告的data
都被轉換成了observable
物件,所以當你使用this.username='xxx'
的時候,vue
能夠主動偵測到你的資料發生了變化,資料和檢視渾然一體。
這兩種方式沒有誰更好誰更優秀一說,效能上不會成為你評判該採用誰是最優解的標準,更多的我們從工程性的角度來說,檢視渲染邏輯和業務邏輯必然耦合在一起,所以才有vuex
、redux
類似的方案,不只是幫你解決狀態管理的問題,同時也幫你分離了業務邏輯和檢視渲染邏輯。
cc接管setState後發生了什麼
讓我們把目光回到pull
和react
的setState
上,setState
的引數其實很簡單,你只需要提交你要修改的partialState
給react
,react
就觸發更新了。
對於cc
而言,將原始的setState
儲存為reactSetState
,然後使用者呼叫的setState
已不再是最初的那個控制程式碼,而是cc
自己的實現了,我們聊cc
的setState
實現步驟之前,看看register
函式的引數簽名。
register(ccClassKey:string, registerOption?:{module?:string, sharedStateKeys?:Array<string>|'*', globalStateKeys?:Array<string>|'*'});
複製程式碼
當你的一個普通的react class
註冊為cc class
的時候,通過設定registerOption.module
告訴cc
這個cc class
屬於哪個module
,通過設定registerOption.sharedStateKeys
告訴cc
這個cc class
的所有例項會共享那些sharedStateKey
的值變化,所以cc
內部的上下文會維護的兩個map
,第一個是module_ccClassKeys_
,鍵就是模組名,值就是這個模組下有哪些ccClassKey
,第二個是ccClassKey_ccClassContext_
,鍵就是ccClassKey
,值就是ccClassContext
,ccClassContext
內部維護一個引用陣列,表示當前ccClassKey
已經例項化了多少個cc instance
。
現在我們看一看如下的程式碼片段示意:
//假設store.foo如下:
store:{
foo:{
name:1,
age:2,
grade:3,
}
}
class Foo extends Component{
//constructor略
onNameChange = (e)=>{
this.setState({name:e.currentTarget.value});
}
onAgeChange = (e)=>{
this.setState({name:e.currentTarget.value});
}
render(){
const {name, age} = this.state;
return (
<Fragment>
<input onChange={this.onNameChange}/>
<input onChange={this.onAgeChange}/>
</Fragment>
);
}
}
const CcFoo1 = cc.register('Foo1', {module:'foo', sharedStateKeys:['name']})(Foo);
const CcFoo2 = cc.register('Foo2', {module:'foo', sharedStateKeys:'*'})(Foo);
//in your App.js
render(){
return (
<div>
<Foo />
<Foo />
<CcFoo1 />
<CcFoo1 />
<CcFoo2 />
<CcFoo2 />
</div>
);
}
複製程式碼
Foo
的例項其實孤立的,它們之間的state是獨立維護的,CcFoo1
儘管屬於foo
模組,但是隻是標記了sharedStateKeys
包含name
,所以只有name
的值變化是共享到了foo
模組的狀態裡,CcFoo2
標記了sharedStateKeys
為*
,所以foo
模組的所有狀態變化都會被cc
同步到CcFoo2
的所有例項上。
- 那我們現在來具體化這個過程,如果
CcFoo1
的一個例項改變了name
,當你呼叫setState
的時候,cc
先呼叫當前例項的reactSetState
觸發UI渲染行為。 - 然後你提交的
{name:'xxx'}
經過cc
分析,當前例項所屬的cc類Foo1
下還有另一個例項CcFoo1_ins2
,所以除了呼叫reactSetState
把狀態設定到當前例項,也會呼叫CcFoo1_ins2.reactSetState
把狀態設定回去。 - 同樣的通過
module_ccClassKeys_
這個對映關係,cc
發現還有另一個cc類Foo2
也屬於foo
模組,然後cc
會通過ccClassKey_ccClassContext_
取出這個cc類的其他例項,遍歷的呼叫reactSetState
把狀態設定到哪些具體的例項上,這樣一個過程,在cc
內部成為狀態廣播
,看到了嗎?原理非常簡單,同時也非常高效,沒有angular
那樣的生成一個個watcher
做髒檢查,僅僅只是找到正確的引用,提取合適的狀態,然後觸發reactSetState
,便結束了,這便是為什麼我說react-control-center
只是讓setState
更加智慧而已。
Foo ins1 --- name changed ---> Foo ins2
Foo ins2 --- name changed ---> Foo ins2
CcFoo1 ins1 --- name changed ---> CcFoo1 ins1
|--> CcFoo1 ins2
|--> CcFoo2 ins1
|--> CcFoo2 ins2
CcFoo2 ins1 --- age changed ---> CcFoo2 ins1
|--> CcFoo2 ins2
複製程式碼
more than setState
當然cc
不只是提供setState
這個入口讓你去修改,因為通常能夠修改資料之前都會有不少的業務邏輯,最後才到setState
這一步觸發UI渲染,所以cc
通過更強大、更靈活的api讓你不在和setState
打交道。
- dispatch(action:Action | reducerDescriptorStr, payload?:any),
dispatch
的本質是找到你定義的reducer
函式去執行,執行完之後返回一個新的partialState
就完了,其它的一切交個cc
搞定。 cc
並不強制reducer
函式返回新的partialState
,提供一個dispatch
控制程式碼讓你組合多個reducer
函式執行,序列或者是並行任君選擇,是不是非常的愜意^_^
//reducer in StartupOption
cc.startup({
reducer:{
'foo':{
changeName({payload:name}){
return {name};
},
async changeNameCool({dispatch, payload:name}){
await dispatch('changeName', name);
// await dispatch(); 組合多個函式序列執行
}
}
}
})
class Foo extends Component{
//constructor略
onNameChange = (e)=>{
//this.$$dispatch({type:'changeName', payload:e.currentTarget.value});
//推薦這種更簡便的寫法
this.$$dispatch('changeName', e.currentTarget.value);
}
changeNameCool = ()=>{
this.$$dispatch('changeNameCool', e.currentTarget.value);
}
render(){
const {name, age} = this.state;
return (
<Fragment>
<input value={name} onChange={this.onNameChange}/>
<input value={name} onChange={this.changeNameCool}/>
</Fragment>
);
}
}
複製程式碼
- invoke(userFn:function, ...args),如果你討厭走
dispatch
去命中reducer
函式這個套路,cc
同樣允許你呼叫自定義函式,invoke預設改變自己例項所屬模組的狀態。 - effect(module:string, userFn:function, ...args),你需要改變其他模組的狀態,cc同樣支援。
- 打破了
redux
的套路,狀態追蹤怎麼辦?其實這是一個你無須擔心的問題,你呼叫dispatch
、invoke
、effect
等這些控制程式碼時,都是暗自攜帶者上下文的。
1 包括這一次呼叫提交的狀態
2 這此呼叫時哪一種方式觸發的,使用者可以使用setState的哦.....
3 這次呼叫是從哪個例項產生的
所以你想一想,是不是比redux
一個孤獨的action type
能給你更多的資訊?當然狀態管理只是cc
裡該做的一部分,同樣的更友好的副作用書寫方式,類vue的computed
、watch
、emit&on
等更好玩的特性才是cc
要幫助你用更優雅的方式書寫react
。
hook
新版的react已經發布了,hook
已成為穩定版的api,facebook
在此基礎上提出了新的元件劃分方式:class component
和function component
,注意到沒有,不再說笨元件和智慧元件了,因為function component
可以使用hook
,它不再是笨蛋了....
function component
可以管理自己狀態,甚至可以通過useContext
實現不同的function component
之間共享狀態,看起來class component
慢慢會被取代嗎?
這一點目前個人不敢下結論,但是在cc
的世界裡,因為有了CcFragment
的存在,能夠讓你不用為了使用一些現有的store
和reducer
組合一個新的檢視而去抽一個class
出來的不必要局面,你可以達到快速複用現有的stateless component
包裹在CcFragment
,同樣的考慮到使用者需要在CcFragment
管理自己的狀態,cc
最新版本已支援在CcFragment
裡使用hook
,這不是一個對react hook
的包裹,而是獨立的實現,所以你依然可以在react 15
裡使用,api命名和使用效果和react hook
保持100%一致,當然使用規則也是一樣的:不要在迴圈,條件或巢狀函式中呼叫Hook
,注意哦,cc
的hook
僅僅限在CcFragment
內使用。
<CcFragment connect={{'counter/*':''}} render={({ hook, propState }) => {
const [count, setCount] = hook.useState(0);
hook.useEffect(()=>{
document.title = 'count '+count;
return ()=>{
document.title = 'CcFragment unmount ';
}
});
//如果只想讓effect函式在didMount的執行,可以寫為 hook.useEffect(fn, []);
//如果只想讓effect函式依賴count值是否變化才執行,可以寫為 hook.useEffect(fn, [count]);
return (
<div style={{border:'6px solid gold', margin:'6px'}}>
<h3>show CcFragment hook feature</h3>
{propState.counter.count}
<hr />
{count}
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
</div>
)
}} />
複製程式碼
有了hook
,CcFragment
不僅能打通store
,也能夠獨立管理自己的狀態,是不是更可愛了呢?
hook實現如下,其實正如react hook
所說,不是魔法,只是陣列.....
// hook implement fo CcFragment
const __hookMeta = {
isCcFragmentMounted:false,
useStateCount: 0,
useStateCursor: 0,
stateArr:[],
useEffectCount: 0,
useEffectCursor: 0,
effectCbArr:[],
effectSeeAoa:[],// "shouldEffectExecute array of array"
effectSeeResult:[],// "collect every effect fn's shouldExecute result"
effectCbReturnArr:[],
}
this.__hookMeta = __hookMeta;
const hook = {
useState: initialState => {
let cursor = __hookMeta.useStateCursor;
const stateArr = __hookMeta.stateArr;
__hookMeta.useStateCursor++;
if (__hookMeta.isCcFragmentMounted === false) {//render CcFragment before componentDidMount
__hookMeta.useStateCount++;
stateArr[cursor] = initialState;
} else {
cursor = cursor % __hookMeta.useStateCount;
}
const setter = newState => {
stateArr[cursor] = newState;
this.cc.reactForceUpdate();
}
return [stateArr[cursor], setter];
},
useEffect: (cb, shouldEffectExecute) => {
let cursor = __hookMeta.useEffectCursor;
__hookMeta.useEffectCursor++;
if (__hookMeta.isCcFragmentMounted === false) {
__hookMeta.effectCbArr.push(cb);
__hookMeta.effectSeeAoa.push(shouldEffectExecute);
__hookMeta.useEffectCount++;
} else {
// if code running jump into this block, CcFragment already mounted, and now compute result for didUpdate
cursor = cursor % __hookMeta.useEffectCount;
if (Array.isArray(shouldEffectExecute)) {
const len = shouldEffectExecute.length;
if (len == 0) {
__hookMeta.effectSeeResult = false;// effect fn will been executed only in didMount
} else {// compare prevSee and curSee
let effectSeeResult = false;
const prevSeeArr = __hookMeta.effectSeeAoa[cursor];
if (!prevSeeArr) {
effectSeeResult = true;
} else {
for (let i = 0; i < len; i++) {
if (shouldEffectExecute[i] !== prevSeeArr[i]) {
effectSeeResult = true;
break;
}
}
}
__hookMeta.effectSeeAoa[cursor] = shouldEffectExecute;
__hookMeta.effectSeeResult[cursor] = effectSeeResult;
if (effectSeeResult) __hookMeta.effectCbArr[cursor] = cb;
}
} else {
__hookMeta.effectSeeResult[cursor] = true;// effect fn will always been executed in didMount and didUpdate
__hookMeta.effectSeeAoa[cursor] = shouldEffectExecute;
__hookMeta.effectCbArr[cursor] = cb;
}
}
}
}
複製程式碼
結語
前人總結出的優秀的方案,為何不融入到cc裡呢?期待看完本文的你,能所有收穫。hook
真的優雅的解決了在CcFragment
裡管理localState
的問題,所以才被加入進來,不是為了加而加,期待你也能夠愛上cc
,愛上CcFragment
,愛上cc hook
。