时间: 2020-09-03 00:08:26 人气: 2281 评论: 0
vue3里的setup特性提出很久了,如果有了setup特性的加持,react应用是不是能变得更加犀利,代码组织方式是不是将具有更大的想象空间呢?
大概是在6月份左右在某乎看到了Vue Function-based API RFC这篇文章,给了我极大的灵感,在这之前我一直有一个想法,想统一函数组件和类组件的装配工作,需要定义一个入口api,但是命名似乎一直感觉定不下来,直到此文中提及setup后,我如醍醐灌顶,它所做的工作和我想要达到的效果本质上是一模一样的呀!于是乎Concent里的setup特性就这样诞生了。
在Function-based API文章里说得很清楚了,setup API 受 React Hooks 的启发,提供了一个全新的逻辑复用方案,能够更好的组织逻辑,更好的在多个组件之间抽取和复用逻辑, 且将不存在以下问题。
使用基于函数的 API,我们可以将相关联的代码抽取到一个 "composition function"(组合函数)中 —— 该函数封装了相关联的逻辑,并将需要暴露给组件的状态以响应式的数据源的方式返回出来
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就完事了对吧?
所以在react基于Fiber的链表式树结构可以模拟出函数调用栈后,hook的诞生就相当于是顺势而为了,但是hook只是给函数组件撕开了一个放置传送门的口子,这个传送门非常神奇,可以定义状态,可以定义生命周期函数等,但是原始的hook和业务开发友好体验度上还是有些间隙,所以大家开始在传送门上开始大做文章,有勤勤恳恳的专注于让你更轻松的使用hook的全家桶react-use,也有专注于某个方向的hook如最近开始大红大紫的专注于fetch data体验的useSWR,当然也有不少开发开始慢慢沉淀自己的业务hook
包。
但是基于hook组织业务逻辑有如下局限性
```js 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来做出对比。
了解更多可以查看往期文章
或进入在线IDE体验(如点击图片无效可点击左侧文字链接)
按照约定,使用任何Concent接口前一定要先配置模型定义
import { run } from 'concent'; import { foo, bar, baz } from 'models'; run({foo, bar, baz}); //foo state export default { loading: false, name: '', age: 12, } // foo reducer 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 c