[react-control-center tutorial 3] 資料驅動檢視的靈魂setState

正楷發表於2019-03-03

目錄回顧


前言

最初的react

react使用者最初接觸接觸react時,一定被洗腦了無數次下面幾句話

  • 資料驅動檢視
  • 單向資料流
  • 元件化

它們體現著react的精髓,最初的時候,我們接觸的最原始的也是最多的觸發react檢視渲染就是setState,這個函式開啟了通往react世界的大門,因為有了setState,我們能夠賦予元件生命,讓它們按照我們開發者的意圖動起來了。
漸漸的我們發現,當我們的單頁面應用元件越來越多的時候,它們各自的狀態形成了一個個孤島,無法相互之間優雅的完成合作,我們越來越需要一個集中式的狀態管理方案,於是facebook提出了flux方案,解決龐大的元件群之間狀態不統一、通訊複雜的問題

[react-control-center tutorial 3] 資料驅動檢視的靈魂setState

狀態管理來了

僅接著社群優秀的flux實現湧現出來,最終沉澱下來形成了龐大使用者群的有reduxmbox等,本文不再這裡比較cc與它們之間的具體差異,因為cc其實也是基於flux實現的方案,但是cc最大的特點是直接接管了setState,以此為根基實現整個react-control-center的核心邏輯,所以cc是對react入侵最小且改寫現有程式碼邏輯最靈活的方案,整個cc核心的簡要實現如下

[react-control-center tutorial 3] 資料驅動檢視的靈魂setState

可以看到上圖裡除了setState,還有dispatcheffect,以及3個點,因為cc觸發有很多種,這裡只提及setStatedispatcheffect這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

上面動態圖中我們可以看到幾點<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字串.

因為我們註冊HelloCCHello的時候,語句如下
const CCHello = cc.register(`Hello`,{sharedStateKeys:`*`})(Hello);
沒有宣告任何模組,所以CCHello屬於$$default模組,定義了sharedStateKeys*
表示觀察和共享$$default模組的整個狀態,所以在starup裡定義的storename就被同步到CCHello

  • 新增了3個<CCHello />後,對其中輸入名字後,另外兩個也同步渲染了

因為對其中一個<CCHello />輸入名字時,
其他兩個<CCHello/>他們也屬於`$$default`模組,也共享和觀察name的變化,
所以其實任意一個<CCHello />的輸入,cc都會將狀態廣播到其他兩個<CCHello />

多模組話組織狀態樹

前面文章我們介紹cc.startup時說起推薦使用者使用多模組話啟動cc,所以我們稍稍改造一下starup啟動引數,讓我們的不僅僅只是使用cc的內建模組$$default$$global
定義兩個新的模組foobar,可以把他們的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類HelloFooHelloBar,然後渲染他們看看效果吧

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>
    )
  }
}
複製程式碼
多個模組的Hello

以上我們演示了用同一個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>
    )
  }
}
複製程式碼
Wow來了

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 });
  }
複製程式碼
Wow來了

對模組精確劃分

上面貼圖中,我們看到當我們修改<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 });
  }
複製程式碼
Wow來了

上圖的演示效果正如我們的預期效果,三個註冊到不同的模組的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 };
      }
    }
複製程式碼
dispatch

cc支援reducer函式可以是async或者generator函式,其實reducer函式的引數excutionContext可以解構出moduleeffectxeffectstatemoduleStateglobalStatedispatch等引數,
我們在reducer函式發起了其他的副作用呼叫

dispatch內部,組合其他dispatch

cc並不強制要求所有的reducer函式返回一個新的state,所以我們可以利用dispatch發起呼叫組合其他的dispatch
基於上面的需求,我們再給自己來下一個這樣的需求,當foo模組的例項輸入的是666的時候,把“foobar的所有例項的那麼重置為恭喜你中獎500萬了,我們保留原來的changeName,新增一個函式changeNameWithAwardawardYou,然後元件裡呼叫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);
      }
    }
複製程式碼
dispatch2

我們可以看到awardYou裡並沒有返回新的state,而是並行呼叫changeName。
cc基於這樣的組合dispatch理念可以讓你跟靈活的組織程式碼和重用已有的reducer函式

effect,最靈活的setState

不想用dispatchreducer組合拳?試試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 });  
    }
  }
複製程式碼
dispatch2

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$$dispatcheffect都可以設定延遲時間,單位是毫秒,側面印證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 });
複製程式碼
dispatch2

類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會取消掉它的監聽函式,並刪除對它的引用,防止記憶體洩露

emit

關於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(``);
    }
  }
}
複製程式碼
computed

關於ccDom

cc預設採用的是反向繼承的方式包裹你的react類,所以在reactDom樹看到的元件非常乾淨,不會有多級包裹

ccdom

關於頂層函式和store

現在,你可以開啟console,輸入cc.,可以直接呼叫dispatchemitsetState等函式,讓你快速驗證你的渲染邏輯,輸入sss,檢視整個cc的狀態樹結構

[react-control-center tutorial 3] 資料驅動檢視的靈魂setState

結語

好了,基本上cc驅動檢視渲染的3個基本函式介紹就到這裡了,cc只是提供了最最基礎驅動檢視渲染的方式,並不強制使用者使用哪一種,使用者可以根據自己的實際情況摸索出最佳實踐
因為cc接管了setState,所以cc可以不需要包裹<Provider />,讓你的可以快速的在已有的專案裡使用起來,

具體程式碼點此處

線上演示點此處,注:線上演示程式碼不完整,最完整的執行此專案

相關文章