开源不易,感谢你的支持,❤ star me if you like concent ^_^html
以前发表了一篇文章 redux、mobx、concent特性大比拼, 看后生如何对局前辈,吸引了很多感兴趣的小伙伴入群开始了解和使用 concent,并得到了不少正向的反馈,实实在在的帮助他们提升了开发体验,群里人数虽然还不多,但你们热情高涨,技术讨论氛围浓厚,对不少新鲜技术都有保持必定的敏感度,如上个月开始逐渐被说起得愈来愈多的出自facebook的最新状态管理方案 recoil,虽然还处于实验状态,可是相必你们已经私底下开始欲欲跃试了,毕竟出生名门,有fb背书,必定会大放异彩。vue
不过当我体验完recoil后,我对其中标榜的精确更新保持了怀疑态度,有一些误导的嫌疑,这一点下文会单独分析,是否属于误导读者在读完本文后天然能够得出结论,总之本文主要是分析Concent
与Recoil
的代码风格差别性,并探讨它们对咱们未来的开发模式有何新的影响,以及思惟上须要作什么样的转变。node
目前主流的数据流方案按形态均可以划分如下这三类react
redux、和基于redux衍生的其余做品,以及相似redux思路的做品,表明做有dva、rematch等等。git
借助definePerperty和Proxy完成数据劫持,从而达到响应式编程目的的表明,类mobx的做品也有很多,如dob等。github
这里的Context指的是react自带的Context api,基于Context api打造的数据流方案一般主打轻量、易用、概览少,表明做品有unstated、constate等,大多数做品的核心代码可能不超过500行。编程
到此咱们看看Recoil
应该属于哪一类?很显然按其特征属于Context流派,那么咱们上面说的主打轻量对Recoil
并不适用了,打开其源码库发现代码并非几百行完事的,因此基于Context api
作得好用且强大就未必轻量,由此看出facebook
对Recoil
是有野心并给予厚望的。redux
咱们同时也看看Concent
属于哪一类呢?Concent
在v2
版本以后,重构数据追踪机制,启用了defineProperty和Proxy特性,得以让react应用既保留了不可变的追求,又享受到了运行时依赖收集和ui精确更新的性能提高福利,既然启用了defineProperty和Proxy,那么看起来Concent
应该属于mobx流派?api
事实上Concent
属于一种全新的流派,不依赖react的Context api,不破坏react组件自己的形态,保持追求不可变的哲学,仅在react自身的渲染调度机制之上创建一层逻辑层状态分发调度机制,defineProperty和Proxy只是用于辅助收集实例和衍生数据对模块数据的依赖,而修改数据入口仍是setState(或基于setState封装的dispatch, invoke, sync),让Concent
能够0入侵的接入react应用,真正的即插即用和无感知接入。数组
即插即用的核心原理是,Concent
自建了一个平行于react运行时的全局上下文,精心维护这模块与实例之间的归属关系,同时接管了组件实例的更新入口setState,保留原始的setState为reactSetState,全部当用户调用setState时,concent除了调用reactSetState更新当前实例ui,同时智能判断提交的状态是否也还有别的实例关心其变化,而后一并拿出来依次执行这些实例的reactSetState,进而达到了状态所有同步的目的。
咱们以经常使用的counter来举例,熟悉一下Recoil
暴露的四个高频使用的api
外部使用atom
接口,定义一个key为num
,初始值为0
的状态
const numState = atom({ key: "num", default: 0 });
外部使用selector
接口,定义一个key为numx10
,初始值是依赖numState
再次计算而获得
const numx10Val = selector({ key: "numx10", get: ({ get }) => { const num = get(numState); return num * 10; } });
selector
的get
支持定义异步函数
须要注意的点是,若是有依赖,必需先书写好依赖在开始执行异步逻辑
const delay = () => new Promise(r => setTimeout(r, 1000)); const asyncNumx10Val = selector({ key: "asyncNumx10", get: async ({ get }) => { // !!!这句话不能放在delay之下, selector须要同步的肯定依赖 const num = get(numState); await delay(); return num * 10; } });
组件里使用useRecoilState
接口,传入想要获去的状态(由atom
建立而得)
const NumView = () => { const [num, setNum] = useRecoilState(numState); const add = ()=>setNum(num+1); return ( <div> {num}<br/> <button onClick={add}>add</button> </div> ); }
组件里使用useRecoilValue
接口,传入想要获去的派生数据(由selector
建立而得),同步派生数据和异步派生数据,皆可经过此接口得到
const NumValView = () => { const numx10 = useRecoilValue(numx10Val); const asyncNumx10 = useRecoilValue(asyncNumx10Val); return ( <div> numx10 :{numx10}<br/> </div> ); };
暴露定义好的这两个组件, 查看在线示例
export default ()=>{ return ( <> <NumView /> <NumValView /> </> ); };
顶层节点包裹React.Suspense
和RecoilRoot
,前者用于配合异步计算函数须要,后者用于注入Recoil
上下文
const rootElement = document.getElementById("root"); ReactDOM.render( <React.StrictMode> <React.Suspense fallback={<div>Loading...</div>}> <RecoilRoot> <Demo /> </RecoilRoot> </React.Suspense> </React.StrictMode>, rootElement );
若是读过concent文档(还在持续建设中...),可能部分人会认为api太多,难于记住,其实大部分都是可选的语法糖,咱们以counter为例,只须要使用到如下两个api便可
运行run接口后,会生成一份concent全局上下文
如下示例咱们先脱离ui,直接完成定义状态&修改状态的目的
import { run, setState, getState } from "concent"; run({ counter: {// 声明一个counter模块 state: { num: 1 }, // 定义状态 } }); console.log(getState('counter').num);// log: 1 setState('counter', {num:10});// 修改counter模块的num值为10 console.log(getState('counter').num);// log: 10
咱们能够看到,此处和redux
很相似,须要定义一个单一的状态树,同时第一层key就引导用户将数据模块化管理起来.
上述示例中咱们直接掉一个呢setState
修改数据,可是真实的状况是数据落地前有不少同步的或者异步的业务逻辑操做,因此咱们对模块填在reducer
定义,用来声明修改数据的方法集合。
import { run, dispatch, getState } from "concent"; const delay = () => new Promise(r => setTimeout(r, 1000)); const state = () => ({ num: 1 });// 状态声明 const reducer = {// reducer声明 inc(payload, moduleState) { return { num: moduleState.num + 1 }; }, async asyncInc(payload, moduleState) { await delay(); return { num: moduleState.num + 1 }; } }; run({ counter: { state, reducer } });
而后咱们用dispatch
来触发修改状态的方法
因dispatch会返回一个Promise,因此咱们须要用一个async 包裹起来执行代码
import { dispatch } from "concent"; (async ()=>{ console.log(getState("counter").num);// log 1 await dispatch("counter/inc");// 同步修改 console.log(getState("counter").num);// log 2 await dispatch("counter/asyncInc");// 异步修改 console.log(getState("counter").num);// log 3 })()
注意dispatch调用时基于字符串匹配方式,之因此保留这样的调用方式是为了照顾须要动态调用的场景,其实更推荐的写法是
import { dispatch } from "concent"; (async ()=>{ console.log(getState("counter").num);// log 1 await dispatch(reducer.inc);// 同步修改 console.log(getState("counter").num);// log 2 await dispatch(reducer.asyncInc);// 异步修改 console.log(getState("counter").num);// log 3 })()
上述示例主要演示了如何定义状态和修改状态,那么接下来咱们须要用到如下两个api来帮助react组件生成实例上下文(等同于与vue 3 setup里提到的渲染上下文),以及得到消费concent模块数据的能力
import { register, useConcent } from "concent"; @register("counter") class ClsComp extends React.Component { changeNum = () => this.setState({ num: 10 }) render() { return ( <div> <h1>class comp: {this.state.num}</h1> <button onClick={this.changeNum}>changeNum</button> </div> ); } } function FnComp() { const { state, setState } = useConcent("counter"); const changeNum = () => setState({ num: 20 }); return ( <div> <h1>fn comp: {state.num}</h1> <button onClick={changeNum}>changeNum</button> </div> ); }
注意到两种写法区别很小,除了组件的定义方式不同,其实渲染逻辑和数据来源都如出一辙。
const rootElement = document.getElementById("root"); ReactDOM.render( <React.StrictMode> <div> <ClsComp /> <FnComp /> </div> </React.StrictMode>, rootElement );
对比Recoil
,咱们发现没有顶层并无Provider
或者Root
相似的组件包裹,react组件就已接入concent,作到真正的即插即用和无感知接入,同时api
保留为与react
一致的写法。
concent为每个组件实例都生成了实例上下文,方便用户直接经过ctx.mr
调用reducer方法
mr 为 moduleReducer的简写,直接书写为ctx.moduleReducer也是合法的
// --------- 对于类组件 ----------- changeNum = () => this.setState({ num: 10 }) // ===> 修改成 changeNum = () => this.ctx.mr.inc(10);// or this.ctx.mr.asynCtx() // --------- 对于函数组件 ----------- const { state, mr } = useConcent("counter");// useConcent 返回的就是ctx const changeNum = () => mr.inc(20);// or ctx.mr.asynCtx()
run
接口里支持扩展computed
属性,即让用户定义一堆衍生数据的计算函数集合,它们能够是同步的也能够是异步的,同时支持一个函数用另外一个函数的输出做为输入来作二次计算,计算的输入依赖是自动收集到的。
const computed = {// 定义计算函数集合 numx10({ num }) { return num * 10; }, // n:newState, o:oldState, f:fnCtx // 结构出num,表示当前计算依赖是num,仅当num发生变化时触发此函数重计算 async numx10_2({ num }, o, f) { // 必需调用setInitialVal给numx10_2一个初始值, // 该函数仅在初次computed触发时执行一次 f.setInitialVal(num * 55); await delay(); return num * 100; }, async numx10_3({ num }, o, f) { f.setInitialVal(num * 1); await delay(); // 使用numx10_2再次计算 const ret = num * f.cuVal.numx10_2; if (ret % 40000 === 0) throw new Error("-->mock error"); return ret; } } // 配置到counter模块 run({ counter: { state, reducer, computed } });
上述计算函数里,咱们刻意让numx10_3
在某个时候报错,对于此错误,咱们能够在run
接口的第二位options
配置里定义errorHandler
来捕捉。
run({/**storeConfig*/}, { errorHandler: (err)=>{ alert(err.message); } })
固然更好的作法,利用concent-plugin-async-computed-status
插件来完成对全部模块计算函数执行状态的统一管理。
import cuStatusPlugin from "concent-plugin-async-computed-status"; run( {/**storeConfig*/}, { errorHandler: err => { console.error('errorHandler ', err); // alert(err.message); }, plugins: [cuStatusPlugin], // 配置异步计算函数执行状态管理插件 } );
该插件会自动向concent配置一个cuStatus
模块,方便组件链接到它,消费相关计算函数的执行状态数据
function Test() { const { moduleComputed, connectedState, setState, state, ccUniqueKey } = useConcent({ module: "counter",// 属于counter模块,状态直接从state得到 connect: ["cuStatus"],// 链接到cuStatus模块,状态从connectedState.{$moduleName}得到 }); const changeNum = () => setState({ num: state.num + 1 }); // 得到counter模块的计算函数执行状态 const counterCuStatus = connectedState.cuStatus.counter; // 固然,能够更细粒度的得到指定结算函数的执行状态 // const {['counter/numx10_2']:num1Status, ['counter/numx10_3']: num2Status} = connectedState.cuStatus; return ( <div> {state.num} <br /> {counterCuStatus.done ? moduleComputed.numx10 : 'computing'} {/** 此处拿到错误能够用于渲染,固然也抛出去 */} {/** 让ErrorBoundary之类的组件捕捉并渲染降级页面 */} {counterCuStatus.err ? counterCuStatus.err.message : ''} <br /> {moduleComputed.numx10_2} <br /> {moduleComputed.numx10_3} <br /> <button onClick={changeNum}>changeNum</button> </div> ); }
![]https://raw.githubusercontent...
开篇我说对Recoli
提到的精确更新保持了怀疑态度,有一些误导的嫌疑,此处咱们将揭开疑团
你们知道hook
使用规则是不能写在条件控制语句里的,这意味着下面语句是不容许的
const NumView = () => { const [show, setShow] = useState(true); if(show){// error const [num, setNum] = useRecoilState(numState); } }
因此用户若是ui渲染里若是某个状态用不到此数据时,某处改变了num
值依然会触发NumView
重渲染,可是concent
的实例上下文里取出来的state
和moduleComputed
是一个Proxy
对象,是在实时的收集每一轮渲染所须要的依赖,这才是真正意义上的按需渲染和精确更新。
const NumView = () => { const [show, setShow] = useState(true); const {state} = useConcent('counter'); // show为true时,当前实例的渲染对state.num的渲染有依赖 return {show ? <h1>{state.num}</h1> : 'nothing'} }
固然若是用户对num值有ui渲染完毕后,有发生改变时须要作其余事的需求,相似useEffect
的效果,concent也支持用户将其抽到setup
里,定义effect
来完成此场景诉求,相比useEffect
,setup里的ctx.effect
只需定义一次,同时只需传递key名称,concent会自动对比前一刻和当前刻的值来决定是否要触发反作用函数。
conset setup = (ctx)=>{ ctx.effect(()=>{ console.log('do something when num changed'); return ()=>console.log('clear up'); }, ['num']) } function Test1(){ useConcent({module:'cunter', setup}); return <h1>for setup<h1/> }
关于concent是否支持current mode
这个疑问呢,这里先说答案,concent
是100%彻底支持的,或者进一步说,全部状态管理工具,最终触发的都是setState
或forceUpdate
,咱们只要在渲染过程当中不要写具备任何反作用的代码,让相同的状态输入获得的渲染结果幂,便是在current mode
下运行安全的代码。
current mode
只是对咱们的代码提出了更苛刻的要求。
// bad function Test(){ track.upload('renderTrigger');// 上报渲染触发事件 return <h1>bad case</h1> } // good function Test(){ useEffect(()=>{ // 就算仅执行了一次setState, current mode下该组件可能会重复渲染, // 但react内部会保证该反作用只触发一次 track.upload('renderTrigger'); }) return <h1>bad case</h1> }
咱们首先要理解current mode原理是由于fiber架构模拟出了和整个渲染堆栈(即fiber node上存储的信息),得以有机会让react本身以组件为单位调度组件的渲染过程,能够悬停并再次进入渲染,安排优先级高的先渲染,重度渲染的组件会切片为多个时间段反复渲染,而concent的上下文自己是独立于react存在的(接入concent不须要再顶层包裹任何Provider), 只负责处理业务生成新的数据,而后按需派发给对应的实例(实例的状态自己是一个个孤岛,concent只负责同步创建起了依赖的store的数据),以后就是react本身的调度流程,修改状态的函数并不会由于组件反复重入而屡次执行(这点须要咱们遵循不应在渲染过程当中书写包含有反作用的代码原则),react仅仅是调度组件的渲染时机,而组件的中断和重入针对也是这个渲染过程。
因此一样的,对于concent
const setup = (ctx)=>{ ctx.effect(()=>{ // effect是对useEffect的封装, // 一样在current mode下该反作用也只触发一次(由react保证) track.upload('renderTrigger'); }); } // good function Test2(){ useConcent({setup}) return <h1>good case</h1> }
一样的,依赖收集在current mode
模式下,重复渲染仅仅是致使触发了屡次收集,只要状态输入同样,渲染结果幂等,收集到的依赖结果也是幂等的。
// 假设这是一个渲染很耗时的组件,在current mode模式下可能会被中断渲染 function HeavyComp(){ const { state } = useConcent({module:'counter'});// 属于counter模块 // 这里读取了num 和 numBig两个值,收集到了依赖 // 即当仅当counter模块的num、numBig的发生变化时,才触发其重渲染(最终仍是调用setState) // 而counter模块的其余值发生变化时,不会触发该实例的setState return ( <div>num: {state.num} numBig: {state.numBig}</div> ); }
最后咱们能够梳理一下,hook
自己是支持把逻辑剥离到用的自定义hook(无ui返回的函数),而其余状态管理也只是多作了一层工做,引导用户把逻辑剥离到它们的规则之下,最终仍是把业务处理数据交回给react
组件调用其setState
或forceUpdate
触发重渲染,current mode
的引入并不会对现有的状态管理或者新生的状态管理方案有任何影响,仅仅是对用户的ui代码提出了更高的要求,以避免由于current mode
引起难以排除的bug
为此react还特别提供了
React.Strict
组件来故意触发双调用机制,
https://reactjs.org/docs/stri... 以引导用户书写更符合规范的react代码,以便适配未来提供的current mode。
react全部新特性其实都是被fiber
激活了,有了fiber
架构,衍生出了hook
、time slicing
、suspense
以及未来的Concurrent Mode
,class组件和function组件均可以在Concurrent Mode
下安全工做,只要遵循规范便可。
摘取自: https://reactjs.org/docs/stri...
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
因此呢,React.Strict
其实为了引导用户写可以在Concurrent Mode
里运行的代码而提供的辅助api,先让用户慢慢习惯这些限制,按部就班一步一步来,最后再推出Concurrent Mode
。
Recoil
推崇状态和派生数据更细粒度控制,写法上demo看起来简单,实际上代码规模大以后依然很繁琐。
// 定义状态 const numState = atom({key:'num', default:0}); const numBigState = atom({key:'numBig', default:100}); // 定义衍生数据 const numx2Val = selector({ key: "numx2", get: ({ get }) => get(numState) * 2, }); const numBigx2Val = selector({ key: "numBigx2", get: ({ get }) => get(numBigState) * 2, }); const numSumBigVal = selector({ key: "numSumBig", get: ({ get }) => get(numState) + get(numBigState), }); // ---> ui处消费状态或衍生数据 const [num] = useRecoilState(numState); const [numBig] = useRecoilState(numBigState); const numx2 = useRecoilValue(numx2Val); const numBigx2 = useRecoilValue(numBigx2Val); const numSumBig = useRecoilValue(numSumBigVal);
Concent
遵循redux
单一状态树的本质,推崇模块化管理数据以及派生数据,同时依靠Proxy
能力完成了运行时依赖收集和追求不可变的完美整合。
run({ counter: {// 声明一个counter模块 state: { num: 1, numBig: 100 }, // 定义状态 computed:{// 定义计算,参数列表里解构具体的状态时肯定了依赖 numx2: ({num})=> num * 2, numBigx2: ({numBig})=> numBig * 2, numSumBig: ({num, numBig})=> num + numBig, } }, }); // ---> ui处消费状态或衍生数据,在ui处结构了才产生依赖 const { state, moduleComputed, setState } = useConcent('counter') const { numx2, numBigx2, numSumBig} = moduleComputed; const { num, numBig } = state;
因此你将得到: