redux 入门(细说reduxmobxconcent特性比较)
redux 入门(细说reduxmobxconcent特性比较)通过models把按模块把功能拆到不同的reducer里,目录结构如下counter作为demo界的靓仔被无数次推上舞台,这一次我们依然不例外,来个counter体验3个框架的开发套路是怎样的,以下3个版本都使用create-react-app创建,并以多模块的方式来组织代码,力求接近真实环境的代码场景。介绍完三者的背景,我们的舞台正式交给它们,开始一轮轮角逐,看谁到最后会是你最中意的范儿?以下5个较量回合实战演示代码较多,此处将对比结果提前告知,方便粗读看客可以快速了解。todo-mvc实战 redux todo-mvc mobx todo-mvc concent todo-mvc
作者:幻魂
转发链接:https://juejin.im/post/5e7c18d9e51d455c2343c7c4
序言redux、mobx本身是一个独立的状态管理框架,各自有自己的抽象api,与其他UI框架无关(react vue...),本文主要说的和react搭配使用的对比效果,所以下文里提到的redux、mobx暗含了react-redux、mobx-react这些让它们能够在react中发挥功能的绑定库,而concent本身是为了react贴身打造的开发框架,数据流管理只是作为其中一项功能,附带的其他增强react开发体验的特性可以按需使用,后期会刨去concent里所有与react相关联的部分发布concent-core,它的定位才是与redux、mobx 相似的。
所以其实将在本文里登场的选手分别是
redux & react-redux- slogan JavaScript 状态容器,提供可预测化的状态管理
- 设计理念 单一数据源,使用纯函数修改状态
- slogan: 简单、可扩展的状态管理
- 设计理念 任何可以从应用程序状态派生的内容都应该派生
- slogan: 可预测、0入侵、渐进式、高性能的react开发方案
- 设计理念 相信融合不可变 依赖收集的开发方式是react的未来,增强react组件特性,写得更少,做得更多。
介绍完三者的背景,我们的舞台正式交给它们,开始一轮轮角逐,看谁到最后会是你最中意的范儿?
结果预览以下5个较量回合实战演示代码较多,此处将对比结果提前告知,方便粗读看客可以快速了解。
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的原始模板组织代码,实际情况可能不少开发者选择了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>
);
}
复制代码
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,剩下的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
- 配置在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包裹根组件,同时还收手写mapStateToProps和mapActionToProps来辅助组件获取store的数据和方法
- mobx通过合并多个subStore到一个store对象并配合Provider包裹根组件,store的数据和方法可直接获取
- concent通过run接口集中配置或者configure接口分离式的配置,store的数据和方法可直接获取
3个框架对状态的修改风格差异较大。 redux里严格限制状态修改途径,所以的修改状态行为都必须派发action,然后命中相应reducer合成新的状态。
mobx具有响应式的能力,直接修改即可,但因此也带来了数据修改途径不可追溯的烦恼从而产生了mobx-state-tree来配套约束修改数据行为。
concent的修改完完全全遵循react的修改入口setState风格,在此基础之上进而封装dispatch、invoke、sync系列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里不再区分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>
</>
)
}
复制代码
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,则人工维护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,这样的方式对测试容器也更加友好。
round 3 - 依赖收集这个回合是非常重量级的一个环节,依赖收集让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版本(不支持)解决依赖收集不是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函数上下文传递已计算结果,同时显式的区分state和computed的盛放容器对象。
依赖收集 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.
mobx和concent都自带计算支持,我们在上面的依赖收集回合里已经演示了mobx和concent的衍生数据代码,所以此轮仅针对redux书写衍生数据示例
redux(reselect)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;
复制代码
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>
);
}
作者:幻魂
转发链接:https://juejin.im/post/5e7c18d9e51d455c2343c7c4