Last Updated 2020-05-09 23:23:58

模块reducer

定义reducer函数

reducer对象里是一个个片段状态生成函数的集合,key就是函数名称,value就是片段状态生成函数, 负责生成并返回新的部分状态,也可以不做任何返回动作,仅仅只是组合调用其他的片段状态生成函数

以下将片段状态生成函数称之为reducer函数

类型定义:

type PartialStateFn = (
  payload: any, moduleState: ModuleState, actionCtx: ActionCtx
) => Promise<object | undefined>
  • reducer函数可以是纯函数,可以是async函数,也可以是生成器函数
  • 可以返回一个部分状态,可以调用其他reducer函数后再返回一个部分状态,也可以啥都不返回,
    只是组合其他reducer函数来调用。

run接口里对foo模块配置reducer

const foo = {
  state: { ... },
  reducer: {    changeName(name) {
      return { name };
    },
    async changeNameAsync(name) {
      await api.track(name);
      return { name };
    },
    async changeNameCompose(name, moduleState, actionCtx) {
      await actionCtx.setState({ loading: true });
      await actionCtx.dispatch('changeNameAsync', name);
      return { loading: false };
    },
    *changeNameGen(){
      yield api.track(name);
      return { name };
    }
  }
}
关于actionCtx.setState

所有的模块reducer里如果没有定义setState函数,concent会自动为其生成一个,书写actionCtx.setState(state)等同于书写actionCtx.dispatch('setState', state);

建议的做法是将reducer函数独立放一个文件,在暴露出来给module配置,这样的reducer里函数间的相互调用可以不用基于字符串了,同时因为concent的module是包含多个可选定义项的,分离它们有利于后期维护和扩展。

├── modules
    ├── foo
        ├── state.js
        ├── reducer.js
        ├── computed.js
        ├── watch.js
        ├── init.js
        ├── index.js
    ├── bar
        ├── ...

此时reducer文件里,调用可以基于函数引用了

// code in models/foo/reducer.js
export function changeName(name) {
  return { name };
}

export async function  changeNameAsync(name) {
  await api.track(name);
  return { name };
}

export async function changeNameCompose(name, moduleState, actionCtx) {
  await actionCtx.setState({ loading: true });
  await actionCtx.dispatch(changeNameAsync, name);//基于函数引用调用  return { loading: false };
}

调用reducer函数

实例上dispatch触发

在类组件实例里直接通过实例的this.ctx.dispatch触发

@register('foo')
class FooComp extends Component {
  changeName = (e)=>{
    // this.setState({name:e.currentTarget.value})

    this.ctx.dispatch('changeName', e.currentTarget.value);
    // or this.ctx.dispatch('changeNameAsync', e.currentTarget.value);
    // or this.ctx.dispatch('changeNameCompose', e.currentTarget.value);
  }
}

在类组件实例里直接通过实例的this.ctx.settings去呼叫预先定义好的函数触发

@register('foo')
class FooComp extends Component {
  $$setup(ctx){
    return {
      changeName : e=> ctx.dispatch('changeName', e.currentTarget.value),
      changeNameAsync : e=> ctx.dispatch('changeNameAsync', e.currentTarget.value),
      changeNameCompose : e=> ctx.dispatch('changeNameCompose', e.currentTarget.value),
    };
  }
  render(){
    //将它们绑定在具体的dom上
    const { changeName, changeNameAsync, changeNameCompose } = this.ctx.settings;
  }
}

在函数组件实例里直接通过实例的ctx.dispatch触发

function FooComp(){
  //从useConcent接口返回的实例上下文里解构出dispatch
  const { dispatch } = useConcent('foo');

 //将它们绑定在具体的dom上
  const changeName = e=> ctx.dispatch('changeName', e.currentTarget.value);
  const changeNameAsync = e=> ctx.dispatch('changeNameAsync', e.currentTarget.value);
  const changeNameCompose = e=> ctx.dispatch('changeNameCompose', e.currentTarget.value);
}

在函数组件实例里直接通过实例的ctx.settings去呼叫预先定义好的函数触发

//定义setup,该函数只会在组件初次渲染前被调用一次
const setup = ctx=>{
  return {
    changeName : e=> ctx.dispatch('changeName', e.currentTarget.value),
    changeNameAsync : e=> ctx.dispatch('changeNameAsync', e.currentTarget.value),
    changeNameCompose : e=> ctx.dispatch('changeNameCompose', e.currentTarget.value),
  };
}

function FooComp(){
  //从useConcent接口返回的实例上下文里解构出settings
  const { settings } = useConcent({module:'foo', setup});

  //将它们绑定在具体的dom上
  const { changeName, changeNameAsync, changeNameCompose } = this.ctx.settings;
}

实例上reducer触发

在实例上,除了dispatch接口基于字符串的方式去定位具体的reducer函数,也提供
ctx.reducer.{moduleName}.{functionName}的方式去调用具体的reducer函数

ctx.reducer调用限制

出于性能考虑,concent不会为所有实例绑定所有模块的reducer函数到上下文实例中,所以你只能调用组件所属模块或者所连接模块的reducer函数,如果想调用其他模块的reducer函数,请使用dispatch

class组件调用ctx.reducer.{moduleName}.{functionName}

@register('foo')
class FooComp extends Component {
  $$setup(ctx){
    return {
      changeName : e=> ctx.reducer.foo.changeName(e.currentTarget.value),
    };
  }
}

function组件调用ctx.reducer.{moduleName}.{functionName}

//定义setup,该函数只会在组件初次渲染前被调用一次
const setup = ctx=>{
  return {
    changeName : e=> ctx.reducer.foo.changeName(e.currentTarget.value),
  };
}

function FooComp(){
  //从useConcent接口返回的实例上下文里解构出的settings取到changeName
  const { settings: {changeName} } = useConcent({module:'foo', setup});
}

reducer函数内部触发

concent会为每一个reducer函数的第三个参数注入actionCtx对象,使用此actionCtx.dispatch来触发其他的reducer函数

// code in models/foo/reducer.js

export async changeNameAsync(name){
  await api.updateName(name);
  return {name}
}

export async function changeNameCompose(name, moduleState, actionCtx) {
  await actionCtx.setState({ loading: true });
  await actionCtx.dispatch(changeNameAsync, name);//基于函数引用调用  return { loading: false };
}

全局上下文dispatch触发

concent暴露了顶层接口dispatch,可以直接使用dispatch(type:string, payload:any)来触发具体的reducer函数

import cc, { dispatch } from 'concent';

cc.dispatch('foo/changeName', 'newName');
// or
dispatch('foo/changeName', 'newName');
全局上下文dispatch、实例上下文dispatch 区别

1 全局上下文dispatch必需指定具体的模块,实例上下文dispatch可以不用指定具体的默认,默认调用自己的所属模块。

2 全局上下文dispatch触发的函数生成的新状态不能包含私有状态,实例上下文dispatch触发的函数生成的新状态可以包含实例的私有状态。

全局上下文reducer触发

支持调用reducer.{moduleName}.{functionName}触发reducer

import cc, { reducer } from 'concent';

reducer.foo.changeName('newName');
// or
cc.reducer.foo.changeName('newName');

全局上下文reducer、实例上下文reducer区别同上

reducer函数调用链

reducer函数的源头触发一定是从实例上下文ctx.dispatch或者全局上下文cc.dispatch(or cc.reducer)开始的,呼叫某个模块的某个reducer函数,然后在其reducer函数内部再触发的其他reducer函数的话,会形成一个调用链,正常情况下每一个返回了新的部分状态的reducer函数都会触发一次相关实例渲染,这本来也是符合我们预期的结果,但是如果遇到某些特殊场景,我们的reducer函数粒度拆得很细很原子,每一个都负责独立更新某一个和某几个key的值,以便更灵活的组合它们来完成高度复用的目的,虽然代码结构上变优雅了,但是因为这些reducer函数多了之后其实会触发多次渲染,而每一个reducer函数仅更新了一两个值。
concent针对这种调用链提供lazy特性,以既能够达到缩小渲染次数,又支持reducer函数细粒度拆分的理想效果。

以下reducer调用链调用未启用lazy特性之前

//reducer fns
export async function updateAge(id){
  // ....
  return {age: 100};
}

export async function trackUpdate(id){
  // ....
  return {trackResult: {}};
}

export async function fetchStatData(id){
  // ....
  return {statData: {}};
}

// compose other reducer fns
export async function complexUpdate(id, moduleState, actionCtx) {
  await actionCtx.dispatch(updateAge, id);
  await actionCtx.dispatch(trackUpdate, id);
  await actionCtx.dispatch(fetchStatData, id);
}

触发的源头代码

// in your view
<button onClick={()=> ctx.dispatch('complexUpdate', 2)}>复杂的更新</button>

触发的更新流程如下图所示 dispatch

启动lazy只需要在源头调用处将dispatch替换为lazyDispatch

// in your view
<button onClick={()=> ctx.lazyDispatch('complexUpdate', 2)}>复杂的更新</button>

则触发的更新流程将变为 dispatch

concent将延迟reducer函数调用链上所有reducer函数触发ui更新的时机,仅将他们返回的新部分状态按模块分类合并后暂存起来,最后的源头函数调用结束时才一次性的提交到store并触发相关实例渲染。

当然lazyScope也是可以自定义的,不一定非要在源头函数上就开始启用延迟特性。

// in your view
const a=  <button onClick={()=> ctx.dispatch('complexUpdateWithLoading', 2)}>复杂的更新</button>

// in your reducer
export async function complexUpdateWithLoading(id, moduleState, actionCtx) {
  //这里会实时的触发更新
  await actionCtx.setState({ loading: true });

  //从这里开始启用lazy特性,complexUpdate函数结束前,其内部的调用链都不会触发更新
  await actionCtx.lazyDispatch(complexUpdate, id);

  //这里返回了一个新的部分状态,也会实时的触发更新
  return { loading: false };
}

当然在实例上触发lazy除了lazyDispatch,调用还有
ctx.dispatch(type:string, payload:any, {lazy:true})
全局上下文也可以触发lazy
cc.reducer.{moduleName}.{fnName}(payload:any, {lazy:true})

为了方便读者进一步理解lazy特性,可以点击此处查看在线示例