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