模块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>
  )
}