Once you understand Mobx, the project will never be pitted again _(:3"∠)_

1. Introduction

We often use mobx when writing code every day, and we have encountered some strange problems, so let's take a look at the operation mechanism of mobx today to solve these strange little problems (ps: not mentioned in the article Questions can be added in the comment area)~

First, let's take a look at an actual project case, as shown in the figure below, which is a very common scenario. The page is a left-right layout, the left side is a sidebar that can be expanded and collapsed, and the upper right side is a chart and the lower side is a table .

常见问题After the menu on the left is expanded, the chart in the content area on the right is still the width before the menu is expanded, resulting in a scroll bar, and the chart also exceeds the display area of ​​the viewport. How do you deal with this problem on a daily basis?

image.png

Xiong's solution is to trigger the chart resize method in the content area on the right through mobx to monitor the change of the expanded and collapsed states of the sidebar~

@observable viewState = { isSidebarCollapsed: false };

this.disposer = reaction(
    () => viewState.isSidebarCollapsed,
    () => {
        chartResize();
    }
);
复制代码

Have you noticed that Xiong wrote a disposer, which will call this method when the component is unloaded to clean up the reaction~ There is an optimization suggestion for the reaction below, you can learn about it~

优化建议Always clean up reactions, otherwise they will only be garbage collected when all the objects they observe are garbage collected. It's a lot trickier with reactions like autorun, because they may observe many different observables, and as long as one of them is still in scope, the reaction will stay in scope, which means that all other observables it uses also stay active to Support for future recalculations. - Mobx official documentation (always clean up -reaction)

Closer to home, let's take a look at what these lines of mobx code do~ Tips: Because the source code is long for a better reading experience of the article, we will attach the core logic of the source code here, and the specific source code will be attached in in the link~

Second, mobx operating mechanism

2.1 observable

Let's take a look at the source code of the observable first and see what it does~

[Source code] observable

var observable: IObservableFactory = assign(createObservable, observableFactories);
复制代码

从上面的源码,我们可以发现 observable 包含两部分,一部分 createObservable 函数,一部分是  observableFactories 对象。接下来让我们分别看看这两部分。

【源码】createObservable

function createObservable(v: any, arg2?: any, arg3?: any) {
  // @observable someProp;
  if (isStringish(arg2)) {
    storeAnnotation(v, arg2, observableAnnotation)
    return
  }
 
  // already observable - ignore
  if (isObservable(v)) {
    return v
  }
 
  // plain object
  if (isPlainObject(v)) {
    return observable.object(v, arg2, arg3)
  }
 
  // Array
  if (Array.isArray(v)) {
    return observable.array(v, arg2)
  }
 
  // Map
  if (isES6Map(v)) {
    return observable.map(v, arg2)
  }
 
  // Set
  if (isES6Set(v)) {
    return observable.set(v, arg2)
  }
 
  // other object - ignore
  if (typeof v === 'object' && v !== null) {
    return v
  }
 
  // anything else
  return observable.box(v, arg2)
}
复制代码

我们可以发现 createObservable 其实是一个转发函数,它只是把传入的对象根据对象的类型转发到不同的转换函数。

【源码】observableFactories

const observableFactories: IObservableFactory = {
  box(){},
  array(){},
  map(){},
  set(){},
  object<T = any>(props: T, decorators?: AnnotationsMap<T, never>, options?: CreateObservableOptions): T {
    return extendObservable(
      globalState.useProxies === false || options?.proxy === false
        ? asObservableObject({}, options)
        : asDynamicObservableObject({}, options),
      props,
      decorators
    )
  },
  ref: createDecoratorAnnotation(observableRefAnnotation),
  shallow: createDecoratorAnnotation(observableShallowAnnotation),
  deep: observableDecoratorAnnotation,
  struct: createDecoratorAnnotation(observableStructAnnotation)
} as any
复制代码

observableFactories 包含两部分,一部分与 createObservable 相似是各种类型的转换函数,如:box, array, map, set, object,另一部分是一些属性,这几个属性的含义如下:

  • ref:不需要观察属性变化(属性是只读的),频繁变更引用时使用 ref。底层处理禁用自动的 observable 转换,只是创建一个 observable 引用。
  • shallow:只观察第一层数据。底层处理时不进行递归转换。
  • deep:是否开启深度观察。
  • struct:对象每次更新都会触发 reaction,但是有时只是 reference 更新了实际属性内容没变,这就是 struct 存在的意义。struct 会基于 property 做深比较。

为了方便讲解,我们选择一个转换函数继续往下看看这些转换函数做了什么,我们选取日常用到最多的 observable.object 来看一下~ 还是上面那段 observableFactories 的代码,我们可以发现 observable.object 方法实际是调用了 extendObservable 方法,我们顺着往下看看 extendObservable 做了什么。

【源码】extendObservable

function extendObservable<A extends Object, B extends Object>(
  target: A,
  properties: B,
  annotations?: AnnotationsMap<B, never>,
  options?: CreateObservableOptions
): A & B {
  // Pull descriptors first, so we don't have to deal with props added by administration ($mobx)
  const descriptors = getOwnPropertyDescriptors(properties)
 
  const adm: ObservableObjectAdministration = asObservableObject(target, options)[$mobx]
  startBatch()
  try {
    ownKeys(descriptors).forEach(key => {
      adm.extend_(
        key,
        descriptors[key as any],
        // must pass "undefined" for { key: undefined }
        !annotations ? true : key in annotations ? annotations[key] : true
      )
    })
  } finally {
    endBatch()
  }
  return target as any
}
复制代码

顺着源码调用我们来看一下 extendObservable 做了什么事情,它做了如下事情:

1 . 调用 asObservableObject 方法,给 target 挂载 $mobx 属性

2 . 遍历 target 的属性值,让每个属性经过 decorator 改造后重新挂载到 target 上。我们顺着代码看有这样一条调用链:

$mobx.extend_ → defineProperty_ → defineObservableProperty_ → getCachedObservablePropDescriptor

如下所示是 getCachedObservablePropDescriptor 的源码,我们发现它拦截了 target 每个属性的读取操作,并将操作映射到了  m o b x 属性上面,这意味着以后对 t a g e r t 的读取都会映射到 mobx 属性上面,这意味着以后对 tagert 的读取都会映射到 mobx 上。

【源码】getCachedObservablePropDescriptor

function getCachedObservablePropDescriptor(key) {
  return (
    descriptorCache[key] ||
      (descriptorCache[key] = {
        get() {
          return this[$mobx].getObservablePropValue_(key)
        },
        set(value) {
          return this[$mobx].setObservablePropValue_(key, value)
        }
      })
  )
}
复制代码

我们继续往下看看,getObservablePropValue 和 setObservablePropValue_ 究竟做了什么,也就是说我们对 observable 包装的对象做读取操作时它内部做了什么事呢 ~ 往下看我们可以发现这样一条调用链:

getObservablePropValue_ → get_ → has_ → get → reportObserved → queueForUnobservation

这条调用链的重点在 reportObserved 函数,所以我们重点看一下它的源码~

【源码】reportObserved

function reportObserved(observable: IObservable): boolean {
    checkIfStateReadsAreAllowed(observable)
 
    const derivation = globalState.trackingDerivation
    if (derivation !== null) {
        if (derivation.runId_ !== observable.lastAccessedBy_) {
            observable.lastAccessedBy_ = derivation.runId_
            // Tried storing newObserving, or observing, or both as Set, but performance didn't come close...
            derivation.newObserving_![derivation.unboundDepsCount_++] = observable
            if (!observable.isBeingObserved_ && globalState.trackingContext) {
                observable.isBeingObserved_ = true
                observable.onBO()
            }
        }
        return true
    } else if (observable.observers_.size === 0 && globalState.inBatch > 0) {
        queueForUnobservation(observable)
    }
 
    return false
}
复制代码

从读取的源码可以看出,一步步走下来读取会触发 reportObserved,最终将当前对象的属性包装成一个 ObservableValue 对象加入全局变量 isPendingUnobservation_ 中。

到这里我们悟了,这相当于观察者模式啊,对 observable  对象的读取操作就相当于在对应的被观察环境里做了一次依赖收集 ~ 那下面的写值操作会做什么事情我们心里也大概有数了,让我们简单看一下~ 下面是写值操作的调用链,具体源码点击对应函数可跳转至 Github 仓库查看:

setObservablePropValue_prepareNewValue_interceptChangesetNewValue_reportChanged + notifyListeners

在对 observable 对象进行写值操作时,先在写值前进行了拦截操作,在拦截器进行了一些处理。在写值后通知所有的观察者值被更新。emmmm,这里是典型的观察者模式~

小结一下,observable 源码一路看下来我们可以发现:

  1. observable 先采用策略模式根据对象的类型进行转发
  2. 转发到对应的转换函数后通过在对象上挂载 $mobx 属性代理对原对象的操作,这里用到了代理模式
  3. 代理读操作时进行依赖收集,代理写操作时进行观察者的通知,这里用到了观察者模式
  4. 在写值前后安插了两次回调(这里用到了面向切面编程的思想-AOP),写值前的回调像是axios的拦截器(intercept),写值后的回调像是reaction对新值做出响应。

2.2 reaction

emmmm,看完了 observable 干得活,下面我们来一起看看看 reaction 都做了什么吧~

【源码】reaction:emmmm,这里太长了熊就不贴了大家自己跳转过去康康吧~

从 reaction 源码出发一路找下去,我们可以找到这样一条调用栈:

Reaction.schedule_runReactionsrunReactionsHelperrunReaction_ 

我们阅读 runReaction_ 的实现可以发现,它主要做了以下几件事:

顺着 onInvalidate_ 往下找可以找到这样的调用栈:

onInvalidate_  → reactionRunnertracktrackDerivedFunctionbindDependencies

根据这条调用栈,我们可以发现onInvalidate_主要做了以下几件事: 

  • 跟踪任务(track):开启了一层新的事务,并将当前的 reaction 挂载到全局变量 globalState.trackingContext 上
  • 执行任务(trackDerivedFunction):执行 reaction 的 callback,更新 newObserving_ 属性方便后面更新依赖关系
  • 更新依赖(bindDependencies):用 newObserving_ 更新当前 derivation(reaction 运行环境的this)中的 observing_ 属性,完成依赖更新

下面插入一个小小的拓展知识点~


在依赖更新这里涉及到 observing_ 和 newObserving_ 两个数组,正常双层循环去判断的算法时间复杂度是O(n^2),但 mobx 通过3次循环 + diffValue 属性的辅助将复杂度降到了 O(n),该理解引用参考资料1的2.2.3.2节。

1 . 第一次循环遍历 newObserving,利用 diffValue 进行去重,diffValue 初始值为 0,如果 diffValue 为0则将其值修改为1,如果 diffValue 为1说明重复了直接去掉即可,第一次遍历完的结果如下:

image.png

这次遍历后,所有 最新的依赖 的 diffValue 值都是 1 了,而且去除了所有重复的依赖。

2 . 第二次循环遍历 observing, diffValue的值为0说明其不在更新后的依赖里面可以调用 removeObserver 直接去掉,diffValue 的值为1的话将其变为0.

image.png

这一次遍历之后,去除了 observing 中所有陈旧的依赖,且遗留下来的对象的 diffValue 值都是 0 了。

3 . 第三次循环遍历 newObserving,如果 diffValue 为 1,说明是新增的依赖,调用 addObserver 新增依赖,并将 diffValue 置为 0。

image.png


emmmm,看到这里我们简单的了解了 observable 和 reaction 的实现,但我们还不知道 observable 的改变如何触发 reaction 的运行~ 那下面就让我们看一下 mobx 是如何将两者关联到一起的吧~

observable 写值时会触发 reportChanged,reportChanged 内开启了一层新的事务随后调用 propagateChanged,propagateChanged 会遍历该 observable 的观察者列表(即做过 observable 读取操作的 reaction 们,在读取时被加入到 observable 的观察者队列),然后执行每个观察者的onBecomeStale_ 方法,onBecomeStale_ 方法直接调用 reaction 的 schedule_ 方法,也就是把我们上面讲的 reaction 的任务流程重新跑一边~ emmm,到此 observable 和 reaction 的流程就串起来拉~

三、总结

最后让我们一起来回顾一下本文的主要内容。本文开始我们从实际项目侧边栏展开右侧图表区域内容不会重新调整尺寸聊起,然后给出了 mobx 的解决方案,然后根据解决方案中的代码顺着源码往下读。了解了 mobx 的 observable 和 reaction 都做了什么事情。

observable

将传入数据包装成 ObservableValue 对象,并代理其读取和写入操作:

  • 在读取时调用 reportObserved 方法进行依赖收集

(reportObserved 把 observable 放进了 derivation 的 newObserving_ 队列里,derivation 执行到 bindDependencies 时会调用 addObservable 和 removeObservale 更新依赖,addObservable 和 removeObservale 会更新 observable 的 observers_ 属性)

  • 在写入时调用 reportChanged 方法进行通知发放

(reportChanged 调用 propagateChanged,在 propagateChanged 遍历 observable的观察者们-observers_,然后让它们执行各自的 onBecomeStale_ 方法,从onBecomeStale_方法走下去就是把reaction的执行流程全部重新跑一边)

reaction

通过 shouldCompute 判断有没有必要执行,shouldCompute主要做性能的优化,具体实现原理见该文章。如果有必要执行下述任务:

  • 跟踪任务(track):开启了一层新的事务,并将当前的reaction挂载到全局变量 globalState.trackingContext 上
  • 执行任务(trackDerivedFunction):执行reaction的callback,更新 newObserving_ 属性方便后面更新依赖关系
  • 更新依赖(bindDependencies):用 newObserving_ 更新当前 derivation(reaction 运行环境的this)中的 observing_ 属性,完成依赖更新

emmmm,那现在看到这里的小伙伴们可以从 mobx 源码的角度给大家讲一下为什么最开始的代码可以实现侧边栏展开收起时右侧图表的内容重新调整大小嘛?

四、小测试

  1. 以下两道题目的输出是什么,为什么是这样的输出?

image.png

image.png

  1. 我们没讲 autorun,大家可以猜一猜 autorun 和 reaction 的关系嘛?

  2. 参考 action 源码 回答,为什么 mobx 严格模式要求对 mobx 值的变更必须包裹在 action 里?猜猜看 action 和 runInAction 有何不同?

  3. 下面这段代码里 SomeContainer 组件的 title 会更新嘛?如果不会请说明为什么不会,以及如何让它更新~

image.png

五、调试技巧

恭喜你做完了所有题目,熊最后附赠两个 mobx 调试的小技巧~

  1. 使用 track 监听页面的变化是由哪个 observable 变化导致的,具体操作参考文档,效果如下:

image.png

  1. MobX 附带的开发者工具  mobx-react-devtools 可以用来追踪应用的渲染行为和数据依赖关系,具体操作参考文档

image.png

最后的最后,既然你不辞辛苦的读完了整片文章,那就奖励你在文档上随意提问的权力吧_(:3」∠)_

六、参考资料

  1. 用故事解读MobX源码系列文章
  2. mobx github源码
  3. Mobx官方文档
  4. Mobx6核心源码解析(五): Reaction和事务

Guess you like

Origin juejin.im/post/7078956617206611982