应战Vue3 setup,Concent携手React出招了!

❤ 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

Vue3 setup 设计动机

在Function-based API文章里说得很清楚了,setup API 受 React Hooks 的启发,提供了一个全新的逻辑复用方案,可以更好的组织逻辑,更好的在多个组件之间抽取和复用逻辑, 且将不存在如下问题。typescript

  • 模版中的数据来源不清晰。举例来讲,当一个组件中使用了多个 mixin 的时候,光看模版会很难分清一个属性究竟是来自哪个 mixin。HOC 也有相似的问题。
  • 命名空间冲突。由不一样开发者开发的 mixin 没法保证不会正好用到同样的属性或是方法名。HOC 在注入的 props 中也存在相似问题。
  • 性能。HOC 和 Renderless Components 都须要额外的组件实例嵌套来封装逻辑,致使无谓的性能开销。

使用基于函数的 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 设计动机

说起Concentsetup的设计动机以前,咱们再来复盘下官方给出的hook设计动机数组

  • 在组件之间复用状态逻辑很难
  • 复杂组件变得难以理解
  • 难以理解的 class

这里面提到的复用状态逻辑很难,是两大框架都达成了一致的共识点,社区也一致在经过各类尝试解决此问题,到了最后,你们发现一个有趣的现象,咱们写UI的时候,基本上用不到继承,并且官方也是极力推荐组合大于继承的思想,试想一下,谁会写个BasicModal,而后漫天的各类***Modal继承自BasicModal来写业务实现呢?基本上基础组件设计者都是BasicModal留几个接口和插槽,而后你引入BasicModal本身再封装一个***Modal就完事了对吧?bash

因此在react基于Fiber的链表式树结构能够模拟出函数调用栈后,hook的诞生就至关因而顺势而为了,可是hook只是给函数组件撕开了一个放置传送门的口子,这个传送门很是神奇,能够定义状态,能够定义生命周期函数等,可是原始的hook和业务开发友好体验度上仍是有些间隙,因此你们开始在传送门上开始大作文章,有勤勤恳恳的专一于让你更轻松的使用hook的全家桶react-use,也有专一于某个方向的hook如最近开始大红大紫的专一于fetch data体验的useSWR,固然也有很多开发开始慢慢沉淀本身的业务hook 包。闭包

可是基于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不是异步的,咱们的实际业务逻辑复杂的时候,请求多且相互依赖多的时候,它内部的处理会有更多的额外消耗。

  • hook和class的开发流程是不同的,二者之间互相共用逻辑已经不可能

基于这些问题的存在,Concentsetup诞生了,巧妙的利用hook这个传送门,让组件初次渲染时执行setup,从而开辟了另外一个空间,斡旋在function组件class组件之间,让二者的业务逻辑能够互相共享,从而达成了function组件class组件完美的和谐共存局面,实现了Concent的核心目标,不管是function组件class组件,它们都只是ui的载体,真正的业务逻辑处于model里。

初探useConcent

本文要说的主角是setup,为何这里要提useConcent呢?由于setup须要传送门呀,在ConcentuseConcent就扮演着这个重要的传送门角色,咱们接下来经过代码一步一步的分析,最后引入setup来作出对比。

了解更多能够查看往期文章
聊一聊状态管理&Concent设计理念
进入在线IDE体验(如点击图片无效可点击左侧文字连接)

https://codesandbox.io/s/concent-guide-xvcej

定义model

按照约定,使用任何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

定义Concent函数组件

下面咱们经过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的精髓就是只会在组件初次渲染前执行一次,利用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.effectReact.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的数据和操做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共享业务逻辑

由于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和类组件的componentDidMountcomponentDidUpdatecomponentWillUnmount,从而抹平了函数组件和类组件之间的生命周期函数的差别。

例如ctx上提供的emit&on接口,让组件之间除了数据驱动ui的模式,仍是更松耦合的经过事件来驱动目标组件完成一些其余动做。

下图完整了的解释了整个Concent组件在建立期、存在期和销毁期各个阶段的工做细节。

对比Vue3 setup

最后的最后,咱们使用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代码修改哦(如点击图片无效可点击文字连接)

Edit on CodeSandbox

https://codesandbox.io/s/concent-guide-xvcej

Edit on StackBlitz

https://stackblitz.com/edit/cc-multi-ways-to-wirte-code

若是有关于concent的疑问,能够扫码加群咨询,我会尽力答疑解惑,帮助你了解更多。

相关文章
相关标签/搜索