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

模块computed

computed可以定义多个计算结果键retKey,每一个retKey对应一个计算函数,首次载入模块时,将按定义顺序依次执行完所有的计算函数并将其结果缓存起来。

type ComputedFn = (
  oldState:any,
  newState:any, 
  fnCtx:FnCtx,
)=> any;

type ComputedFnDesc = {
  fn: ComputedFn,
  compare?: boolean,
  depKeys?: string[],
}

type ComputedValueDef = ComputedFn | ComputedFnDesc;

读者可fork此在线示例做修改来加深理解。

依赖收集

在首次载入模块执行完所有计算函数时,依赖收集系统将收集到各个计算函数的依赖列表depKeys,此后对于任意一个retKey来说,仅当它的depKeys里的对应的值再次变化时,才会再次触发它的计算函数

我们可以在子模块computed属性的对象里定义计算,key就是获取计算结果的retKey,value就是computed函数computed描述体

import { run } from 'concent';

run({
  foo:{
    state: {
      firstName:'Jim',
      lastName:'Green',
      nickName:'xx',
    },
    computed:{
      // firstName收集到的的依赖是 ['firstName']
      firstName: (n)=> n.firstName.split('').reverse().join(''),
      // fullName收集到的依赖是 ['firstName', 'lastName']
      fullName: (n)=> `${n.firstName}_${n.lastName}`,
      // funnyName基于fullName的计算结果做2次计算,收集到的依赖是 ['nickName', 'firstName', 'lastName']
      funnyName: (n, o, f)=> `${n.nickName}_${f.cuVal.fullName}`,
    }
  }
});

当然,建议的做法是为模块单独定义一个计算函数文件,导出来给模块用

// code in models/foo/computed.js
export function firstName(newState){
  return newState.firstName.split('').reverse().join('');
}

export function fullName(newState){
  return `${newState.firstName}_${newState.lastName}`;
}

// 这里的函数书写顺序很重要,因为funnyName需要用到fullName计算结果,必需写在fullName函数之后
export function funnyName(n, o, f){
  return `${n.nickName}_${f.cuVal.fullName}`;
}

依赖标记

对于复杂的计算函数,用户需要将参与计算的因子提前声明在函数块的头部,方便concent载入模块执行完所以的计算函数时收集正确的依赖

concent只会在首次执行所有的计算函数时收集计算依赖,此后便不再有收集行为产生,所以要求用户提前把所有可能参与计算的因子声明在函数块的头部

错误的复杂计算示例

// 收集的依赖可能是 ['nickName', 'lastName']
// 也可能是 ['nickName', 'firstName']
export function complexName(n, o, f){
  if(n.nickName == 'xx'){
    return `${n.nickName}_${n.lastName}`;
  }else{
    return `${n.nickName}_${n.firstName}`;
  }
}

如果用户明确依赖关系的话,concent支持使用depKeys参数人工标记依赖关系列表,以上示例可修正为

export const complexName = {
  fn:()=>{
    if(n.nickName == 'xx'){
      return `${n.nickName}_${n.lastName}`;
    }else{
      return `${n.nickName}_${n.firstName}`;
    }
  },
  depKeys:['nickName', 'firstName', 'firstName']
}

如果不想显示的标记依赖,则需要按照约定将参与计算的因子提前声明在函数块的头部,所以正确的复杂计算示例应该如下

export function complexName(n, o, f){
  const { firstName, lastName, nickName } = n;
  if(nickName == 'xx'){
    return `${nickName}_${lastName}`;
  }else{
    return `${nickName}_${firstName}`;
  }
}

同名计算结果键

当计算结果名retKey和模块状态里的某个stateKey同名时,concent则会为此retKey自动生成一个包含此stateKey的依赖列表

  • 函数体内无其他任何依赖时
// code in models/foo/computed.js

// 此函数体内没有用到任何其他计算因子参与计算,因`retKey`和`stateKey`同名,
// concent为它的生成的依赖列表为['firstName']
export firstName(){
  return `just_for_fun_${Date.now()}`;
}
  • 函数体内包含其他依赖时
// 因`retKey`和`stateKey`同名,concent为它的生成的依赖列表为['firstName']
// 同时函数体内还有一个依赖['LastName']
// 所以最终计算函数firstName的依赖为 ['firstName', 'lastName']
export firstName(n)){
  return `just_for_fun_${n.LastName}`;
}

如果想要关闭此规则,有以下几种方式

  • 让函数保持依赖收集状态,只是关闭同名计算结果依赖规则
import { defComputed } from 'concent';

// 设置retKeyDep为false,表示关闭同名计算结果规则
export const firstName = defComputed(()=>{
  return `just_for_fun_${n.lastName}`;
}, { retKeyDep:false })
  • 显式地传递depKeys列表

此时函数处于依赖标记状态,你需要根据函数体内具体用到的状态键来标记依赖

一个无任何依赖的函数

import { defComputed } from 'concent';

export const firstName = defComputed(()=>{
  return `just_for_fun_${Date.now()}`;
}, []) // 显示的指定依赖列表为空

// or defComputed(fn, {depKeys:[]}

对于这种只会有一次机会被触发并计算结果的零依赖函数,我们称之为静态计算函数,还可以使用defComputedVal来定义

import { defComputedVal } from 'concent';
export const firstName = defComputedVal(`just_for_fun_${Date.now()}`);

如果函数体内用到了具体的状态键,需要人工标记依赖

import { defComputed } from 'concent';

export const firstName = defComputed(()=>{
  return `just_for_fun_${n.lastName}`;
}, ['lastName'])

// or defComputed(fn, {depKeys:['lastName']}
推荐使用依赖收集策略

处于依赖收集状态的函数会自动收集函数体用到的依赖,从而避免了人工维护因遗漏了某个依赖导致出错的机会

触发计算

计算的触发时机有两个

  • 模块被加载时,所有计算函数都会被触发(和该模块下有没有相关的组件被实例化没有关系)
  • 模块的某些状态被改变时,按各个retKey依赖列表挑出需要执行的计算函数并逐个执行
注意相同引用的比较

当key对应的应该是primitive类型的(如number, string, boolean)时,新状态和旧状态的比较总是如我们预期的那样成立,但是如果是object型,则需要总是返回新的引用才能触发计算

// code in models/foo/computed.js

//hobbies在模块状态里对应的值是一个数组
export function hobbies(n, o) {
  return n.hobbies.length * 2;
}

// **********************************************

// code in models/foo/reducer.js

export function addHobby(hobby, moduleState){
  const { hobbies } = moduleState;  hobbies.push(hobby);
  // return { hobbies };不会触发hobbies的计算函数
  return { hobbies: [...hobbies] };//正确的写法
}

如果需要return { hobbies }能触发计算,可将其计算定义写为计算描述体,并设置compare为false,表示只要对这个key设了值就触发计算

export const hobbies = {
  fn: (n) => n.hobbies.length * 2,
  compare: false,//不做比较,只要片段状态里对设了`hobbies`的值,就触发计算}

或者启动concent时,全局配置computedCompare参数为false,表示只要有设值行为就触发计算

该配置对所有模块的计算函数均有效,如果此时想针对某些计算函数失效,单独为其配置compare为true即可

run({
  foo:{...},
},{
  computedCompare: false, // 默认为true
})

获取计算结果

通过实例上下文ctx.moduleComputed属性下的对象去获取,该对象的key就是computed里定义的各个retKey

  • 类组件里获取
import { register } from 'concent';

@register('foo')
class Foo extends Components{
  render(){
    const moduleComputed = this.ctx.moduleComputed;

    return <h1>{moduleComputed.fullName}</h1>
  }
}
动态的依赖收集

渲染函数里的依赖收集是每一轮渲染过程中都在实时收集的,如上面例子,因fullName的依赖是['firstName', 'lastName'],所以当前实例的依赖是['firstName', 'lastName'],当用户额外加一个开关,在某一次渲染不再读取fullName时,则当前实例依赖列表为空,这意味着其他任意地方修改了模块的firstName值时,尽管触发了模块计算函数fullName重计算,但是不会触发改实例重渲染

@register('foo')
class Foo extends Components{
  state = {show:true};
  render(){
    const { show } = this.state;
    const { moduleComputed, syncBool }= this.ctx;

    return (
      <div>
        {/**当show为false时,当前实例的依赖列表为空*/}
        {show ? <h1>{moduleComputed.fullName}</h1> : ''}
      </div>
    )
  }
}
  • function组件里获取
import { useConcent } from 'concent';

export default Foo(){
  const { moduleComputed } = useConcent('foo');

  return <h1>{moduleComputed.fullName}</h1>
}

失去依赖的函数组件写法示范

const iState = ()=>({show:true});
export default Foo(){
  const {
    state, moduleComputed, syncBool 
  } = useConcent({module:'foo', state:iState});

  return (
    <div>
      {/**当show为false时,当前实例的依赖列表为空*/}
      {state.show ? <h1>{moduleComputed.fullName}</h1> : ''}
    </div>
  )
}