concent v2
版本的发布了,在保留了和v1
如出一辙的api使用方式上,内置了依赖收集系统,支持同时从状态、计算结果和反作用3个维度收集依赖,创建其精确更新路径,固然也保留了v1
的依赖标记特性,支持用户按需选择是让系统自动收集依赖仍是人工管理依赖,大多数场景,推荐使用自动收集依赖,除非很是在乎渲染期间自动收集和更新依赖的那一点微弱的额外计算以及很是清楚本身组件对状态的依赖关系,那么能够降级为人工标记依赖,固然了,若是是v1
版本,那就没得选了,只能是人工标记了。html
在正式了解依赖收集以前,咱们先会细聊一下组件编程体验统一这个话题,本质来讲concent
并无刻意的要统一类组件和函数组件的编码方式,只是基于为组件实例注入标记了元数据的实例上下文ref ctx
的核心运行机制,随着迭代的进行,发现了组件的形态已再也不那么重要,它们表达的都是react vdom
,并最终会被react-dom
转换成的真实的html dom
渲染到浏览器窗口里,react开发者针对hook
也说过,hook
并无改变react的本质,只是换了一种编码方式书写组件而已,包括状态的定义和生命周期的定义,均可以在类组件和函数组件的不一样表达代码里一一映射。vue
class ClassComp extends React.Component{
constructor(props, context){
super(props, context);
this.state = {tag:props.tag, name:''}
}
}
function FnComp(props){
const propsTag = props.tag;
const [tag, setTag] = useState(propsTag);
const [name, setName] = useState('');
}
复制代码
class ClassComp extends React.Component{
componentDidMount(){
//组件初次挂载触发
}
}
function FnComp(){
React.useEffect(()=>{
//组件初次挂载触发
}, [])
}
复制代码
class ClassComp extends React.Component{
componentDidUpdate(){
//组件存在期渲染完毕触发
}
}
function FnComp(){
const efFlag = React.useRef(0);
React.useEffect(()=>{
efFlag.current++;
if(efFlag.current>1){
//组件存在期渲染完毕触发
}
})
}
复制代码
class ClassComp extends React.Component{
componentWillUnmount(){
//组件卸载前触发
}
}
function FnComp(){
React.useEffect(()=>{
return ()=>{
//组件卸载前触发
}
}, [])
}
复制代码
class SomePage extends Component{
static getDerivedStateFromProps (props, state) {
if (props.tag !== state.tag) return {tag: props.tag}
return null
}
}
function FnComp(props){
const propsTag = props.tag;
const [tag, setTag] = useState(propsTag);
React.useEffect(()=>{
// 首次渲染时,此反作用仍是会执行的,在内部巧妙的再比较一次,避免一次多余的ui更新
// 等价于上面组件类里getDerivedStateFromProps里的逻辑
if(tag !== propsTag)setTag(tag);
}, [propsTag, tag]);
}
复制代码
既然他们本质上只是表达方式的不一样,concent
经过setup
只在组件初次渲染前执行一次的特性,开辟另外一个空间,完美和谐的统一他们的表达方式,而且还顺带额外提供其余可选的特性给开发者使用。react
这里提早申明一下,下面的代码演示setup特性以及相关生命周期统一的函数是都是可选的,并不是必定要这样编码才能接入concent,你依然能够按照最传统的方式组织代码,使用setState就能够了git
如下举一个实战例子:es6
const api = {
async fetchProducts() {
return {
products: [
{name:'name_'+Math.random(), author:'zzk_invoke'},
{name:'name_'+Math.random(), author:'concent_invoke'},
]
};
}
};
export const setup = ctx => {
//初始化props.tag到state里,initState会自动作合并
ctx.initState({ tag: ctx.props.tag });
const fetchProducts = () => {
const { type, sex, addr, keyword } = ctx.state;
api.fetchProducts({ type, sex, addr, keyword })
.then(({products}) => ctx.setState({ products }))
.catch(err => alert(err.message));
};
ctx.effect(() => {
fetchProducts();
}, ["type", "sex", "addr", "keyword"]);
/** 原函数组件内写法: useEffect(() => { fetchProducts(type, sex, addr, keyword); }, [type, sex, addr, keyword]); */
ctx.effect(() => {
return () => {
// 返回一个清理函数
// 等价于componentWillUnmout, 这里搞清理事情
};
}, []);
/** 原函数组件内写法: useEffect(()=>{ return ()=>{// 返回一个清理函数 // 等价于componentWillUnmout, 这里搞清理事情 } }, []);//第二位参数传空数组,次反作用只在初次渲染完毕后执行一次 */
ctx.effectProps(() => {
// 对props上的变动书写反作用
const curTag = ctx.props.tag;
if (curTag !== ctx.prevProps.tag) ctx.setState({ tag: curTag });
}, ["tag"]);
/** 原函数组件内写法: useEffect(()=>{ // 首次渲染时,此反作用仍是会执行的,在内部巧妙的再比较一次,避免一次多余的ui更新 // 等价于上面组件类里getDerivedStateFromProps里的逻辑 if(tag !== propTag)setTag(tag); }, [propTag, tag]); */
return {
// 返回结果收集在ctx.settings里
fetchProducts,
fetchByInfoke: () => ctx.invoke(api.fetchProducts),
//推荐使用此方式,把方法定义在settings里
changeType: ctx.sync("type")
};
};
复制代码
定义一个初始化状态函数github
export const iState = () => ({
products: [],
type: "",
sex: "",
addr: "",
keyword: "",
tag: ""
});
复制代码
如今咱们来看看组件长什么样子吧编程
import { useConcent } from 'concent';
const ConcentFnPage = React.memo(function(props) {
// useConcent返回ctx,这里直接解构ctx,拿想用的对象或方法
const { state, settings, sync } = useConcent({ setup, state: iState, props });
// 渲染须要的数据
const { products, type, sex, addr, keyword, tag } = state;
// 装配好的方法
const { fetchProducts, fetchByInfoke } = settings;
return <div>... your ui ... </div>
});
复制代码
import { register } from 'concent';
@register({ setup, module:'product' })
class ConcentFnModuleClass extends React.Component{
render(){
const { state, settings, sync } = this.ctx;
const { products, type, sex, addr, keyword, tag } = state;
const { fetchProducts, fetchByInfoke } = settings;
return <div>... your ui ... </div>
}
}
复制代码
点我查看上述示例api
经过观察发现,是否是长得如出一辙呢?惟一不一样的是实例上下文在类组件里经过this.ctx
得到,在函数组件里经过useConcent
返回,并且setup
相比传统的函数组件带来了几大优点数组
settings
里返回给用户使用,没有了每一轮渲染都生成临时闭包函数的多余消耗以及其余值捕获陷阱、useCallback
进一步封装等问题。concent
自动维护着一个上一刻状态和当前状态的引用,同构浅比较直接决定要不要触发反作用函数下面一个示例演示闭包陷阱和使用setup
后如何避免此问题,且复用在类与函数组件之间浏览器
// 这是一个普通的函数组件
function NormalDemo() {
const [count, setCount] = useState(0);
const dom = useRef(null);
useEffect(() => {
const cur = dom.current;
const add = () => setCount(count + 1);
cur.addEventListener("click", add);
return () => cur.removeEventListener("click", add);
}, [count]);//须要显示传递count值做为依赖
return <div ref={dom}>normal {count}</div>;
}
//定义一个setup函数
const setup = ctx => {
const addCount = () => {
const count = ctx.state.count;
ctx.setState({ count: count + 1 });
};
// 由于锁住了count在ctx.state里,这里不须要重复绑定和去绑定click事件了
ctx.effect(() => {
const cur = ctx.refs.dom.current;
cur.addEventListener("click", addCount);
return () => cur.removeEventListener("click", addCount);
}, []);
return { addCount };
};
function ConcentFnDemo() {
const { useRef, state, settings } = useConcent({
setup,
state: { count: 0 }
});
return (
<div> <div ref={useRef("dom")}>click me fn {state.count}</div> <button onClick={settings.addCount}>add</button> </div>
);
}
// or @register({setup, state:{count:0}})
@register({ setup })
class ConcentClassDemo extends React.Component {
state = { count: 0 };
render() {
const { useRef, state, settings } = this.ctx;
// this.ctx.state === this.state
return (
<div> <div ref={useRef("dom")}>click me class {state.count}</div> <button onClick={settings.addCount}>add</button> </div>
);
}
}
复制代码
经过上面的示例代码咱们发现,协调类组件和函数组件的共享和复用业务逻辑的方式是如此的简单与轻松,但这并非必需的,你依然能够像传统方式同样为类组件和函数组件组织代码,不过仅仅是多了一种更棒的方式提供给你罢了。
咱们已提到依赖收集,首先会想到vue
框架,依赖收集做为其核心驱动视图精确的原理,让很多react
须要人工维护shouldComponentUpdate
,useCallback
等额外api才能写出性能更好的react代码眼馋,不论是vue2
的defineProperty
和vue3
的proxy
,本质上都能隐式的收集视图对数据的依赖关系来作到精确更新。
那么concent
又怎样来实现依赖收集呢?仍是离不开咱们提到的实例上下文,它将做为咱们收集到依赖的重要媒介,来帮助咱们毫无违和感的书写具备依赖收集的react代码。
为何说毫无违和感?由于你书写的代码和原始react代码并无区别,依然保持react的味道。
咱们定义一个普通的Concent组件
run();
const iState = ()=>({firstName:'Jim', lastName:'Green'});
function NormalPerson(){
const { state, sync } = useConcent({state:iState});
return (
<div className="box">
<input value={state.firstName} onChange={sync('firstName')} />
<input value={state.lastName} onChange={sync('lastName')} />
</div>
);
}
复制代码
若是咱们渲染这两个组件的话,它们的状态是各自独立的
export default function App() {
return (
<div className="App"> <NormalPerson /> <NormalPerson /> </div>
);
}
复制代码
咱们提高一下状态,让全部示例共享 定义一个模块名为login
const iState = ()=>({firstName:'Jim', lastName:'Green'});
run(
{
login: {// 定义login模块
state: iState, // 传递状态初始化函数,固然了这里也能够传对象
}
}
);
复制代码
而后指定组件属于login
模块
function SharedPerson(){
const { state, sync } = useConcent('login');
return (
<div className="box">
<input value={state.firstName} onChange={sync('firstName')} />
<input value={state.lastName} onChange={sync('lastName')} />
</div>
);
}
复制代码
渲染它们看看效果吧
是否是提高状态是从没有感受过如此轻松惬意,无Provider
包裹根组件,仅仅只是标记模块,就完成了状态提高和共享,示例是为了方便使用sync
,若是咱们更传统一点,应该是这样的
const { state, setState } = useConcent('login');
const changeFirstName = (e)=> setState({firstName: e.target.value})
<input value={state.firstName} onChange={changeFirstName} />
复制代码
固然对于类组件也是同样的,并无任何改变你认知的react组件形态
@register('login')
class SharedPersonC extends React.Component{
changeFirstName = (e)=> this.setState({firstName: e.target.value})
render(){
const { state, sync } = this.ctx;
return (
<div className="box">
<input value={state.firstName} onChange={this.changeFirstName} />
<input value={state.lastName} onChange={sync('lastName')} />
</div>
);
}
}
复制代码
事实上this.state
上能够定义额外的key做为私有状态
@register('login')
class SharedPersonC extends React.Component{
// 由于privKey并非模块里的key,因此这个key的状态变动仅影响当前实例,
// 并不会派发到其余同属于login模块的实例
state = {privKey:'key1'}
render(){
// this.state
// {firstName:'', lastName:'', privKey:'key1'}
}
}
复制代码
若是咱们不喜欢共享状态状态合并到this.state
,那就使用connect
就行了,connect
支持传递数组意味着能够跨多个模块消费共享数据。
function SharedPerson(){
const { connectedState, sync } = useConcent({connect:['login']});
// connectedState.login.firstName
}
@register({connect:['login']})
class SharedPersonC extends React.Component{
render(){
const { connectedState, sync } = this.ctx;
// connectedState.login.firstName
}
}
复制代码
铺垫了这么久,咱们说的依赖收集在哪里,体如今何处,不要慌,咱们给组件加个开关,控制firstName
和lastName
是否显示
const spState = () => ({ showF: true, showL: true });
function SharedPerson() {
const { state, sync, syncBool } = useConcent({
module: "login",
state: spState
});
return (
<div className="box">
{state.showF ? (
<input value={state.firstName} onChange={sync("firstName")} />
) : (
""
)}
{state.showL ? (
<input value={state.lastName} onChange={sync("lastName")} />
) : (
""
)}
<br />
<button onClick={syncBool('showF')}>toggle showF</button>
<button onClick={syncBool('showL')}>toggle showL</button>
</div>
);
}
复制代码
若是咱们实例化2个实例,将第一个showF
值置为false,意味着视图里再也不有读取state.firstName
的行为,那么当前组件的依赖列表里仅有lastName
一个字段了,咱们在另外一个组件实例里对lastName
输入新内容时,会触发第一个实例渲染,可是对firstName
输入新内容时不该该触发第一个实例渲染,如今咱们看看效果吧。
固然了用户必定会有一个疑问,实例1不触发更新,那么当我须要用这个firstName
时,是否是已通过期了,的确,若是你切换实例1的showF
为true,stata.firstName
会拿到最新的值渲染,可是若是你不切换,而是直接点击实例1的某个按钮直接用firstName
做业务逻辑处理的话,从state.firstName
取到的的确是旧值,你只需从ctx.moduleState
上去取就解决了,取到的值必定是最新值,由于全部属于login
模块的实例的moduleState
指向的是同一个对象,固然就不存在值过时的问题,固然你能够一开始在视图里使用模块数据时,就从moduleState
里取(同样能收集到依赖),而不是从合并后的state
上取,就不会形成渲染逻辑从state
取而业务逻辑从moduleState
里取同一个值的违和感了。
咱们知道concent是支持定义计算函数的,分为实例级别的计算和模块级别的计算,咱们一个个来讲
首先咱们经过setup
一次性定义好实例计算函数,而后交给useConcent
const setup = ctx=>{
ctx.computed('fullName', (newState, oldState)=>{
return `${newState.firstName}_${newState.lastName}`;
})
}
const spState = () => ({ showF: true, showL: true });
function SharedPerson() {
// 从refComputed取实例计算结果
const { state, sync, ccUniqueKey, syncBool, refComputed } = useConcent({
module: "login",
state: spState,
setup,
});
console.log(`%c${ccUniqueKey}`, "color:green");
return (
<div className="box">
{state.showF ? (
<input value={state.firstName} onChange={sync("firstName")} />
) : (
""
)}
{state.showL ? (
<input value={state.lastName} onChange={sync("lastName")} />
) : (
""
)}
<br />
{/** 此处渲染实例计算结果 */}
fullName: {refComputed.fullName}
<br />
<button onClick={syncBool('showF')}>toggle showF</button>
<button onClick={syncBool('showL')}>toggle showL</button>
</div>
);
}
复制代码
接下来咱们要说此处有趣的事了,咱们依然渲染两个实例,当咱们点击第一个实例toggle showF
按钮设置showF
为false,可是注意哦,实例1的读取了refComputed.fullName
,而这个值是经过${newState.firstName}_${newState.lastName}
计算出来的数据,因此尽管视图不显示firstName
了,可是当前实例的依赖列表依然为firstName, lastName
,因此咱们在实例2里输入firstName
,依然能触发实例1渲染
run({
login: {
// 定义login模块
state: iState, // 传递状态初始化函数,固然了这里也能够传对象
computed:{
fullName(newState, oldState){
return `${newState.firstName}_${newState.lastName}`;
}
}
}
});
复制代码
如今咱们的组件代码仅需将refComputed.fullName
改成moduleComputed.fullName
便可
// const { state, sync, ccUniqueKey, syncBool, refComputed } = useConcent({
// 改成从moduleComputed取实例计算结果
const { state, sync, ccUniqueKey, syncBool, moduleComputed } = useConcent({
module: "login",
state: spState,
setup,
});
复制代码
让咱们看看效果吧
还记得开文里咱们说组件编程体验统一里提到的ctx.effect
吗,埋了这么久的伏笔,在这里终于要排上用场了,ctx.effect
的执行时机是组件渲染完毕,检查依赖列表里是否有变化从而决定是否要触发反作用函数。
在这里咱们简单定义一个反作用即firstName
发生变化时打印一句话。
const setup2 = ctx=>{
ctx.effect(()=>{
console.log('firstName changed');
}, ['firstName']);
}
复制代码
嘿嘿,接下来咱们声明一个空组件并将其传给它
function EmptyPerson() {
console.log('render EmptyPerson');
useConcent({module:'login', setup:setup2});
return <h1>EmptyPerson</h1>
}
复制代码
这个组件仅仅是标记它属于login
模块,可是咱们并无读取任何模块状态用于渲染,只不过在setup
里定义了一个反作用,依赖列表里有firstName
,因此当咱们把EmptyPerson
和SharedPerson
放一块儿实例化后,当咱们在SharedPerson
实例里输入firstName
新内容时,会触发EmptyPerson
渲染并触发它的反作用函数。
随着再也不考虑古老的浏览器支持,拥抱es6新特性后,v2的concent
已携带一整套完整的方案,可以渐进式的开发react组件,即不干扰react自己的开发哲学和组件形态,同时也可以得到巨大的性能收益,这意味着咱们能够至下而上的增量式的迭代,状态模块的划分,派生数据的管理,事件模型的分类,业务代码的分隔均可以逐步在开发过程勾勒和剥离出来,其过程是丝滑柔顺的,也容许咱们至上而下统筹式的开发,一开始吧全部的领域模型和业务模块抽象的清清楚楚,同时在迭代过程当中也能很是快速的灵活调整而影响整个项目架构,指望读到此文的你可以了解到concent
在依赖收集到所作的努力并有兴趣开始了解和试用。
某一个夜晚,我作了个梦,发现基于现有的concent
运行机制,加以适当的约束,好像可让react
和vue
之间相互转译彷佛有那么一点点可能.....
❤ star me if you like concent ^_^
若是有关于concent的疑问,能够扫码加群咨询,会尽力答疑解惑,帮助你了解更多。