❤ star me if you like concent ^_^css
redux
、mobx
自己是一個獨立的狀態管理框架,各自有本身的抽象api,以其餘UI框架無關(react, vue...),本文主要說的和react
搭配使用的對比效果,因此下文裏提到的redux
、mobx
暗含了react-redux
、mobx-react
這些讓它們可以在react
中發揮功能的綁定庫,而concent
自己是爲了react
貼身打造的開發框架,數據流管理只是做爲其中一項功能,附帶的其餘加強react開發體驗的特性能夠按需使用,後期會刨去concent
裏全部與react
相關聯的部分發布concent-core
,它的定位纔是與redux
、mobx
類似的。vue
因此其實將在本文裏登場的選手分別是react
slogan
JavaScript 狀態容器,提供可預測化的狀態管理git
設計理念
單一數據源,使用純函數修改狀態github
slogan:
簡單、可擴展的狀態管理編程
設計理念
任何能夠從應用程序狀態派生的內容都應該派生json
slogan:
可預測、0入侵、漸進式、高性能的react開發方案redux
設計理念
相信融合不可變+依賴收集的開發方式是react的將來,加強react組件特性,寫得更少,作得更多。api
介紹完三者的背景,咱們的舞臺正式交給它們,開始一輪輪角逐,看誰到最後會是你最中意的範兒?數組
如下5個較量回合實戰演示代碼較多,此處將對比結果提早告知,方便粗讀看客能夠快速瞭解。
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 |
依賴收集 | concent | mbox | redux |
---|---|---|---|
支持運行時收集依賴 | Yes | Yes | No |
精準渲染 | Yes | Yes | No |
無this | Yes | No | No |
只需一個api介入 | Yes | No | No |
衍生數據 | 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
counter做爲demo界的靚仔被無數次推上舞臺,這一次咱們依然不例外,來個counter體驗3個框架的開發套路是怎樣的,如下3個版本都使用create-react-app
建立,並以多模塊的方式來組織代碼,力求接近真實環境的代碼場景。
經過models
把按模塊把功能拆到不一樣的reducer裏,目錄結構以下
|____models # business models
| |____index.js # 暴露store
| |____counter # counter模塊相關的action、reducer
| | |____action.js
| | |____reducer.js
| |____ ... # 其餘模塊
|____CounterCls # 類組件
|____CounterFn # 函數組件
|____index.js # 應用入口文件
複製代碼
此處僅與redux的原始模板組織代碼,實際狀況可能很多開發者選擇了
rematch
,dva
等基於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;
複製代碼
上面的示例書寫了一個類組件,而針對如今火熱的hook
,redux v7
也發佈了相應的apiuseSelector
、useDispatch
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>
);
}
複製代碼
當應用存在多個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和redux同樣,存在一個全局單一的根狀態RootStore
,該根狀態下第一層key用來當作模塊命名空間,concent的一個模塊必需配置state
,剩下的reducer
、computed
、watch
、init
是可選項,能夠按需配置,若是把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的state
和reducer
// 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
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
包裹根組件,同時還收手寫mapStateToProps
和mapActionToProps
來輔助組件獲取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 |
3個框架對狀態的修改風格差別較大。 redux
裏嚴格限制狀態修改途徑,因此的修改狀態行爲都必須派發action
,而後命中相應reducer
合成新的狀態。
mobx
具備響應式的能力,直接修改便可,但所以也帶來了數據修改途徑不可追溯的煩惱從而產生了mobx-state-tree
來配套約束脩改數據行爲。
concent
的修改完徹底全遵循react
的修改入口setState
風格,在此基礎之上進而封裝dispatch
、invoke
、sync
系列api,且不管是調用哪種api,都可以不僅是追溯數據修改完整鏈路,還包括觸發數據修改的源頭。
同步的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];
});
};
複製代碼
同步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裏再也不區分action
和reducer
,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> </> ) } 複製代碼
非lazy調用流程
lazy調用流程
固然了,除了reducer
,其餘3種方式均可以任意搭配,且和reducer
同樣擁有同步狀態到其餘屬於同一個模塊且對某狀態有依賴的實例上
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(){...}
}
複製代碼
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(){...}
}
複製代碼
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,則人工維護tag
,ccClassKey
既可,再配合上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這樣一條鏈路,無所謂的存函數仍是反作用函數的區分(rematch
、dva
等提取的概念),把這些概念交給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 |
這個回合是很是重量級的一個環節,依賴收集讓ui渲染能夠保持最小範圍更新,即精確更新,因此vue
某些測試方面會勝出react
,當咱們爲react
插上依賴收集的翅膀後,看看會有什麼更有趣的事情發生吧。
再開始聊依賴收集
以前,咱們覆盤一下react
本來的渲染機制吧,當某一個組件發生狀態改變時,若是它的自定義組件沒有人工維護shouldComponentUpdate
判斷時,老是會從上往下所有渲染一遍,而redux
的cconnect
接口接管了shouldComponentUpdate
行爲,當一個action觸發了動做修改時,全部connect過的組件都會將上一刻mapStateToProps
獲得的狀態和當前最新mapStateToProps
獲得的狀態作淺比較,從而決定是否要刷新包裹的子組件。
到了hook時代,提供了React.memo
來用戶阻斷這種"株連式"的更新,可是須要用戶儘可能傳遞primitive
類型數據或者不變化的引用給props
,不然React.memo
的淺比較會返回false。
可是redux
存在的一個問題是,若是視圖裏某一刻已經再也不使用某個狀態了,它不應被渲染卻被渲染了,mobx
攜帶得基於運行時獲取到ui對數據的最小訂閱子集理念優雅的解決了這個問題,可是concent
更近一步將依賴收集行爲隱藏的更優雅,用戶不須要不知道observable
等相關術語和概念,某一次渲染你取值有了點這個值的依賴,而下一次渲染沒有了對某個stateKey
的取值行爲就應該移出依賴,這一點vue
作得很好,爲了讓react
擁有更優雅、更全面的依賴收集機制,concent
一樣作出了不少努力。
解決依賴收集不是redux
誕生的初衷,這裏咱們只能默默的將它請到候選區,參與下一輪的較量了。
利用裝飾器或者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對數據的依賴關係。
無需任何裝飾器來標記觀察屬性和計算結果,僅僅是普通的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
的依賴收集形式、和組件表達形式,和mobx
區別都很是大,整個依賴收集過程沒有任何其餘多餘的api介入, 而mbox
需用computed
修飾getter字段,在函數組件須要使用useObserver
包狀態返回UI,concent
更注重一切皆函數,在組織計算代碼的過程當中消除的this
這個關鍵字,利用fnCtx
函數上下文傳遞已計算結果,同時顯式的區分state
和computed
的盛放容器對象。
依賴收集 | concent | mbox | redux |
---|---|---|---|
支持運行時收集依賴 | Yes | Yes | No |
精準渲染 | Yes | Yes | No |
無this | Yes | No | No |
只需一個api介入 | Yes | No | No |
還記得mobx
的口號嗎?任何能夠從應用程序狀態派生的內容都應該派生,揭示了一個的的確確存在且咱們沒法逃避的問題,大多數應用狀態傳遞給ui使用前都會伴隨着一個計算過程,其計算結果咱們稱之爲衍生數據。
咱們都知道在vue
裏已內置了這個概念,暴露了一個可選項computed
用於處理計算過程並緩存衍生數據,react
並沒有此概念,redux
也並不提供此能力,可是redux
開放的中間件機制讓社區得以找到切入點支持此能力,因此此處咱們針對redux
說到的計算指的已成爲事實上的流行標準庫reslect
.
mobx
和concent
都自帶計算支持,咱們在上面的依賴收集回合裏已經演示了mobx
和concent
的衍生數據代碼,因此此輪僅針對redux
書寫衍生數據示例
redux最新發布v7
版本,暴露了兩個api,useDispatch
和useSelector
,用法以以前的mapStateToState
和mapDispatchToProps
徹底對等,咱們的示例裏會用類組件和函數組件都演示出來。
定義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;
複製代碼
見上面依賴收集的實例代碼,此處再也不重敘。
見上面依賴收集的實例代碼,此處再也不重敘。
相比mobx
能夠直接從this.pops.someStore
獲取,concent
能夠直接從ctx.moduleComputed
上獲取,多了一個手動維護計算依賴的過程或映射挑選結果的過程,相信哪一種方式是開發者更願意使用的這個結果已經一目瞭然了。
衍生數據 | concent | mbox | redux(reselect) |
---|---|---|---|
自動維護計算結果之間的依賴 | Yes | Yes | No |
觸發讀取計算結果時收集依賴 | Yes | Yes | No |
計算函數無this | Yes | No | Yes |
上面4個回合結合了一個個鮮活的代碼示例,綜述了3個框架的特色與編碼風格,相信讀者指望能有更加接近生產環境的代碼示例來看出其差別性吧,那麼最後讓咱們以TodoMvc
來收尾此次特性大比拼,期待你可以更多的瞭解並體驗concent
,開啓 不可變 & 依賴收集 的react編程之旅吧。
action 相關
reducer 相關
computed 相關
action 相關
computed 相關
reducer相關
computed相關
最後讓咱們用一個最簡版本的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 ^_^
若是有關於concent的疑問,能夠掃碼加羣諮詢,會盡力答疑解惑,幫助你瞭解更多。