聊一聊cc的變化偵測和hook實現

正楷發表於2019-03-17

前言

最近瀏覽到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是基於reactsetState做了增強,所以不存在黑魔法,只是讓你更優雅的呼叫setState而已哦,接下來我聊一聊變化偵測,再結合setState你一定會明白,或許我們不需要redux這種方式,而是迴歸react本質去做狀態管理,一樣可以高效而簡單,但是卻可以更加強大和有趣。


變化偵測

pull & push

變化偵測這個詞在尤大的採訪中提過不少次,我們同時也能看到尤大提到了變化偵測分為兩種pullpush,這裡我結合我對尤大的理解的解讀和從我自己的視覺來談一談pullpush,本質上來說,這是兩種不同的驅動方式來驅動資料和檢視保持同步,只不過前者pull對於UI框架來說被動觸發,react裡暴露一個setState入口來讓開發人工的提交要改變的資料,這樣react才知道資料變化了,push對於UI框架來說主動觸發,對於vue來說,你為元件宣告的data都被轉換成了observable物件,所以當你使用this.username='xxx'的時候,vue能夠主動偵測到你的資料發生了變化,資料和檢視渾然一體。
這兩種方式沒有誰更好誰更優秀一說,效能上不會成為你評判該採用誰是最優解的標準,更多的我們從工程性的角度來說,檢視渲染邏輯和業務邏輯必然耦合在一起,所以才有vuexredux類似的方案,不只是幫你解決狀態管理的問題,同時也幫你分離了業務邏輯和檢視渲染邏輯。

cc接管setState後發生了什麼

讓我們把目光回到pullreactsetState上,setState的引數其實很簡單,你只需要提交你要修改的partialStatereactreact就觸發更新了。
對於cc而言,將原始的setState儲存為reactSetState,然後使用者呼叫的setState已不再是最初的那個控制程式碼,而是cc自己的實現了,我們聊ccsetState實現步驟之前,看看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,值就是ccClassContextccClassContext內部維護一個引用陣列,表示當前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

複製程式碼

聊一聊cc的變化偵測和hook實現

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的套路,狀態追蹤怎麼辦?其實這是一個你無須擔心的問題,你呼叫dispatchinvokeeffect等這些控制程式碼時,都是暗自攜帶者上下文的。

1 包括這一次呼叫提交的狀態
2 這此呼叫時哪一種方式觸發的,使用者可以使用setState的哦.....
3 這次呼叫是從哪個例項產生的

所以你想一想,是不是比redux一個孤獨的action type能給你更多的資訊?當然狀態管理只是cc裡該做的一部分,同樣的更友好的副作用書寫方式,類vue的computedwatchemit&on等更好玩的特性才是cc要幫助你用更優雅的方式書寫react

hook

新版的react已經發布了,hook已成為穩定版的api,facebook在此基礎上提出了新的元件劃分方式:class componentfunction component,注意到沒有,不再說笨元件和智慧元件了,因為function component可以使用hook,它不再是笨蛋了....
function component可以管理自己狀態,甚至可以通過useContext實現不同的function component之間共享狀態,看起來class component慢慢會被取代嗎?
這一點目前個人不敢下結論,但是在cc的世界裡,因為有了CcFragment的存在,能夠讓你不用為了使用一些現有的storereducer組合一個新的檢視而去抽一個class出來的不必要局面,你可以達到快速複用現有的stateless component包裹在CcFragment,同樣的考慮到使用者需要在CcFragment管理自己的狀態,cc最新版本已支援在CcFragment裡使用hook,這不是一個對react hook的包裹,而是獨立的實現,所以你依然可以在react 15裡使用,api命名和使用效果和react hook保持100%一致,當然使用規則也是一樣的:不要在迴圈,條件或巢狀函式中呼叫Hook,注意哦,cchook僅僅限在CcFragment內使用。

聊一聊cc的變化偵測和hook實現

 <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>
    )
  }} />
複製程式碼

有了hookCcFragment不僅能打通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

相關文章