阅读 1430

Concent 2.4发布, 最小粒度观察与渲染组件单元

❤ star me if you like concent ^_^

来自Observer的启发

在阅读mobx文档时,为了适配最新的函数组件,除了暴露一个api名为useObserver,还发现暴露了另外一个比较有意思的组件Observer,支持更精细的控制渲染单元,大概用起来的姿势是这样的。

在细说Observer之前,我们先看看useObserver的使用套路,我们先建立一个名为login的store

import { observable, action, computed } from "mobx";

class LoginStore {
  @observable firstName = 'f'
  @observable lastName = 'l'

  @computed
  get nickName(){
    return `nick_${this.firstName}_${this.lastName}`
  }

  @action.bound
  changeFirstName(e) {
    this.firstName = e.target.value;
  }
  @action.bound
  changeLastName(e) {
    this.lastName = e.target.value;
  }
}

export default new LoginStore();
复制代码

紧接着实例化一个登录框函数组件,使用useObserver切出一片需要观察和渲染的组件片段

import React from "react"
import { useObserver } from "mobx-react";
import store from "./models";

const LoginBox1 = () => {
  const { login } = store;
  return useObserver(() => (
    <div>
      <div>
        firstName:
        <input value={login.firstName} onChange={login.changeFirstName} />
      </div>
      <div>
        lastName:
        <input value={login.lastName} onChange={login.changeLastName} />
      </div>
      <div>
      nickName: {login.nickName}
      </div>
    </div>
  ));
};
复制代码

如果我们需要进一步切割渲染范围,改变了哪个属性的值就仅渲染与这个属性相关的视图片段,则可搭配Obserser来达到我们的"切割"目的。

import { useObserver, Observer } from "mobx-react";

const LoginBox2 = () => {
  const { login } = store;
  return useObserver(() => (
    <div>
      <Observer>
        {() => (
          <div>
            ob firstName:
            <input value={login.firstName} onChange={login.changeFirstName} />
          </div>
        )}
      </Observer>
      <Observer>
        {() => (
          <div>
            ob lastName:
            <input value={login.lastName} onChange={login.changeLastName} />
          </div>
        )}
      </Observer>
      <Observer>
        {() => <div>ob nickName: {login.nickName}</div>}
      </Observer>
    </div>
  ));
};
复制代码

注意此处的Observer组件使用方式,必需在useObserver回调内部使用,渲染它们看看效果吧。

查看在线示例

基于useConcent"切割"组件

我们知道,concent已提供的接口useConcent,可以直接注册某个函数属于某个模块

function FooComp(){
    //属于login模块
    const { state } = useConcent('login');
    
    //当前视图对name有依赖
    return <div>{state.name}</div>
}
复制代码

也可以组成某个函数组件连接了其他多个模块

function FooComp(){
    //连接到了login模块,xxx模块
    const { connectedState } = useConcent({connect:['login', 'xxx']});
    
    //当前视图对login模块的name有依赖
    return <div>{connectedState.login.name}</div>
}
复制代码

当然了也可以既属于某个模块,同时也连接到其他模块,

function FooComp(){
    //属于login模块, 连接到了xxx模块,yyy模块
    const { state, connectedState } = useConcent({module:'login', connect:['xxx', 'yyy']});
}
复制代码

所以只需要对useConcent做进一步的封装,即可达到支持观察与渲染最小粒度的组件单元的目的了,2.4版本里新暴露了组件Ob,就是其具体实现。

同样的我们先来创建一个login模块吧

  • state
// code in models/login/state.js
export default ()=>({
  firstName: "f",
  lastName: "l",
});
复制代码
  • computed
// code in models/login/computed.js
export function nickName(n, o, f){
  return `nick_${n.firstName}_${n.lastName}`
}
复制代码
  • reducer
// code in models/login/reducer.js
export function changeFirstName(e) {
  return { firstName: e.target.value };
}

export function changeLastName(e) {
  return { lastName: e.target.value };
}
复制代码

写一个和useObserver目的一样的组件

import * as React from "react";
import { useConcent } from "concent";

const LoginBox1 = React.memo(() => {
  // mr is alias of moduleReducer
  const { state, moduleComputed: mcu, mr } = useConcent("login");

  return (
    <>
      <div>
        firstName:
        <input value={state.firstName} onChange={mr.changeFirstName} />
      </div>
      <div>
        lastName:
        <input value={state.lastName} onChange={mr.changeLastName} />
      </div>
      <div> nickName:{mcu.nickName}</div>
    </>
  );
});
复制代码

此组件里firstNamelastName任意一个字段的值,改变都会引起LoginBox1渲染,现在我们像Observer组件一样细粒度的控制渲染范围吧

export const LoginBox2 = React.memo(() => {
  return (
    <>
     <h3>show Ob capability</h3>
      <Ob module="login">
        {([state, _, {mr}]) => (
          <div>
            firstName:
            <input value={state.firstName} onChange={mr.changeFirstName} />
          </div>
        )}
      </Ob>
      <Ob module="login">
        {([state, _, {mr}]) => (
          <div>
            firstName:
            <input value={state.lastName} onChange={mr.changeLastName} />
          </div>
        )}
      </Ob>
      <Ob module="login">
        {([_, computed]) => (
          <div> nickName:{computed.nickName}</div>
        )}
      </Ob>
    </>
  );
});
复制代码

渲染它们看看效果吧

查看在线示例

源码解读

useConcent返回的模块状态或者计算数据,本身具有运行时收集依赖的能力,所以我们只需在源码里对useConcent做二次封装,就拥有了像Observer组件一样的提供更细粒度的观察与渲染组件的能力了。

import React from 'react';
import { useConcentForOb } from '../core/hook/use-concent';

const obView = () => 'Ob view';

export default React.memo(function (props) {
  const { module, connect, classKey, render, children } = props;
  if (module && connect) {
    throw new Error(`module, connect can not been supplied both`);
  } else if (!module && !connect) {
    throw new Error(`module or connect should been supplied`);
  }

  const view = render || children || obView;
  const register = module ? { module } : { connect };
  // 设置为1,最小化ctx够造过程,仅附加状态数据,衍生数据、和reducer相关函数
  register.lite = 1;
  const ctx = useConcentForOb(register, classKey);
  const { mr, cr, r} = ctx;

  let state, computed;
  if (module) {
    state = ctx.moduleState;
    computed = ctx.moduleComputed;
  } else {
    state = ctx.connectedState;
    computed = ctx.connectedComputed;
  }

  return view([state, computed, { mr, cr, r}]);
})
复制代码

在源码里,我们对渲染函数提供状态数据,衍生数据、和reducer相关函数,方便用户按需选择,Ob组件对比Observe,有以下几点使用体验提升

  • 1,无需在代码实现处人工import对应的store,实例化Ob时传入module值即可获取对应的数据
  • 2,使用更自由,无需被嵌套在其他方法内(Observe必需配合useObserve
  • 3,保持和useConcent一样的使用方式,随处插拔,代码无需过多改造

动态的模块替换

useConcent允许动态的传入module或者connect参数,以此满足用户一些需要创建不同模块组件的工厂函数场景。

首先我们创建一个多模块的store吧,?global模块用于存储选择的模块

run({
  counter: {
    state: { count: 999 }
  },
  counter2: {
    state: { count: 100000 }
  },
  $$global: {
    state: { mod: "counter" }
  }
});
复制代码

然后书写一个函数组件,因为需要动态的传入模块值,所以我们需要先读取global模块的模块值,再传给useConcent以确定属于哪个模块。

const setup = ctx => {
  console.log(
    ctx.ccUniqueKey +
      " setup method will been called before first render period !!"
  );
  ctx.effect(()=>{
    return ()=>{
      console.log('trigger unmount ' + ctx.state.count);
    }
  }, [])

  return {
    add: () => ctx.setState({ count: ctx.state.count + 1 }),
  };
};

function SetupFnCounter() {
  const {state: { mod } } = useConcent(cst.MODULE_GLOBAL);
  const ctx = useConcent({ module: mod, setup });
  return (
    <View
      tag="fn comp with useConcent&setup --"
      add={ctx.settings.add}
      count={ctx.state.count}
    />
  );
}
复制代码

注意这里,如果组件存在期切换了新的模块,当它们改变时,会有一次实力上下文卸载和重加载的过程,比如从counter切换为counter2,那么ctx.effect的返回函数作为unmount逻辑会被触发。

既然useConcent支持模块热替换,那么Ob当然也支持了,我们书写一个切换模块的逻辑在顶层App里,同时也渲染一个Ob实例。

export default function App() {
  const {
    state: { mod },
    setState
  } = useConcent(cst.MODULE_GLOBAL);
  const changeMod = () =>
    setState({ mod: mod === "counter" ? "counter2" : "counter" });

  return (
    <div className="App">
      <button onClick={changeMod}>change mod</button>
      <Ob module={mod}>
        {([state]) => {
          console.log("render ob");
          return <div>xx: {state.count}</div>;
        }}
      </Ob>
      <SetupFnCounter />
    </div>
  );
}
复制代码

最后看看效果吧^_^ 查看在线示例

往期回顾

❤ star me if you like concent ^_^

Edit on CodeSandbox

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

Edit on StackBlitz

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

如果有关于concent的疑问,可以扫码加群咨询,会尽力答疑解惑,帮助你了解更多。