作者:幻魂
转发链接:https://juejin.im/post/5e9317cfe51d4546cc26c289
来自Observer的启发
在阅读mobx文档时,为了适配最新的函数组件,除了暴露一个api名为useObserver,还发现暴露了另外一个比较有意思的组件Observer,支持更精细的控制渲染单元,大概用起来的姿势是这样的。
在细说Observer之前,我们先看看useObserver的使用套路,我们先建立一个名为login的store
import { observable, action, computed } from "mobx";
class LoginStore {
@observable firstName = 'f'
@observable lastName = 'l'
@computed
get nickName(){
return `nick_${this.firstName}_${this.lastName}`
}
@action.bound
changeFirstName(e) {
this.firstName = e.target.value;
}
@action.bound
changeLastName(e) {
this.lastName = e.target.value;
}
}
export default new LoginStore();
复制代码
紧接着实例化一个登录框函数组件,使用useObserver切出一片需要观察和渲染的组件片段
import React from "react"
import { useObserver } from "mobx-react";
import store from "./models";
const LoginBox1 = () => {
const { login } = store;
return useObserver(() => (
<div>
<div>
firstName:
<input value={login.firstName} onChange={login.changeFirstName} />
</div>
<div>
lastName:
<input value={login.lastName} onChange={login.changeLastName} />
</div>
<div>
nickName: {login.nickName}
</div>
</div>
));
};
复制代码
如果我们需要进一步切割渲染范围,改变了哪个属性的值就仅渲染与这个属性相关的视图片段,则可搭配Obserser来达到我们的"切割"目的。
import { useObserver, Observer } from "mobx-react";
const LoginBox2 = () => {
const { login } = store;
return useObserver(() => (
<div>
<Observer>
{() => (
<div>
ob firstName:
<input value={login.firstName} onChange={login.changeFirstName} />
</div>
)}
</Observer>
<Observer>
{() => (
<div>
ob lastName:
<input value={login.lastName} onChange={login.changeLastName} />
</div>
)}
</Observer>
<Observer>
{() => <div>ob nickName: {login.nickName}</div>}
</Observer>
</div>
));
};
复制代码
注意此处的Observer组件使用方式,必需在useObserver回调内部使用,渲染它们看看效果吧。
基于useConcent"切割"组件
我们知道,concent已提供的接口useConcent,可以直接注册某个函数属于某个模块
function FooComp(){
//属于login模块
const { state } = useConcent('login');
//当前视图对name有依赖
return <div>{state.name}</div>
}
复制代码
也可以组成某个函数组件连接了其他多个模块
function FooComp(){
//连接到了login模块,xxx模块
const { connectedState } = useConcent({connect:['login', 'xxx']});
//当前视图对login模块的name有依赖
return <div>{connectedState.login.name}</div>
}
复制代码
当然了也可以既属于某个模块,同时也连接到其他模块,
function FooComp(){
//属于login模块, 连接到了xxx模块,yyy模块
const { state, connectedState } = useConcent({module:'login', connect:['xxx', 'yyy']});
}
复制代码
所以只需要对useConcent做进一步的封装,即可达到支持观察与渲染最小粒度的组件单元的目的了,2.4版本里新暴露了组件Ob,就是其具体实现。
同样的我们先来创建一个login模块吧
- state
// code in models/login/state.js
export default ()=>({
firstName: "f",
lastName: "l",
});
复制代码
- computed
// code in models/login/computed.js
export function nickName(n, o, f){
return `nick_${n.firstName}_${n.lastName}`
}
复制代码
- reducer
// code in models/login/reducer.js
export function changeFirstName(e) {
return { firstName: e.target.value };
}
export function changeLastName(e) {
return { lastName: e.target.value };
}
复制代码
写一个和useObserver目的一样的组件
import * as React from "react";
import { useConcent } from "concent";
const LoginBox1 = React.memo(() => {
// mr is alias of moduleReducer
const { state, moduleComputed: mcu, mr } = useConcent("login");
return (
<>
<div>
firstName:
<input value={state.firstName} onChange={mr.changeFirstName} />
</div>
<div>
lastName:
<input value={state.lastName} onChange={mr.changeLastName} />
</div>
<div> nickName:{mcu.nickName}</div>
</>
);
});
复制代码
此组件里firstName和lastName任意一个字段的值,改变都会引起LoginBox1渲染,现在我们像Observer组件一样细粒度的控制渲染范围吧
export const LoginBox2 = React.memo(() => {
return (
<>
<h3>show Ob capability</h3>
<Ob module="login">
{([state, _, {mr}]) => (
<div>
firstName:
<input value={state.firstName} onChange={mr.changeFirstName} />
</div>
)}
</Ob>
<Ob module="login">
{([state, _, {mr}]) => (
<div>
firstName:
<input value={state.lastName} onChange={mr.changeLastName} />
</div>
)}
</Ob>
<Ob module="login">
{([_, computed]) => (
<div> nickName:{computed.nickName}</div>
)}
</Ob>
</>
);
});
复制代码
渲染它们看看效果吧
源码解读
因useConcent返回的模块状态或者计算数据,本身具有运行时收集依赖的能力,所以我们只需在源码里对useConcent做二次封装,就拥有了像Observer组件一样的提供更细粒度的观察与渲染组件的能力了。
import React from 'react';
import { useConcentForOb } from '../core/hook/use-concent';
const obView = () => 'Ob view';
export default React.memo(function (props) {
const { module, connect, classKey, render, children } = props;
if (module && connect) {
throw new Error(`module, connect can not been supplied both`);
} else if (!module && !connect) {
throw new Error(`module or connect should been supplied`);
}
const view = render || children || obView;
const register = module ? { module } : { connect };
// 设置为1,最小化ctx够造过程,仅附加状态数据,衍生数据、和reducer相关函数
register.lite = 1;
const ctx = useConcentForOb(register, classKey);
const { mr, cr, r} = ctx;
let state, computed;
if (module) {
state = ctx.moduleState;
computed = ctx.moduleComputed;
} else {
state = ctx.connectedState;
computed = ctx.connectedComputed;
}
return view([state, computed, { mr, cr, r}]);
})
复制代码
在源码里,我们对渲染函数提供状态数据,衍生数据、和reducer相关函数,方便用户按需选择,Ob组件对比Observe,有以下几点使用体验提升
- 1,无需在代码实现处人工import对应的store,实例化Ob时传入module值即可获取对应的数据
- 2,使用更自由,无需被嵌套在其他方法内(Observe必需配合useObserve)
- 3,保持和useConcent一样的使用方式,随处插拔,代码无需过多改造
动态的模块替换
useConcent允许动态的传入module或者connect参数,以此满足用户一些需要创建不同模块组件的工厂函数场景。
首先我们创建一个多模块的store吧,$global模块用于存储选择的模块
run({
counter: {
state: { count: 999 }
},
counter2: {
state: { count: 100000 }
},
$global: {
state: { mod: "counter" }
}
});
复制代码
然后书写一个函数组件,因为需要动态的传入模块值,所以我们需要先读取global模块的模块值,再传给useConcent以确定属于哪个模块。
const setup = ctx => {
console.log(
ctx.ccUniqueKey +
" setup method will been called before first render period !!"
);
ctx.effect(()=>{
return ()=>{
console.log('trigger unmount ' + ctx.state.count);
}
}, [])
return {
add: () => ctx.setState({ count: ctx.state.count + 1 }),
};
};
function SetupFnCounter() {
const {state: { mod } } = useConcent(cst.MODULE_GLOBAL);
const ctx = useConcent({ module: mod, setup });
return (
<View
tag="fn comp with useConcent&setup --"
add={ctx.settings.add}
count={ctx.state.count}
/>
);
}
复制代码
注意这里,如果组件存在期切换了新的模块,当它们改变时,会有一次实力上下文卸载和重加载的过程,比如从counter切换为counter2,那么ctx.effect的返回函数作为unmount逻辑会被触发。
既然useConcent支持模块热替换,那么Ob当然也支持了,我们书写一个切换模块的逻辑在顶层App里,同时也渲染一个Ob实例。
export default function App() {
const {
state: { mod },
setState
} = useConcent(cst.MODULE_GLOBAL);
const changeMod = () =>
setState({ mod: mod === "counter" ? "counter2" : "counter" });
return (
<div className="App">
<button onClick={changeMod}>change mod</button>
<Ob module={mod}>
{([state]) => {
console.log("render ob");
return <div>xx: {state.count}</div>;
}}
</Ob>
<SetupFnCounter />
</div>
);
}
复制代码
总结工作进度流程
来着github: https://github.com/concentjs/concent
作者:幻魂
转发链接:https://juejin.im/post/5e9317cfe51d4546cc26c289