redux、mobx、concent特性大比拼, 看後生如何對局前輩

❤ star me if you like concent ^_^css

序言

reduxmobx自己是一個獨立的狀態管理框架,各自有本身的抽象api,以其餘UI框架無關(react, vue...),本文主要說的和react搭配使用的對比效果,因此下文裏提到的reduxmobx暗含了react-reduxmobx-react這些讓它們可以在react中發揮功能的綁定庫,而concent自己是爲了react貼身打造的開發框架,數據流管理只是做爲其中一項功能,附帶的其餘加強react開發體驗的特性能夠按需使用,後期會刨去concent裏全部與react相關聯的部分發布concent-core,它的定位纔是與reduxmobx 類似的。vue

因此其實將在本文裏登場的選手分別是react

redux & react-redux

  • slogan
    JavaScript 狀態容器,提供可預測化的狀態管理
  • 設計理念
    單一數據源,使用純函數修改狀態

mobx & mobx-react

  • slogan:
    簡單、可擴展的狀態管理
  • 設計理念
    任何能夠從應用程序狀態派生的內容都應該派生

concent

  • slogan:
    可預測、0入侵、漸進式、高性能的react開發方案
  • 設計理念
    相信融合不可變+依賴收集的開發方式是react的將來,加強react組件特性,寫得更少,作得更多。

介紹完三者的背景,咱們的舞臺正式交給它們,開始一輪輪角逐,看誰到最後會是你最中意的範兒?git

結果預覽

如下5個較量回合實戰演示代碼較多,此處將對比結果提早告知,方便粗讀看客能夠快速瞭解。github

store配置 concent mbox redux
支持分離 Yes Yes No
無根Provider & 使用處無需顯式導入 Yes No No
reducer無this Yes No Yes
store數據或方法無需人工映射到組件 Yes Yes No

redux counter示例
mobx counter示例
concent counter示例
_編程

狀態修改 concent mbox redux
基於不可變原則 Yes No Yes
最短鏈路 Yes Yes No
ui源頭可追蹤 Yes No No
無this Yes No Yes
原子拆分&合併提交 Yes(基於lazy) Yes(基於transaction) No

_json

依賴收集 concent mbox redux
支持運行時收集依賴 Yes Yes No
精準渲染 Yes Yes No
無this Yes No No
只需一個api介入 Yes No No

mobx 示例
concent 示例 redux

_api

衍生數據 concent mbox redux(reselect)
自動維護計算結果之間的依賴 Yes Yes No
觸發讀取計算結果時收集依賴 Yes Yes No
計算函數無this Yes No Yes

redux computed示例
mobx computed示例
concent computed示例 數組

_
todo-mvc實戰
redux todo-mvc
mobx todo-mvc
concent todo-mvc

round 1 - 代碼風格初體驗

counter做爲demo界的靚仔被無數次推上舞臺,這一次咱們依然不例外,來個counter體驗3個框架的開發套路是怎樣的,如下3個版本都使用create-react-app建立,並以多模塊的方式來組織代碼,力求接近真實環境的代碼場景。

redux(action、reducer)

經過models把按模塊把功能拆到不一樣的reducer裏,目錄結構以下

|____models             # business models
| |____index.js         # 暴露store
| |____counter          # counter模塊相關的action、reducer
| | |____action.js     
| | |____reducer.js     
| |____ ...             # 其餘模塊
|____CounterCls         # 類組件
|____CounterFn          # 函數組件
|____index.js           # 應用入口文件
此處僅與redux的原始模板組織代碼,實際狀況可能很多開發者選擇了 rematchdva等基於redux作二次封裝並改進寫法的框架,可是並不妨礙咱們理解counter實例。

構造counter的action

// code in models/counter/action
export const INCREMENT = "INCREMENT";

export const DECREMENT = "DECREMENT";

export const increase = number => {
  return { type: INCREMENT, payload: number };
};

export const decrease = number => {
  return {  type: DECREMENT, payload: number };
};

構造counter的reducer

// code in models/counter/reducer
import { INCREMENT, DECREMENT } from "./action";

export default (state = { count: 0 }, action) => {
  const { type, payload } = action;
  switch (type) {
    case INCREMENT:
      return { ...state, count: state.count + payload };
    case DECREMENT:
      return { ...state, count: state.count - payload };
    default:
      return state;
  }
};

合併reducer構造store,並注入到根組件

mport { createStore, combineReducers } from "redux";
import  countReducer  from "./models/counter/reducer";

const store = createStore(combineReducers({counter:countReducer}));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

使用connect鏈接ui與store

import React from "react";
import { connect } from "react-redux";
import { increase, decrease } from "./redux/action";

@connect(
  state => ({ count: state.counter.count }),// mapStateToProps
  dispatch => ({// mapDispatchToProps
    increase: () => dispatch(increase(1)),
    decrease: () => dispatch(decrease(1))
  }),
)
class Counter extends React.Component {
  render() {
    const { count, increase, decrease } = this.props;
    return (
      <div>
        <h1>Count : {count}</h1>
        <button onClick={increase}>Increase</button>
        <button onClick={decrease}>decrease</button>
      </div>
    );
  }
}

export default Counter;

上面的示例書寫了一個類組件,而針對如今火熱的hookredux v7也發佈了相應的apiuseSelectoruseDispatch

import * as React from "react";
import { useSelector, useDispatch } from "react-redux";
import * as counterAction from "models/counter/action";

const Counter = () => {
  const count = useSelector(state => state.counter.count);
  const dispatch = useDispatch();
  const increase = () => dispatch(counterAction.increase(1));
  const decrease = () => dispatch(counterAction.decrease(1));

  return (
    <>
      <h1>Fn Count : {count}</h1>
      <button onClick={increase}>Increase</button>
      <button onClick={decrease}>decrease</button>
    </>
  );
};

export default Counter;

渲染這兩個counter,查看redux示例

function App() {
  return (
      <div className="App">
        <CounterCls/>
        <CounterFn/>
      </div>
  );
}

mobx(store, inject)

當應用存在多個store時(這裏咱們能夠把一個store理解成redux裏的一個reducer塊,聚合了數據、衍生數據、修改行爲),mobx的store獲取方式有多種,例如在須要用的地方直接引入放到成員變量上

import someStore from 'models/foo';// 是一個已經實例化的store實例

@observer
class Comp extends React.Component{
    foo = someStore;
    render(){
        this.foo.callFn();//調方法
        const text = this.foo.text;//取數據
    }
}

咱們此處則按照公認的最佳實踐來作,即把全部store合成一個根store掛到Provider上,並將Provider包裹整個應用根組件,在使用的地方標記inject裝飾器便可,咱們的目錄結構最終以下,和redux版本並沒有區別

|____models             # business models
| |____index.js         # 暴露store
| |____counter          # counter模塊相關的store
| | |____store.js     
| |____ ...             # 其餘模塊
|____CounterCls         # 類組件
|____CounterFn          # 函數組件
|____index.js           # 應用入口文件

構造counter的store

import { observable, action, computed } from "mobx";

class CounterStore {
  @observable
  count = 0;

  @action.bound
  increment() {
    this.count++;
  }

  @action.bound
  decrement() {
    this.count--;
  }
}

export default new CounterStore();

合併全部store根store,並注入到根組件

// code in models/index.js
import counter from './counter';
import login from './login';

export default {
  counter,
  login,
}

// code in index.js
import React, { Component } from "react";
import { render } from "react-dom";
import { Provider } from "mobx-react";
import store from "./models";
import CounterCls from "./CounterCls";
import CounterFn from "./CounterFn";

render(    
    <Provider store={store}>
      <App />
    </Provider>, 
    document.getElementById("root")
);

建立一個類組件

import React, { Component } from "react";
import { observer, inject } from "mobx-react";

@inject("store")
@observer
class CounterCls extends Component {
  render() {
    const counter = this.props.store.counter;
    return (
      <div>
        <div> class Counter {counter.count}</div>
        <button onClick={counter.increment}>+</button>
        <button onClick={counter.decrement}>-</button>
      </div>
    );
  }
}

export default CounterCls;

建立一個函數組件

import React from "react";
import { useObserver, observer } from "mobx-react";
import store from "./models";

const CounterFn = () => {
  const { counter } = store;
  return useObserver(() => (
      <div>
        <div> class Counter {counter.count}</div>
        <button onClick={counter.increment}>++</button>
        <button onClick={counter.decrement}>--</button>
      </div>
  ));
};

export default CounterFn;

渲染這兩個counter,查看mobx示例

function App() {
  return (
      <div className="App">
        <CounterCls/>
        <CounterFn/>
      </div>
  );
}

concent(reducer, register)

concent和redux同樣,存在一個全局單一的根狀態RootStore,該根狀態下第一層key用來當作模塊命名空間,concent的一個模塊必需配置state,剩下的reducercomputedwatchinit是可選項,能夠按需配置,若是把store全部模塊寫到一處,最簡版本的concent示例以下

import { run, setState, getState, dispatch } from 'concent';
run({
    counter:{// 配置counter模塊
        state: { count: 0 }, // 【必需】定義初始狀態, 也可寫爲函數 ()=>({count:0})
        // reducer: { ...}, // 【可選】修改狀態的方法
        // computed: { ...}, // 【可選】計算函數
        // watch: { ...}, // 【可選】觀察函數
        // init: { ...}, // 【可選】異步初始化狀態函數
    }
});

const count = getState('counter').count;// count is: 0
// count is: 1,若是有組件屬於該模塊則會被觸發重渲染
setState('counter', {count:count + 1});

// 若是定義了counter.reducer下定義了changeCount方法
// dispatch('counter/changeCount')

啓動concent載入store後,可在其它任意類組件或函數組件裏註冊其屬於於某個指定模塊或者鏈接多個模塊

import { useConcent, register } from 'concent';

function FnComp(){
    const { state, setState, dispatch } = useConcent('counter');
    // return ui ...
}

@register('counter')
class ClassComp extends React.Component(){
    render(){
        const { state, setState, dispatch } = this.ctx;
        // return ui ...
    }
}

可是推薦將模塊定義選項放置到各個文件中,以達到職責分明、關注點分離的效果,因此針對counter,目錄結構以下

|____models             # business models
| |____index.js         # 配置store各個模塊
| |____counter          # counter模塊相關
| | |____state.js       # 狀態
| | |____reducer.js     # 修改狀態的函數
| | |____index.js       # 暴露counter模塊
| |____ ...             # 其餘模塊
|____CounterCls         # 類組件
|____CounterFn          # 函數組件
|____index.js           # 應用入口文件
|____runConcent.js      # 啓動concent

構造counter的statereducer

// code in models/counter/state.js
export default {
  count: 0,
}

// code in models/counter/reducer.js
export function increase(count, moduleState) {
  return { count: moduleState.count + count };
}

export function decrease(count, moduleState) {
  return { count: moduleState.count - count };
}

兩種方式配置store

  • 配置在run函數裏
import counter from 'models/counter';

run({counter});
  • 經過configure接口配置, run接口只負責啓動concent
// code in runConcent.js
import { run } from 'concent';
run();

// code in models/counter/index.js
import state from './state';
import * as reducer from './reducer';
import { configure } from 'concent';

configure('counter', {state, reducer});// 配置counter模塊

建立一個函數組件

import * as React from "react";
import { useConcent } from "concent";

const Counter = () => {
  const { state, dispatch } = useConcent("counter");
  const increase = () => dispatch("increase", 1);
  const decrease = () => dispatch("decrease", 1);

  return (
    <>
      <h1>Fn Count : {state.count}</h1>
      <button onClick={increase}>Increase</button>
      <button onClick={decrease}>decrease</button>
    </>
  );
};

export default Counter;

該函數組件咱們是按照傳統的hook風格來寫,即每次渲染執行hook函數,利用hook函數返回的基礎接口再次定義符合當前業務需求的動做函數。

可是因爲concent提供setup接口,咱們能夠利用它只會在初始渲染前執行一次的能力,將這些動做函數放置到setup內部定義爲靜態函數,避免重複定義,因此一個更好的函數組件應爲

import * as React from "react";
import { useConcent } from "concent";

export const setup = ctx => {
  return {
    // better than ctx.dispatch('increase', 1);
    increase: () => ctx.moduleReducer.increase(1),
    decrease: () => ctx.moduleReducer.decrease(1)
  };
};

const CounterBetter = () => {
  const { state, settings } = useConcent({ module: "counter", setup });
  const { increase, decrease } = settings;
  // return ui...
};

export default CounterBetter;

建立一個類組件,複用setup裏的邏輯

import React from "react";
import { register } from "concent";
import { setup } from './CounterFn';

@register({module:'counter', setup})
class Counter extends React.Component {
  render() {
    // this.state 和 this.ctx.state 取值效果是同樣的
    const { state, settings } = this.ctx;
     // return ui...
  }
}

export default Counter;

渲染這兩個counter,查看concent示例

function App() {
  return (
    <div className="App">
      <CounterCls />
      <CounterFn />
    </div>
  );
}

回顧與總結

此回合裏展現了3個框架對定義多模塊狀態時,不一樣的代碼組織與結構

  • redux經過combineReducers配合Provider包裹根組件,同時還收手寫mapStateToPropsmapActionToProps來輔助組件獲取store的數據和方法
  • mobx經過合併多個subStore到一個store對象並配合Provider包裹根組件,store的數據和方法可直接獲取
  • concent經過run接口集中配置或者configure接口分離式的配置,store的數據和方法可直接獲取
store配置 concent mbox redux
支持分離 Yes Yes No
無根Provider & 使用處無需顯式導入 Yes No No
reducer無this Yes No Yes
store數據或方法無需人工映射到組件 Yes Yes No

round 2 - 狀態修改

3個框架對狀態的修改風格差別較大。
redux裏嚴格限制狀態修改途徑,因此的修改狀態行爲都必須派發action,而後命中相應reducer合成新的狀態。

mobx具備響應式的能力,直接修改便可,但所以也帶來了數據修改途徑不可追溯的煩惱從而產生了mobx-state-tree來配套約束脩改數據行爲。

concent的修改完徹底全遵循react的修改入口setState風格,在此基礎之上進而封裝dispatchinvokesync系列api,且不管是調用哪種api,都可以不僅是追溯數據修改完整鏈路,還包括觸發數據修改的源頭。

redux(dispatch)

同步的action

export const changeFirstName = firstName => {
  return {
    type: CHANGE_FIRST_NAME,
    payload: firstName
  };
};

異步的action,藉助redux-thunk來完成

// code in models/index.js, 配置thunk中間件
import  thunk  from "redux-thunk";
import { createStore, combineReducers, applyMiddleware } from "redux";
const store = createStore(combineReducers({...}), applyMiddleware(thunk));

// code in models/login/action.js
export const CHANGE_FIRST_NAME = "CHANGE_FIRST_NAME";

const delay = (ms = 1000) => new Promise(r => setTimeout(r, ms));
// 工具函數,輔助寫異步action
const asyncAction = asyncFn => {
  return dispatch => {
    asyncFn(dispatch).then(ret => {
      if(ret){
        const [type, payload] = ret;
        dispatch({ type, payload });
      }
    }).catch(err=>alert(err));
  };
};

export const asyncChangeFirstName = firstName => {
  return asyncAction(async (dispatch) => {//可用於中間過程屢次dispatch
    await delay();
    return [CHANGE_FIRST_NAME, firstName];
  });
};

mobx版本(this.XXX)

同步action與異步action

import { observable, action, computed } from "mobx";

const delay = (ms = 1000) => new Promise(r => setTimeout(r, ms));

class LoginStore {
  @observable firstName = "";

  @observable lastName = "";

  @action.bound
  changeFirstName(firstName) {
    this.firstName = firstName;
  }

  @action.bound
  async asyncChangeFirstName(firstName) {
    await delay();
    this.firstName = firstName;
  }

  @action.bound
  changeLastName(lastName) {
    this.lastName = lastName;
  }
}

export default new LoginStore();

直接修改

const LoginFn = () => {
  const { login } = store;
  const changeFirstName = e => login.firstName = e.target.value;
  // ...    
}

經過action修改

const LoginFn = () => {
  const { login } = store;
  const const changeFirstName = e => login.changeFirstName(e.target.value);
  // ...    
}

concent(dispatch,setState,invoke,sync)

concent裏再也不區分actionreducer,ui直接調用reducer方法便可,同時reducer方法能夠是同步也能夠是異步,支持相互任意組合和lazy調用,大大減輕開發者的心智負擔。

同步reducer與異步reducer

// code in models/login/reducer.js
const delay = (ms = 1000) => new Promise(r => setTimeout(r, ms));

export function changeFirstName(firstName) {
  return { firstName };
}

export async function asyncChangeFirstName(firstName) {
  await delay();
  return { firstName };
}

export function changeLastName(lastName) {
  return { lastName };
}

可任意組合的reducer,屬於同一個模塊內的方法能夠直接基於方法引用調用,且reducer函數並不是強制必定要返回一個新的片段狀態,僅用於組合其餘reducer也是能夠的。

// reducerFn(payload:any, moduleState:{}, actionCtx:IActionCtx)
// 當lazy調用此函數時,任何一個函數出錯了,中間過程產生的全部狀態都不會提交到store
export async changeFirstNameAndLastName([firstName, lastName], m, ac){
    await ac.dispatch(changeFirstName, firstName);
    await ac.dispatch(changeFirstName, lastName);
    // return {someNew:'xxx'};//可選擇此reducer也返回新的片段狀態
}

// 視圖處
function UI(){
    const ctx useConcent('login');
    // 觸發兩次渲染
    const normalCall = ()=>ctx.mr.changeFirstNameAndLastName(['first', 'last']);
    // 觸發一次渲染
    const lazyCall = ()=>ctx.mr.changeFirstNameAndLastName(['first', 'last'], {lazy:true});
    
    return (
        <>
            <button onClick={handleClick}> normalCall </button>
            <button onClick={handleClick}> lazyCall </button>
        </>
    )
}

lazyReducer示例

非lazy調用流程

lazy調用流程

固然了,除了reducer,其餘3種方式均可以任意搭配,且和reducer同樣擁有同步狀態到其餘屬於同一個模塊且對某狀態有依賴的實例上

  • setState
function FnUI(){
    const {setState} = useConcent('login');
    const changeName = e=> setState({firstName:e.target.name});
    // ... return ui
}

@register('login')
class ClsUI extends React.Component{
    changeName = e=> this.setState({firstName:e.target.name})
    render(){...}
}
  • invoke
function _changeName(firstName){
    return {firstName};
}

function FnUI(){
    const {invoke} = useConcent('login');
    const changeName = e=> invoke(_changeName, e.target.name);
    // ... return ui
}

@register('login')
class ClsUI extends React.Component{
    changeName = e=> this.ctx.invoke(_changeName, e.target.name)
    render(){...}
}
  • sync

更多關於sync, 查看App2-1-sync.js文件

function FnUI(){
    const {sync, state} = useConcent('login');
    return  <input value={state.firstName} onChange={sync('firstName')} />
}

@register('login')
class ClsUI extends React.Component{
    changeName = e=> this.ctx.invoke(_changeName, e.target.name)
    render(){
        return  <input value={this.state.firstName} onChange={this.ctx.sync('firstName')} />
    }
}

還記得咱們在round 2開始比較前對concent提到了這樣一句話:可以不僅是追溯數據修改完整鏈路,還包括觸發數據修改的源頭,它是何含義呢,由於每個concent組件的ctx都擁有一個惟一idccUniqueKey標識當前組件實例,它是按{className}_{randomTag}_{seq}自動生成的,即類名(不提供是就是組件類型$$CClass`, `$$CCFrag, $$CCHook)加隨機標籤加自增序號,若是想刻意追蹤修改源頭ui,則人工維護tagccClassKey既可,再配合上concent-plugin-redux-devtool就能完成咱們的目標了。

function FnUI(){
    const {sync, state, ccUniqueKey} = useConcent({module:'login', tag:'xxx'}, 'FnUI');
    // tag 可加可不加,
    // 不加tag,ccUniqueKey形如: FnUI_xtst4x_1
    // 加了tag,ccUniqueKey形如: FnUI_xxx_1
}

@register({module:'login', tag:'yyy'}, 'ClsUI')
class ClsUI extends React.Component{...}

接入concent-plugin-redux-devtool後,能夠看到任何動做修改Action裏都會包含一個字段ccUniqueKey

回顧與總結

這一個回合咱們針對數據修改方式作了全面對比,從而讓開發者瞭解到從concent的角度來講,爲了開發者的編碼體驗作出的各方面巨大努力。

針對狀態更新方式, 對比redux,當咱們的全部動做流程壓到最短,無action-->reducer這樣一條鏈路,無所謂的存函數仍是反作用函數的區分(rematchdva等提取的概念),把這些概念交給js語法自己,會顯得更加方便和清晰,你須要純函數,就寫export function,須要反作用函數就寫export async function

對比mobx,一切都是能夠任何拆開任意組合的基礎函數,沒有this,完全得面向FP,給一個input預期output,這樣的方式對測試容器也更加友好。

狀態修改 concent mbox redux
基於不可變原則 Yes No Yes
最短鏈路 Yes Yes No
ui源頭可追蹤 Yes No No
無this Yes No Yes
原子拆分&合併提交 Yes(基於lazy) Yes(基於transaction) No

round 3 - 依賴收集

這個回合是很是重量級的一個環節,依賴收集讓ui渲染能夠保持最小範圍更新,即精確更新,因此vue某些測試方面會勝出react,當咱們爲react插上依賴收集的翅膀後,看看會有什麼更有趣的事情發生吧。

再開始聊依賴收集以前,咱們覆盤一下react本來的渲染機制吧,當某一個組件發生狀態改變時,若是它的自定義組件沒有人工維護shouldComponentUpdate判斷時,老是會從上往下所有渲染一遍,而reduxcconnect接口接管了shouldComponentUpdate行爲,當一個action觸發了動做修改時,全部connect過的組件都會將上一刻mapStateToProps獲得的狀態和當前最新mapStateToProps獲得的狀態作淺比較,從而決定是否要刷新包裹的子組件。

到了hook時代,提供了React.memo來用戶阻斷這種"株連式"的更新,可是須要用戶儘可能傳遞primitive類型數據或者不變化的引用給props,不然React.memo的淺比較會返回false。

可是redux存在的一個問題是,若是視圖裏某一刻已經再也不使用某個狀態了,它不應被渲染卻被渲染了,mobx攜帶得基於運行時獲取到ui對數據的最小訂閱子集理念優雅的解決了這個問題,可是concent更近一步將依賴收集行爲隱藏的更優雅,用戶不須要不知道observable等相關術語和概念,某一次渲染你取值有了點這個值的依賴,而下一次渲染沒有了對某個stateKey的取值行爲就應該移出依賴,這一點vue作得很好,爲了讓react擁有更優雅、更全面的依賴收集機制,concent一樣作出了不少努力。

redux版本(不支持)

解決依賴收集不是redux誕生的初衷,這裏咱們只能默默的將它請到候選區,參與下一輪的較量了。

mobx版本(computed,useObserver)

利用裝飾器或者decorate函數標記要觀察的屬性或者計算的屬性

import { observable, action, computed } from "mobx";

const delay = (ms = 1000) => new Promise(r => setTimeout(r, ms));

class LoginStore {
  @observable firstName = "";

  @observable lastName = "";

  @computed
  get fullName(){
    return `${this.firstName}_${this.lastName}`
  }

  @computed
  get nickName(){
    return `${this.firstName}>>nicknick`
  }

  @computed
  get anotherNickName(){
    return `${this.nickName}_another`
  }
}

export default new LoginStore();

ui裏使用了觀察狀態或者結算結果時,就產生了依賴

  • 僅對計算結果有依賴,類組件寫法
@inject("store")
@observer
class LoginCls extends Component {
  state = {show:true};
  toggle = ()=> this.setState({show:!this.state.show})
  render() {
    const login = this.props.store.login;
    return (
      <>
        <h1>Cls Small Comp</h1>
        <button onClick={this.toggle}>toggle</button>
        {this.state.show ? <div> fullName:{login.fullName}</div>: ""}
      </>
    )
  }
}
  • 僅對計算結果有依賴,函數組件寫法
import { useObserver } from "mobx-react";

// show爲true時,當前組件讀取了fullName,
// fullName由firstName和lastName計算而出
// 因此他的依賴是firstName、lastName
// 當show爲false時,當前組件無任何依賴
export const LoginFnSmall = React.memo((props) => {
  const [show, setShow] = React.useState(true);
  const toggle = () => setShow(!show);
  const { login } = store;

  return useObserver(() => {
    return (
      <>
        <h1>Fn Small Comp</h1>
        <button onClick={toggle}>toggle</button>
        {show ? <div> fullName:{login.fullName}</div>: ""}
      </>
    )
  });
});

對狀態有依賴和對計算結果有依賴無任何區別,都是在運行時從this.props.login上獲取相關結果就產生了ui對數據的依賴關係。

查看mobx示例

concent(state,moduleComputed)

無需任何裝飾器來標記觀察屬性和計算結果,僅僅是普通的json對象和函數,運行時階段被自動轉爲Proxy對象。

計算結果依賴

// code in models/login/computed.js
// n: newState, o: oldState, f: fnCtx

// fullName的依賴是firstName lastName
export function fullName(n, o, f){
  return `${n.firstName}_${n.lastName}`;
}

// nickName的依賴是firstName
export function nickName(n, o, f){
  return `${n.firstName}>>nicknick`
}

// anotherNickName基於nickName緩存結果作二次計算,而nickName的依賴是firstName
// 因此anotherNickName的依賴是firstName,注意需將此函數放置到nickName下面
export function anotherNickName(n, o, f){
  return `${f.cuVal.nickName}_another`;
}
  • 僅對計算結果有依賴,類組件寫法
@register({ module: "login" })
class _LoginClsSmall extends React.Component {
  state = {show:true};
  render() {
    const { state, moduleComputed: mcu, syncBool } = this.ctx;

    // show爲true時實例的依賴爲firstName+lastName
    // 爲false時,則無任何依賴
    return (
      <>
        <h1>Fn Small Comp</h1>
        <button onClick={syncBool("show")}>toggle</button>
        {state.show ? <div> fullName:{mcu.fullName}</div> : ""}
      </>
    );
  }
}
  • 僅對計算結果有依賴,函數組件寫法
export const LoginFnSmall = React.memo(props => {
  const { state, moduleComputed: mcu, syncBool } = useConcent({
    module: "login",
    state: { show: true }
  });

  return (
    <>
      <h1>Fn Small Comp</h1>
      <button onClick={syncBool("show")}>toggle</button>
      {state.show ? <div> fullName:{mcu.fullName}</div> : ""}
    </>
  );
});

mobx同樣,對狀態有依賴和對計算結果有依賴無任何區別,在運行時從ctx.state上獲取相關結果就產生了ui對數據的依賴關係,每一次渲染concent都在動態的收集當前實例最新的依賴,在實例didUpdate階段移出已消失的依賴。

  • 生命週期依賴

concent的架構裏是統一了類組件和函數組件的生命週期函數的,因此當某個狀態被改變時,對此有依賴的生命週期函數會被觸發,並支持類與函數共享此邏輯

export const setupSm = ctx=>{
  // 當firstName改變時,組件渲染渲染完畢後會觸發
  ctx.effect(()=>{
    console.log('fisrtName changed', ctx.state.fisrtName);
  }, ['firstName'])
}

// 類組件裏使用
export const LoginFnSmall = React.memo(props => {
  console.log('Fn Comp ' + props.tag);
  const { state, moduleComputed: mcu, sync } = useConcent({
    module: "login",setup: setupSm, state: { show: true }
  });
  //...
}

// 函數組件裏使用
@register({ module: "login", setup:setupSm })
class _LoginClsSmall extends React.Component {...}

查看concent示例

查看更多關於ctx.effect

回顧與總結

在依賴收集這一個回合,concent的依賴收集形式、和組件表達形式,和mobx區別都很是大,整個依賴收集過程沒有任何其餘多餘的api介入, 而mbox需用computed修飾getter字段,在函數組件須要使用useObserver包狀態返回UI,concent更注重一切皆函數,在組織計算代碼的過程當中消除的this這個關鍵字,利用fnCtx函數上下文傳遞已計算結果,同時顯式的區分statecomputed的盛放容器對象。

依賴收集 concent mbox redux
支持運行時收集依賴 Yes Yes No
精準渲染 Yes Yes No
無this Yes No No
只需一個api介入 Yes No No

round 4 - 衍生數據

還記得mobx的口號嗎?任何能夠從應用程序狀態派生的內容都應該派生,揭示了一個的的確確存在且咱們沒法逃避的問題,大多數應用狀態傳遞給ui使用前都會伴隨着一個計算過程,其計算結果咱們稱之爲衍生數據。

咱們都知道在vue裏已內置了這個概念,暴露了一個可選項computed用於處理計算過程並緩存衍生數據,react並沒有此概念,redux也並不提供此能力,可是redux開放的中間件機制讓社區得以找到切入點支持此能力,因此此處咱們針對redux說到的計算指的已成爲事實上的流行標準庫reslect.

mobxconcent都自帶計算支持,咱們在上面的依賴收集回合裏已經演示了mobxconcent的衍生數據代碼,因此此輪僅針對redux書寫衍生數據示例

redux(reselect)

redux最新發布v7版本,暴露了兩個api,useDispatchuseSelector,用法以以前的mapStateToStatemapDispatchToProps徹底對等,咱們的示例裏會用類組件和函數組件都演示出來。

定義selector

import { createSelector } from "reselect";

// getter,僅用於取值,不參與計算
const getFirstName = state => state.login.firstName;
const getLastName = state => state.login.lastName;

// selector,等同於computed,手動傳入計算依賴關係
export const selectFullName = createSelector(
  [getFirstName, getLastName],
  (firstName, lastName) => `${firstName}_${lastName}`
);

export const selectNickName = createSelector(
  [getFirstName],
  (firstName) => `${firstName}>>nicknick`
);

export const selectAnotherNickName = createSelector(
  [selectNickName],
  (nickname) => `${nickname}_another`
);

類組件獲取selector

import React from "react";
import { connect } from "react-redux";
import * as loginAction from "models/login/action";
import {
  selectFullName,
  selectNickName,
  selectAnotherNickName
} from "models/login/selector";

@connect(
  state => ({
    firstName: state.login.firstName,
    lastName: state.login.lastName,
    fullName: selectFullName(state),
    nickName: selectNickName(state),
    anotherNickName: selectAnotherNickName(state),
  }), // mapStateToProps
  dispatch => ({
    // mapDispatchToProps
    changeFirstName: e =>
      dispatch(loginAction.changeFirstName(e.target.value)),
    asyncChangeFirstName: e =>
      dispatch(loginAction.asyncChangeFirstName(e.target.value)),
    changeLastName: e => dispatch(loginAction.changeLastName(e.target.value))
  })
)
class Counter extends React.Component {
  render() {
    const {
      firstName,
      lastName,
      fullName,
      nickName,
      anotherNickName,
      changeFirstName,
      asyncChangeFirstName,
      changeLastName
    } = this.props;
    return 'ui ...'
  }
}

export default Counter;

函數組件獲取selector

import * as React from "react";
import { useSelector, useDispatch } from "react-redux";
import * as loginAction from "models/login/action";
import {
  selectFullName,
  selectNickName,
  selectAnotherNickName
} from "models/login/selector";

const Counter = () => {
  const { firstName, lastName } = useSelector(state => state.login);
  const fullName = useSelector(selectFullName);
  const nickName = useSelector(selectNickName);
  const anotherNickName = useSelector(selectAnotherNickName);
  const dispatch = useDispatch();
  const changeFirstName = (e) => dispatch(loginAction.changeFirstName(e.target.value));
  const asyncChangeFirstName = (e) => dispatch(loginAction.asyncChangeFirstName(e.target.value));
  const changeLastName = (e) => dispatch(loginAction.changeLastName(e.target.value));

  return 'ui...'
  );
};

export default Counter;

redux衍生數據在線示例

mobx(computed裝飾器)

見上面依賴收集的實例代碼,此處再也不重敘。

concent(moduleComputed直接獲取)

見上面依賴收集的實例代碼,此處再也不重敘。

回顧與總結

相比mobx能夠直接從this.pops.someStore獲取,concent能夠直接從ctx.moduleComputed上獲取,多了一個手動維護計算依賴的過程或映射挑選結果的過程,相信哪一種方式是開發者更願意使用的這個結果已經一目瞭然了。

衍生數據 concent mbox redux(reselect)
自動維護計算結果之間的依賴 Yes Yes No
觸發讀取計算結果時收集依賴 Yes Yes No
計算函數無this Yes No Yes

round 5 - 實戰TodoMvc

上面4個回合結合了一個個鮮活的代碼示例,綜述了3個框架的特色與編碼風格,相信讀者指望能有更加接近生產環境的代碼示例來看出其差別性吧,那麼最後讓咱們以TodoMvc來收尾此次特性大比拼,期待你可以更多的瞭解並體驗concent,開啓 不可變 & 依賴收集 的react編程之旅吧。

redux-todo-mvc

查看redux-todo-mvc演示

action 相關

reducer 相關

computed 相關

mobx-todo-mvc

查看mobx-todo-mvc演示

action 相關

computed 相關

concent-todo-mvc

查看concent-todo-mvc演示

reducer相關

computed相關

end

最後讓咱們用一個最簡版本的concent應用結束此文,將來的你會選擇concent做爲你的react開發武器嗎?

import React from "react";
import "./styles.css";
import { run, useConcent, defWatch } from 'concent';

run({
  login:{
    state:{
      name:'c2',
      addr:'bj',
      info:{
        sex: '1',
        grade: '19',
      }
    },
    reducer:{
      selectSex(sex, moduleState){
        const info = moduleState.info;
        info.sex = sex;
        return {info};
      }
    },
    computed: {
      funnyName(newState){
        // 收集到funnyName對應的依賴是 name
        return `${newState.name}_${Date.now()}`
      },
      otherFunnyName(newState, oldState, fnCtx){
        // 獲取了funnyName的計算結果和newState.addr做爲輸入再次計算
        // 因此這裏收集到otherFunnyName對應的依賴是 name addr
        return `${fnCtx.cuVal.funnyName}_${newState.addr}`
      }
    },
    watch:{
      // watchKey name和stateKey同名,默認監聽name變化
      name(newState, oldState){
        console.log(`name changed from ${newState.name} to ${oldState.name}`);
      },
      // 從newState 讀取了addr, info兩個屬性的值,當前watch函數的依賴是 addr, info
      // 它們任意一個發生變化時,都會觸發此watch函數
      addrOrInfoChanged: defWatch((newState, oldState, fnCtx)=>{
        const {addr, info} = newState;
        if(fnCtx.isFirstCall)return;// 僅爲了收集到依賴,不執行邏輯
        console.log(`addr is${addr}, info is${JSON.stringify(info)}`);
      }, {immediate:true})
    }
  }
})

function UI(){
  console.log('UI with state value');
  const {state, sync, dispatch} = useConcent('login');
  return (
    <div>
      name:<input value={state.name} onChange={sync('name')} />
      addr:<input value={state.addr} onChange={sync('addr')} />
      <br />
      info.sex:<input value={state.info.sex} onChange={sync('info.sex')} />
      info.grade:<input value={state.info.grade} onChange={sync('info.grade')} />
      <br />
      <select value={state.info.sex} onChange={(e)=>dispatch('selectSex', e.target.value)}>
        <option value="male">male</option>
        <option value="female">female</option>
      </select>
    </div>
  );
}

function UI2(){
  console.log('UI2 with comptued value');
  const {state, moduleComputed, syncBool} = useConcent({module:'login', state:{show:true}});
  return (
    <div>
      {/* 當show爲true的時候,當前組件的依賴是funnyName對應的依賴 name */}
      {state.show? <span>dep is name: {moduleComputed.funnyName}</span> : 'UI2 no deps now'}
      <br/><button onClick={syncBool('show')}>toggle show</button>
    </div>
  );
}

function UI3(){
  console.log('UI3 with comptued value');
  const {state, moduleComputed, syncBool} = useConcent({module:'login', state:{show:true}});
  return (
    <div>
      {/* 當show爲true的時候,當前組件的依賴是funnyName對應的依賴 name addr */}
      {state.show? <span>dep is name,addr: {moduleComputed.otherFunnyName}</span> : 'UI3 no deps now'}
      <br/><button onClick={syncBool('show')}>toggle show</button>
    </div>
  );
}

export default function App() {
  return (
    <div className="App">
      <h3>try click toggle btn and open console to see render log</h3>
      <UI />
      <UI />
      <UI2 />
      <UI3 />
    </div>
  );
}

❤ star me if you like concent ^_^

Edit on CodeSandbox
https://codesandbox.io/s/concent-guide-xvcej

Edit on StackBlitz
https://stackblitz.com/edit/cc-multi-ways-to-wirte-code

若是有關於concent的疑問,能夠掃碼加羣諮詢,會盡力答疑解惑,幫助你瞭解更多。

相關文章
相關標籤/搜索