重磅宣布, concent 2.0发布, 依赖收集&惰性计算

1,315 阅读5分钟

夜已经很深了,这个时间宣布2.0版本算是一个抢先预告吧,接下来的一个月里会重点开始更新文档以及铺开更多的示例了。

两个版本的差异

1.* 版本为了尽量向上兼容古老的浏览器,没有使用任何新的es特性,但是时代的洪流一定是向前走的,vue 3.0也一定会今年登陆,所以终结 1.*版本算是为了顺势而为,跟着潮流走,才能让开发者拥有最好的体验,2.*启用defineProperty重构了延迟计算特性,启用Proxy重构了获取依赖的相关逻辑,具体区别见下面介绍

依赖标记(声明组件时) vs 依赖收集(实例渲染时)

1.*时代,组件的更新依赖是人工维护的,我们大致复盘一下

  • 定义foo模块
import { register, useConcent } from 'concent';

// 定义foo模块
run({
  foo: {
    state: {
      f1: "f1",
      f2: "f1",
      user: { name: "zzk", age: 19 }
    }
  }
});

-类组件写法,通过watchedKeys标识当前组件关心f1,f2,user三个key的变化

@register({ 
    module: "foo", 
    watchedKeys: ["f1", "f2", "user"]
 })
class ClassComp extends Component {
  state = { hidden: false };
  render() {
    console.log("ClassComp", this.ctx.renderCount);
    const { state, sync, syncBool } = this.ctx;
    // state === this.state
    return (
      <div style={{ border: "1px solid red", padding: "6px", margin: "6px" }}>
        {!state.hidden ? <span>name: {state.user.name}</span> : ""}
        <br />
        {!state.hidden ? <span>age: {state.user.age}</span> : ""}
        <br />
        {!state.hidden ? (
          <input value={state.user.name} onChange={sync("user.name")} />
        ) : (
          ""
        )}
        <br />
        <button onClick={syncBool("hidden")}>toggle hidden</button>
      </div>
    );
  }
}
  • 函数组件写法,同样通过watchedKeys标识当前组件关心f1,f2,user三个key的变化
// 函数组件写法
const iState = () => ({ hidden: false });

function FnComp() {
  const ctx = useConcent({
    module: "foo",
    state: iState,
    // watchedKeys: ["f1", "f2", "user"]
  });
  console.log("FnComp ", ctx.renderCount);

  const { state, sync, syncBool } = ctx;
  return (
    <div style={{ border: "1px solid red", padding: "6px", margin: "6px" }}>
      {!state.hidden ? <span>name: {state.user.name}</span> : ""}
      <br />
      {!state.hidden ? <span>age: {state.user.age}</span> : ""}
      <br />
      {!state.hidden ? (
        <input value={state.user.name} onChange={sync("user.name")} />
      ) : (
        ""
      )}
      <br />
      <button onClick={syncBool("hidden")}>toggle hidden</button>
    </div>
  );
}

实例化这两个组件

import { setState } from 'concent';

export default function() {
  return (
    <>
      <ClassComp />
      <FnComp />
      <button
        onClick={() =>
          // 此处通过concent的顶层api修改foo模块的user字段值
          setState("foo", { user: { name: Date.now(), age: Math.random() } })
        }
      >call top api to change foo module state</button>
    </>
  );
}

如果在1.*版本里,我们操作ClassComp组件实例或者FnComp组件实例的toggle hidden按钮,其实视图里已经不再使用到user这个属性了,但是我们我们如果外部任意地方修改了use属性都会触发它们渲染。

2.*保留了1.*版本的特性,同时修改了默认规则,1.*版本不指定watchKeys默认表示关心当前所属模块的所有stateKey变化,2.*版本则改为自动收集机制,即每一轮渲染动态的收集当前实例的关心key依赖,进一步提高性能。

所以属于某个模块的组件

funciton OneComp(){
    const ctx = useConcent('foo');
    const { f1, f2 } = ctx.state;//此轮渲染的依赖为f1, f2
}

或者连接了其他几个模块的组件

funciton OneComp(){
    const ctx = useConcent({connect:['foo', 'bar']})
    const { foo, bar } = ctx.connectedState;
    const { f1, f2 } = foo;
    const { b1 }  = bar;
    //此轮渲染的依赖为foo模块的f1, f2,bar模块的b1
}

类组件写法也一样

//定义此组件属于counter模块
@register('counter')
class OneComp extends React.Component{
    add = ()=> this.setState({count:this.count+1})
    render(){
        //此轮渲染的依赖是count
        return <h1>{this.state.count}</h1>
    }
}

它们都将自动收集到相关的依赖,同时如果你强行修改状态的值,将会静默失败并给出警告,让你必需用setStatedispatch去修改(注sync系列api是setState的包装语法糖,不算违反规则)

点击这里查看此示例

注意新版React为了安全的启用异步渲染,在开发模式有double-invoking双调用检查机制(生产模式不会有此情况),所以示例里class有一次多余的渲染是正常现象。

gif图在线示例

延迟计算之方法获取 vs 属性获取

1.*新增了惰性计算特性,即依赖某个stateKey的computed函数,在stateKey发生变化时,不触发计算,仅标记需要重新计算,待用户真正读取时才计算,并标记已计算同时缓存计算结果

同样我们先定义模块

import { register, run, useConcent, defLazyComputed } from "concent";

// run concent with a module named counter
run({
  counter: {
    state: { count: 12, msg: "--" },
    reducer: {
      inc(payload, moduleState, actionCtx) {
        const curCount = payload !== undefined ? payload : moduleState.count;
        return { count: curCount + 1 };
      },
    },
    computed: {
      // when count changed trigger will this fn execute
      count(n, o, f) {
        return n.count * 10;
      },
      // when count changed and read heavyCount will trigger this fn execute
      heavyCount:defLazyComputed((n)=>{
        return n.count * 1000000;
      }, ['count'])
    }
  }
});
  • 1.*版本获取计算结果方式如下
// define a class component that belong to 'counter' module
@register("counter")
class Counter extends Component {
  add = () => this.ctx.dispatch("inc");
  render() {
    const { moduleComputed } = this.ctx;
    return (
      <div>
        count: {this.state.count}<br/>
        heavy count: {moduleComputed.heavyCount()}<br/>
        ten*count: {moduleComputed.count}  <br />
        <button onClick={this.add}>add</button>
      </div>
    );
  }
}

// define a function component that belong to 'counter' module
function FnCounter() {
  const ctx = useConcent("counter");
  const add = () => ctx.dispatch("inc");
  const {state, moduleComputed} = ctx;
  return (
    <div>
      count: {state.count}<br/>
      heavy count: {moduleComputed.heavyCount()}<br/>
      ten*count: {moduleComputed.count}  <br />
      <button onClick={add}>add</button><br/>
      msg: {ctx.state.msg}
    </div>
  );
}

可以看到非延迟计算的可以通过属性直接获取,而延迟计算属性则通过函数获取,所以对于1.*的类型定义则是通过ComputedValType获取时则是根据是否延迟而返回值类型or函数类型

类型返回结果

  • 2.*基于defineProperty重构后,则可以让用户直接直接通过属性获取计算结果了,更符合书写直觉习惯
//  heavy count: {moduleComputed.heavyCount()}<br/>
改为
//  heavy count: {moduleComputed.heavyCount}<br/>

同样的,类型提示页更加直接和简单,不再是一个函数

结语

concent迭代升级的目标和react目标一样,为开发者提供极致的开发体验,为用户提供极致的使用体验,在新的版本里不考虑兼容古老浏览器后,新的es特性将让concent如虎添翼,亲爱的读者,如果你喜欢,还请star和关注concent,抢先体验下2.*版本.

⚖ 其他帮助你快速了解的在线示例