❤ star me if you like concent ^_^vue
导读react
上期写完文章concent 骚操做之组件建立&状态更新后,末尾留下了下面两期的文章预告,按照原来的预告内容,这一次文章题目应该是【探究setup带来的变革】了,可是由于本文会实打实的将
vue3
里的setup
特性提出来和Concent
作对比,因此临时改了题目为【应战Vue3 setup,Concent携手React出招了!】,以便体现出有了setup
特性的加持,你的react应用将变得犀利无比,代码组织方式将具备更大的想象空间,固然这里要认可一点,大概是在6月份左右在某乎看到了Vue Function-based API RFC这篇文章,给了我极大的灵感,在这以前我一直有一个想法,想统一函数组件和类组件的装配工做,须要定义一个入口api,可是命名彷佛一直感受定不下来,直到此文中说起setup
后,我如醍醐灌顶,它所作的工做和我想要达到的效果本质上是如出一辙的呀!因而乎Concent
里的setup
特性就这样诞生了。git
正文开始以前,先预览一个生产环境的setup 示例,以示这是一个生产环境可用的标准特性。 进入在线IDE体验github
在Function-based API文章里说得很清楚了,setup API 受 React Hooks 的启发,提供了一个全新的逻辑复用方案,可以更好的组织逻辑,更好的在多个组件之间抽取和复用逻辑, 且将不存在如下问题。typescript
使用基于函数的 API,咱们能够将相关联的代码抽取到一个 "composition function"(组合函数)中 —— 该函数封装了相关联的逻辑,并将须要暴露给组件的状态以响应式的数据源的方式返回出来api
import { reactive, computed, watch, onMounted } from 'vue'
const App = {
template: ` <div> <span>count is {{ count }}</span> <span>plusOne is {{ plusOne }}</span> <button @click="increment">count++</button> </div> `,
setup() {
// reactive state
const count = reactive(0)
// computed state
const plusOne = computed(() => count.value + 1)
// method
const increment = () => { count.value++ }
// watch
watch(() => count.value * 2, val => {
console.log(`count * 2 is ${val}`)
})
// lifecycle
onMounted(() => {
console.log(`mounted`)
})
// expose bindings on render context
return {
count,
plusOne,
increment
}
}
}
复制代码
说起Concent
的setup
的设计动机以前,咱们再来复盘下官方给出的hook
设计动机数组
这里面提到的复用状态逻辑很难,是两大框架都达成了一致的共识点,社区也一致在经过各类尝试解决此问题,到了最后,你们发现一个有趣的现象,咱们写UI的时候,基本上用不到继承,并且官方也是极力推荐组合大于继承的思想,试想一下,谁会写个BasicModal
,而后漫天的各类***Modal
继承自BasicModal
来写业务实现呢?基本上基础组件设计者都是BasicModal
留几个接口和插槽,而后你引入BasicModal
本身再封装一个***Modal
就完事了对吧?bash
因此在react基于Fiber的链表式树结构能够模拟出函数调用栈后,hook
的诞生就至关因而顺势而为了,可是hook
只是给函数组件撕开了一个放置传送门的口子,这个传送门很是神奇,能够定义状态,能够定义生命周期函数等,可是原始的hook和业务开发友好体验度上仍是有些间隙,因此你们开始在传送门上开始大作文章,有勤勤恳恳的专一于让你更轻松的使用hook的全家桶react-use
,也有专一于某个方向的hook如最近开始大红大紫的专一于fetch data
体验的useSWR
,固然也有很多开发开始慢慢沉淀本身的业务hook 包。闭包
可是基于hook
组织业务逻辑有以下局限性框架
特别注意的陷阱是,闭包函数内部千万不要引入外部的变量,而是要放在依赖列表里
function MyProjects () {
const { data: user } = useSWR('/api/user')
const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)
// When passing a function, SWR will use the
// return value as `key`. If the function throws,
// SWR will know that some dependencies are not
// ready. In this case it is `user`.
if (!projects) return 'loading...'
return 'You have ' + projects.length + ' projects'
}
复制代码
以上面useSWR的官方示例代码为例,看起来第二个useSWR是必定会报错的,可是它内部会try catch住undefined错误,推导user还未准备好,从而巧妙的躲过渲染报错,可是本质上hook不是异步的,咱们的实际业务逻辑复杂的时候,请求多且相互依赖多的时候,它内部的处理会有更多的额外消耗。
基于这些问题的存在,Concent
的setup
诞生了,巧妙的利用hook这个传送门,让组件初次渲染时执行setup,从而开辟了另外一个空间,斡旋在function组件
和class组件
之间,让二者的业务逻辑能够互相共享,从而达成了function组件
和class组件
完美的和谐共存局面,实现了Concent
的核心目标,不管是function组件
和class组件
,它们都只是ui的载体,真正的业务逻辑处于model
里。
本文要说的主角是setup
,为何这里要提useConcent
呢?由于setup
须要传送门呀,在Concent
里useConcent
就扮演着这个重要的传送门角色,咱们接下来经过代码一步一步的分析,最后引入setup
来作出对比。
了解更多能够查看往期文章
聊一聊状态管理&Concent设计理念
或进入在线IDE体验(如点击图片无效可点击左侧文字连接)
按照约定,使用任何Concent
接口前必定要先配置模型定义
/** ------ code in runConcent.js ------ */
import { run } from 'concent';
import { foo, bar, baz } from 'models';
run({foo, bar, baz});
/** ------ code in models/foo/state.js ------ */
export default {
loading: false,
name: '',
age: 12,
}
/** ------ code in models/foo/reducer.js ------ */
export async function updateAge(payload, moduleState, actionCtx){
const { data } = await api.serverCall();
// 各类复杂业务逻辑略
return {age: payload};
}
export async function updateName(payload, moduleState, actionCtx){
const { data } = await api.serverCall();
// 各类复杂业务逻辑略
return {name: payload};
}
export async function updateAgeAndName({name, age}, moduleState, actionCtx){
// actionCtx.setState({loading:true});
// 任意组合调用其余reducer
await actionCtx.dispatch(updateAge, age);
await actionCtx.dispatch(updateName, name);
// return {loading: false}; // 当前这个reducer自己也能够选择返回新的状态
}
复制代码
注意model并不是必定要在run里集中配置,也能够跟着组件就近配置,一个标准的代码组织结构示意以下图
利用configure
就近配置page model
下面咱们经过useConcent
定义一个Concent函数组件
function Foo(){
useConcent();
return (
<div>hello</div>
)
}
复制代码
这就是一个Concent函数组件,固然这样定义是无心义的,由于什么都没有干,因此咱们为此函数组件加个私有状态吧先
function Foo(){
// ctx是Concent为组件注的实例上下文对象
const ctx = useConcent({state:{tip:'I am private', src:'D'}});
const { state } = ctx;
// ...
}
复制代码
尽管Concent会保证此状态只会在组件初次渲染时在赋值给ctx.state做为初始值,可是每次组件重渲染这里都会临时建立一次state对象,因此更优的写法是咱们将其提到函数外面
const iState = {tip:'I am private', src:'D'}; //initialState
function Foo(){
const ctx = useConcent({state:iState});
const { state } = ctx;
// ...
}
复制代码
若是此组件会同时建立多个,建议将iState写为函数,以保证状态隔离
const iState = ()=> {tip:'I am private'}; //initialState
复制代码
定义完组件,能够读取状态了,下一步咱们固然是要修改状态了,同时咱们也定义一些生命周期函数吧
function Foo(){
const ctx = useConcent({state:iState});
const { state, setState } = ctx;
cosnt changeTip = (e)=> setState({tip:e.currentTarget.value});
cosnt changeSrc = (e)=> setState({src:e.currentTarget.value});
React.useEffect(()=>{
console.log('首次渲染完毕触发');
return ()=> console.log('组件卸载时触发');
},[]);
// ...
}
复制代码
这里看起来是否是有点奇怪,只是将React.setState
句柄调用替换成了useConcent
返回的ctx
提供的setState
句柄,可是若是我想定义当tip发生变化时就触发反作用函数,那么React.useEffect
里第二为参数列表该怎么写呢,看起来直接传入state.tip就能够了,可是咱们提供更优的写法。
是时候接入setup
了,setup
的精髓就是只会在组件初次渲染前执行一次,利用setup
开辟的新空间完成组件的功能装配工做吧!
咱们定义当tip
或者src
发生改变时执行的反作用函数吧
// Concent会将实例ctx透传给setup函数
const setup = ctx=>{
ctx.effect(()=>{
console.log('tip发生改变时执行');
return ()=> console.log('组件卸载时触发');
}, ['tip']);
ctx.effect(()=>{
console.log('tip和src任意一个发生改变时执行');
return ()=> console.log('组件卸载时触发');
}, ['tip', 'src'])
}
function Foo(){
// useConcent里传入setup
const ctx = useConcent({state:iState, setup});
const { state, setState } = ctx;
// ...
}
复制代码
注意到没有!ctx.effect
和React.useEffect
使用方式如出一辙,除了第二为参数依赖列表的写法,React.useEffect
须要传入具体的值,而ctx.effect
之须要传入stateKey名称,由于Concent
老是会记录组件最新状态的前一个旧状态,经过二者对比就知道需不须要触发反作用函数了!
由于ctx.effect
已经存在于另外一个空间内,不受hook
语法规则限制了,因此若是你想,你甚至能够这样写(固然了,实际业务在不了解规则的状况下不推荐这样写)
const setup = ctx=>{
ctx.watch('tip', (tipVal)=>{// 观察到tip值变化时,触发的回调
if(tipVal === 'xxx' ){//当tip的值为'xxx'时,就定义一个新的反作用函数
ctx.effect(()=>{
return ()=> console.log('tip改变');
}, ['tip']);
}
});
}
复制代码
咱们经过上面的示例,完成了状态的定义,和反作用函数的迁移,可是状态的修改仍是处于函数组件内部,如今咱们将它们挪到setup
空间内,利用setup
返回的对象能够在ctx.settings
里取到这一特色,将这写方法提高为静态的api定义,而不是每次组件重复渲染期间都须要临时再定义了。
const setup = ctx=>{
ctx.effect(()=>{ /** code */ }, ['tip']);
cosnt changeTip = (e)=> setState({tip:e.currentTarget.value});
cosnt changeSrc = (e)=> setState({src:e.currentTarget.value});
return {changeTip, changeSrc};
}
function Foo(){
const ctx = useConcent({state:iState, setup});
const { state, setState, settings } = ctx;
// 如今能够绑定settings.changeTip , settings.changeSrc 到具体的ui上了
}
复制代码
上面示例里组件始终操做的是本身的状态,若是须要读取model的数据和操做model的方法怎么办呢?你仅须要标注链接的模块名称就行了,注意的是此时state是私有状态和模块状态合成而来,若是你的私有状态里有key和模块状态同名了,那么它其实就自动的被模块状态的值覆盖了。
function Foo(){
// 链接到foo模块
const ctx = useConcent({module:'foo', state:iState, setup});
const { state, setState, settings } = ctx;
// 此时state是私有状态和模块状态合成而来
// {tip:'', src:'', loading:false, name:'', age:12}
}
复制代码
若是你讨厌state被合成出来,污染了你的ctx.state
,你也可使用connect
参数来链接模块,同时connect
还容许你链接多个模块
function Foo(){
// 经过connect链接到foo, bar, baz模块
const ctx = useConcent({connect:['foo', 'bar', 'baz'], state:iState, setup});
const { state, setState, settings, connectedState } = ctx;
const { foo, bar, baz} = connectedState;
// 经过ctx.connectedState读取到各个模块的状态
}
复制代码
还记得咱们上面定义的foo模块的reducer函数吗?如今咱们能够经过dispatch直接调用reducer函数,因此咱们能够在setup
里完成这些桥接函数的装配工做。
const setup = ctx=>{
cosnt updateAgeAndName = e=> ctx.dispatch('updateAgeAndName', e.currentTarget.value);
cosnt updateAge = e=> ctx.dispatch('updateAge', e.currentTarget.value);
cosnt updateName = e=> ctx.dispatch('updateName', e.currentTarget.value);
return {updateAgeAndName, updateAge, updateName};
}
复制代码
固然,上面的写法是在注册Concent组件时指定了明确的module
值,若是是使用connect
参数链接的模块,则须要加明确的模块前缀
const setup = ctx=>{
// 调用的是foo模块updateAge方法
cosnt updateAge = e=> ctx.dispatch('foo/updateAge', e.currentTarget.value);
}
复制代码
等等!你说讨厌字符串调用的形式,由于你已经在上面foo模块的reducer文件里看到函数之间能够直接基于函数引用来组合逻辑了,这里还要写名字很不爽,Concent
知足你直接基于函数应用调用的需求
import * as fooReducer from 'models/foo/reducer';
const setup = ctx=>{
// dispatch fooReducer函数
cosnt updateAge = e=> ctx.dispatch(fooReducer.updateAge, e.currentTarget.value);
}
复制代码
嗯?什么,这样写也以为不舒服,想直接调用,固然能够!
const setup = ctx=>{
// 直接调用fooReducer
cosnt updateAge = e=> ctx.reducer.foo.updateAge(e.currentTarget.value);
}
复制代码
由于class组件也支持setup,也拥有实例上下文对象,那么和function组件间共享业务逻辑天然是水到渠成的事情了
import { register } from 'concent';
register('foo')
class FooClazzComp extends React.Component{
$$setup(ctx){
// 模拟componentDidMount
ctx.effect(()=>{
/** code */
return ()=>{console.log('模拟componentWillUnmount');}
}, []);
ctx.effect(()=>{
console.log('模拟componentDidUpdate');
}, null, false);
// 第二位参数depKeys写null表示每一轮都执行
// 第三位参数immediate写false,表示首次渲染不执行
// 二者一结合,即模拟出了componentDidUpdate
cosnt updateAge = e=> ctx.dispatch('updateAge', e.currentTarget.value);
return { updateAge }
}
render(){
const { state, setState, settings } = this.ctx;
// 这里其实this.state 和 this.ctx.state 指向的是同一个对象
}
}
复制代码
上文里,其实读者有注意的话,咱们一直提到了一个关键词实例上下文,它是Concent管控全部组件和加强组件能力的重要入口。
例如setup在ctx上提供给用户的effect接口,底层会自动去适配函数组件的useEffect
和类组件的componentDidMount
、componentDidUpdate
、componentWillUnmount
,从而抹平了函数组件和类组件之间的生命周期函数的差别。
例如ctx上提供的emit&on接口,让组件之间除了数据驱动ui的模式,仍是更松耦合的经过事件来驱动目标组件完成一些其余动做。
下图完整了的解释了整个Concent组件在建立期、存在期和销毁期各个阶段的工做细节。
最后的最后,咱们使用Concent提供的registerHookComp
接口来写一个组件和Vue3 setup
作个对比,指望此次出招可以打动做为react开发者的你的心,相信基于不可变原则也能写出优雅的组合api型函数组件。
registerHookComp
本质上是基于useConcent
浅封装的,自动将返回的函数组件包裹了一层React.memo
^_^
import { registerHookComp } from "concent";
const state = {
visible: false,
activeKeys: [],
name: '',
};
const setup = ctx => {
ctx.on("openMenu", (eventParam) => { /** code here */ });
ctx.computed("visible", (newVal, oldVal) => { /** code here */ });
ctx.watch("visible", (newVal, oldVal) => { /** code here */ });
ctx.effect( () => { /** code here */ }, []);
const doFoo = param => ctx.dispatch('doFoo', param);
const doBar = param => ctx.dispatch('doBar', param);
const syncName = ctx.sync('name');
return { doFoo, doBar, syncName };
};
const render = ctx => {
const {state, settings} = ctx;
return (
<div className="ccMenu"> <input value={state.name} onChange={settings.syncName} /> <button onClick={settings.doFoo}>doFoo</button> <button onClick={settings.doBar}>doBar</button> </div> ); }; export default registerHookComp({ state, setup, module:'foo', render }); 复制代码
❤ star me if you like concent ^_^,Concent的发展离不开你们的精神鼓励与支持,也期待你们了解更多和提供相关反馈,让咱们一块儿构建更有乐趣,更加健壮和更高性能的react应用吧。
下期预告【concent love typescript】,由于Concent整套api都是面向函数式的,和ts结合是天生一对的好基友,因此基于ts书写concent将是很是的简答和舒服😀,各位敬请期待。
强烈建议有兴趣的你进入在线IDE fork代码修改哦(如点击图片无效可点击文字连接)
若是有关于concent的疑问,能够扫码加群咨询,我会尽力答疑解惑,帮助你了解更多。