g770728y / valor-blog

下里巴人的个人博客
4 stars 1 forks source link

[ mobx ] 进阶 #7

Open g770728y opened 5 years ago

g770728y commented 5 years ago

autorun vs computed

autorun: observable value有赋值, 则运行一次 ( 无论是否有变动 ) computed: observable value 有变动, 则运行一次

const obj = observable({ a: 1, b: 1 });
autorun(() => {
  console.log('by autorun', JSON.stringify(obj));  
});
computed(() => ({ a: obj.a + 1, b: obj.b + 1 })).observe(change =>
  console.log('computed:', change.newValue)
);

obj.a = 1;   <== 输出 `by autorun ...`
obj.a = 2;  <== 输出`by autorun...  by computed...`

应尽量使用computed

g770728y commented 5 years ago

runInAction / @action 会批量更新

修改const obj = observable({a:1, b:1})有两种做法:

const obj = observable({ a: 1, b: 1 });
autorun(() => {
  console.log('by autorun', JSON.stringify(obj));  
});

// 直接赋值会多次响应
obj.a = 1;   <== 第一次打印
obj.a = 2;  <== 第二次打印
obj.b = 3; <== 第三次打印

// action赋值只响应一次
runInAction(() => {
    obj.a = 1;   <== 第一次打印
    obj.a = 2;  <== 第二次打印
    obj.b = 3; <== 第三次打印
});   <= 只打印一次

action可以嵌套, 多层action自动优化为一次批量

class Store { x = []

constructor() {
    // 观察这里会打印多少次
    reaction(()=>this.x.length, len => console.log(len));
}

@action pushX() { this.x.push(1) }

@action pushX2() {this.pushX(); this.pushX()}    <== 将只触发单次reaction!!!

}

g770728y commented 5 years ago

createTransformer

一句话: 它是computed的带参数版本 ( computed不可带参 ) , 实现与computed类似的功能

问题

store, 注意: get 方法

class Store {
  @observable
  xs: { id: number; a: number }[] = [{ id: 1, a: 1 }, { id: 2, a: 2 }];

  get = (id: number) => {
    return this.xs.find(it => it.id === id)!;
  };
}

const store = new Store();

组件C, 绑定到 store.xs里的id上:

const C: React.FC<{ id: number }> = 
  observer(({ id }) => {
    const x = store.get(id);
    console.log('id render', id);
    return <div>{x.a}</div>;
  })

组件List, 绑定到id列表上:

const List: React.FC = observable(() => {
  console.log('app render');
  return (
    <div>
      <button onClick={() => store.xs.push({id:3,a:3})}>push</button>
      <button onClick={() => store.xs.pop()}>delete</button>
      <button onClick={() => {store.xs[0].a = 5}}>change</button>
      <hr />
      {store.xs.map(it => (
        <C key={it.id} id={it.id} />
      ))}
    </div>
  );
})

当你点击上面的push按钮时, 由于id增加了, 所以List肯定刷新, 但 <C id=1>, <C id=2>会刷新吗? 照理说它们的id并未变化啊.

答案是: 会刷新 ( 原因待分析 )

显然, 对于大量结点的应用, 这是不可接受的.

解决

思考: 如果我们使用了:

@computed
getFirst() {
    return this.xs[0]
}

那么 当且仅当 getFirst()的返回值发生变化时, 才会引起刷新.

可是, 上面的get方法带id参数, 无法使用computed

幸好, mobx-utils提供了createTransformer方法, 用法如下:

class Store {
  @observable
  xs: { id: number; a: number }[] = [{ id: 1, a: 1 }, { id: 2, a: 2 }];

  get: (id: number) => { id: number; a: number };

  constructor() {
    this.get = createTransformer(this._get);
  }
  _get = (id: number) => {
    return this.xs.find(it => it.id === id)!;
  };
}

之后, 其它代码不用修改, 你会发现: <C id=1> <C id=2> 不再无效刷新

原因: computed 实现了一个内部缓存: 当args发生变化时, 才会重新计算 createTransformer也实现了一个内部缓存: 它为每个args提供一个缓存, 所以基本原理是相通的. 当然, 也正因为如此, 如果某个args不再可用, 那么缓存显然应该清除

g770728y commented 5 years ago

多个computed可以保证依赖关系

回顾 之前使用到computed2依赖computed1的情形 当时不知为何, computed2用到的始终是computed1的旧值 所以一直认为computed无法正确解析依赖关系

今天仔细想了下, 感觉不太对, 重新验证了下:

class Store {
  @observable
  obj = { a: 1 };

  @computed
  get obj_a12() {
    const result = this.obj_a1 + this.obj_a2;
    console.log('obj_a12');
    return result;
  }

  @computed
  get obj_a1() {
    const result = this.obj.a + 1;
    console.log('obj_a1');
    return result;
  }

  @computed
  get obj_a2() {
    const result = this.obj_a1 + 1;
    console.log('obj_a2');
    return result;
  }
}

const store = new Store();
autorun(() => {
  console.log('this.obj_a1', store.obj_a1);
});

输出的store.obj_a1始终正确 打印出的顺序也始终正确 并且考虑到: obj_a12在使用this.obj_a1时, 肯定会去get obj_a1取值, 必须重算(或取当前缓存) 所以, 至少在简单的a依赖b这种简单情形, 顺序是不会错的.


补充: 可以简单证明如下:

g770728y commented 5 years ago

@observable vs observable(...)

一直以为这是等价的, 直到今天才明白: 并不等价!!!

class Store {
// 以下的structure1 / structure2 是等价的吗? 初看起来貌似等价
  @observable
  structure1 = { blocks: [0] };

  structure2: any = observable({ blocks: [0] });

  constructor() {
   // 确实都打印出 Proxy,  看起来也没问题
    console.log('structure1', this.structure1);  => proxy
    console.log('structure2', this.structure2);  => proxy

    // 这样就看出差异来了: 
    this.structure1 = { blocks: [2] };
    console.log('1:', this.structure1.blocks);   => proxy, ok!    
    this.structure2 = { blocks: [2] };    <== 注意这里的赋值方法与上面一模一样
    console.log('2:', this.structure2.blocks);   =>  [2], 非blocks!!!!
  }
}

const store = new Store();

请仔细看上面的代码. 造成的差异就是: 整体赋值后, structure2 就变成了 普通对象!!!


补充: 奇怪的是: 如果你使用this.structure2.blocks = [2] , 也就是属性赋值 而非整体赋值方式, 又不出会出现问题.


结论: 全部采用 @observable方式

g770728y commented 5 years ago

数组的replace方法与=方法

对于以下数组: @observable x = [{a:1}];

我们对其进行观察:

computed(()=>x).observe(change => console.log(...))

我们有三种方法对其进行修改: this.x.push({a:2}); <== 不会触发computed! this.x = [...x, {a:2}]; <== 会 触发computed! this.x.replace([...x, {a:2}]) <== 不会触发computed!

可以看到: 第1种和第3种做法, 不会触发computed 原因在于: push/replace 并没有改变x的引用 而=却改变了x的引用, 自然就触发了 computed

由于我们一般不会直接使用 数组, 而是需要进一步加工, 比如取得 x[0] 所以这种做法本质上并没有差异

g770728y commented 5 years ago

computed中打断mobx proxy引发的问题

比如:

@computed
get xs() {
    return [...this.xs];  或   return this.xs.slice(0,2)
}

如果你直接打印 this.xs, 会发现它是一个Proxy 但如果使用了 [...this.xs], 会发现它会输出: [1,2,3...] 对于computed, 它会对比计算前后的值. 对于js:

[1,2,3] !== [1,2,3]
{a:1} !== {a:1}

所以会导致多余的渲染. 我们可以利用React.useMmeo消除之.

g770728y commented 5 years ago

computed / autorun / reaction 使用注意事项

什么时候响应

  1. autorun: 只要对observable进行赋值, 不管值有无改变, 都会触发
  2. reaction / computed: 仅当值真正改变时才触发

真实情况没这么简单. 由于在js中, {} !=={}, {a:1} !== {a:1}, 所以会导致以下问题:

class X {
  @observable 
  xs = [{}, 1];

  constructor() {
    this.disposer = computed(() => toJS(this.xs[0])).observe(change => 
       console.log(change.oldValue, change.newValue);  => {}, {}
  );
  }
}
const store = new X();
store.xs.push(2);      <=== 会触发 computed!!!

computedreaction正确响应

很简单, 它们都有一个参数: equals, 带上 `{ equals: R.equals } 就行了 ( 具体查d.ts ) 这会保证 仅当它们的值发生变化时 才会调用

最好统一进行dispose

否则可能存在潜在bug

它们都不能跨store进行响应!

我记得之前computed可以跨store响应, 但不清楚为何现在不行, 有时间再仔细看下源码.