浅析mobx-react源码(一)—自动追踪依赖

使用一种工具就像使用一个包装好的黑盒子,我们不必探究其内部到底是如何实现,只需要能够将用法了然于胸,什么样的输入会得到什么样的输出能有完美的预测。但不幸的事,大部分文档都难以让自己达到这点,为此我们不得不浅析一下源码,来探寻他是如何实现的,避免出现意料之外的情况导致bug,也防止做出多余的操作,精简代码,减少bug。

自动追踪依赖

在使用redux的时候,我们不得不使用connect来从store中取到当前组件所需要的state,这其实也是一个依赖分析的问题,只有当组件所依赖的state变化时,当前组件才会更新,避免了不必要的render。而在使用mobx时,这一步被自动完成了,因此在使用mobx时,会感到极其酸爽舒适,只需要一个@observer就能尽情地使用store中的state了,而且完全不用担心性能问题,那么这究竟是如何做到的?

了解mobx机制

在探究mobx-react的源码之前,得先了解一下mobx。

import { Reaction, observable } from 'mobx';
const obj = observable({ num: 0 });
const reaction = new Reaction('reaction name', function () {
  this.track(() => {
    console.log('track num++', obj.num);
  });
});
reaction.track(() => {
  console.log('reaction init', obj.num);
});
// 输出 reaction init 0

obj.num++; // 输出 track num++ 1
obj.num++; // 输出 track num++ 2

mobx在observable改造了{ num: 0 }这个对象为可观察对象,当取obj的属性num时,能会执行其内部的方法。
reaction的track方法传进去的回调中,执行console.log('reaction init', obj.num)时,get到了obj.num值,这时mobx会将这个属性作为依赖与此reaction绑定,当obj.num变化时,就会执行实例化reaction时传进去进去的回调

function () {
  this.track(() => {
    console.log('track num++', obj.num);
  });
}

这时会输出track num++ 1,并从新确定依赖关系。接下来看下一个例子

import { Reaction, observable } from 'mobx';
const obj = observable({ num: 0, letter: 'a', bool: true });
const callback = function () {
  if (obj.bool) {
    console.log(obj.num);
  } else {
    console.log(obj.letter);
  }
};
const reaction = new Reaction('reaction name', function () {
  this.track(callback);
});
reaction.track(callback); // 第一次track,分析到依赖[obj.bool, obj.num],输出 0
obj.num++; // 第二次track,分析到依赖依然是[obj.bool, obj.num],输出 1
obj.letter = 'b'; // obj.letter不在依赖中,不执行this.track(callback)
obj.bool = false; // obj.bool在依赖中,执行this.track(callback);重新分析依赖[obj.bool, obj.letter] 输出 b
obj.num++; // 此时obj.num不在依赖中,不输出任何值
obj.letter = 'c'; // 此时obj.letter在依赖中,输出 c

由此可见,mobx可以在每次执行时会分析依赖,并且将在callback中,却并不执行的属性排除在依赖外,减少了callback。
看到这里,我们应该能想到,只要将callback当成react的render,就能完美地解决对react的对接,并且比使用redux的时候性能更好!因为使用redux的时候将依赖num,letter,bool三个属性,无论哪个改变都讲触发组件的render,除非根据业务逻辑写上复杂的componentShouldUpdate,而使用mobx时,只要其中某个state的改变不影响view的改变时,这个state的变化就不会引起组件render,达到更细粒度上对render的控制,近乎完美地排除不需要的render。

mobx-react源码片段探究

那么接下来看一看mobx-react的源码是如何实现的

// 使用mobx-react
import React, { Component} from 'react';
import { observer } from 'mobx-react';
@observer
export default MyCom extends Component {
  render() {
    // ...do something
  }
}
// mobx-react的源码片段
// ...
const baseRender = this.render.bind(this) // 组件自己的render就是baseRender
let reaction = null
let isRenderingPending = false
// 定义initialRender,用来代替组件第一次render
const initialRender = () => {
  reaction = new Reaction(`${initialName}#${rootNodeID}.render()`, () => {
      if (!isRenderingPending) {
          // 防止在constructor中修改store触发引起的副作用(此时还没有初始render)
          isRenderingPending = true

          // 触发mobx-react添加的一个生命周期
          if (typeof this.componentWillReact === "function") this.componentWillReact() 

          // 防止componentWillRect引起的副作用
          if (this.__$mobxIsUnmounted !== true) {
              let hasError = true
              try {
                  isForcingUpdate = true
                  // skipRender是用来防止死循环,这里不用管
                  // 当依赖的state有变化是会使用forceUpdate强制render
                  // 并解析新的依赖
                  if (!skipRender) Component.prototype.forceUpdate.call(this)
                  hasError = false
              } finally {
                  isForcingUpdate = false
                  if (hasError) reaction.dispose()
              }
          }
      }
  })
  reaction.reactComponent = this
  reactiveRender.$mobx = reaction
  // 用reactRender替代render
  this.render = reactiveRender
  // 执行reactiveRender
  return reactiveRender()
}

const reactiveRender = () => {
  isRenderingPending = false
  let exception = undefined
  let rendering = undefined
  reaction.track(() => {
      if (isDevtoolsEnabled) {
          this.__$mobRenderStart = Date.now()
      }
      try {
          // 允许state改变,然后执行baseRedner,这里将解析一次render中对state属性的依赖
          rendering = extras.allowStateChanges(false, baseRender)
      } catch (e) {
          exception = e
      }
      if (isDevtoolsEnabled) {
          this.__$mobRenderEnd = Date.now()
      }
  })
  if (exception) {
      errorsReporter.emit(exception)
      throw exception
  }
  return rendering
}
// ...

这样就达到了用mobx控制react组件的render。

结论

对这一段代码我们可以知道,仅仅在组件上加一行@observer然后使用mobx的可观察属性进行render控制view,就能达到性能的高效化,对单个state属性的粒度上控制组件的render,所以放心大胆地用mobx来管理应用的state吧!

下一篇 分批处理变化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容