-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
从零实现 Mobx:深入理解 Mobx 原理 #54
Comments
autorun收集依赖好理解。因为它肯定会自动执行一次。所以在第一次执行的时候 通过get 收集依赖就好了。 |
@switers-wang reaction 有两个传参,第一个传参的回调会立即执行一次,所以还是收集到了依赖 |
学习过程中遇见了三个问题: |
1. 前言
Mobx 是 React 的另一种经过战火洗礼的状态管理方案,和 Redux 不同的地方是 Mobx 是一个响应式编程(
Reactive Programming
)库,在一定程度上可以看做没有模板的 Vue,基本原理和 Vue 一致。Mobx 借助于装饰器的实现,使得代码更加简洁易懂。由于使用了可观察对象,所以 Mobx 可以做到直接修改状态,而不必像 Redux 一样编写繁琐的 actions 和 reducers。
Mobx 的执行流程和 Redux 有一些相似。这里借用 Mobx 官网的一张图:
简单的概括一下,一共有这么几个步骤:
computed
计算属性也会根据依赖的状态重新计算属性值。reaction
,从而响应这次状态变化来进行一些操作(渲染组件、打印日志等等)。2. Mobx 核心概念
Mobx 的概念没有 Redux 那么多,学习和上手成本也更低。以下介绍全部基于 ES 装饰器来讲解,更多细节可以参考 Mobx 中文文档:Mobx 中文文档
对于一些实践的用法,也可以参考我一年前写的文章:Mobx 实践
本文涉及到的代码都放到了我的 GitHub 上面:simple-mobx
2.1 observable
observable 可以将接收到的值包装成可观察对象,这个值可以是 JS 基本数据类型、引用类型、普通对象、类实例、数组和映射等等等。
如果在对象里面使用 get,那就是计算属性了。计算属性一般使用
get
来实现,当依赖的属性发生变化的时候,就会重新计算出新的值,常用于一些计算衍生状态。更多时候,我们会配合装饰器一起使用来使用
observable
方法。2.2 computed
想像一下,在 Redux 中,如果一个值 A 是由另外几个值 B、C、D 计算出来的,在 store 中该怎么实现?
如果要实现这么一个功能,最麻烦的做法是在所有 B、C、D 变化的地方重新计算得出 A,最后存入 store。
当然我也可以在组件渲染 A 的地方根据 B、C、D 计算出 A,但是这样会把逻辑和组件耦合到一起,如果我需要在其他地方用到 A 怎么办?
我甚至还可以在所有
connect
的地方计算 A,最后传入组件。但由于 Redux 监听的是整个store
的变化,所以无法准确的监听到 B、C、D 变化后才重新计算 A。但是 Mobx 中提供了
computed
来解决这个问题。正如 Mobx 官方介绍的一样,computed
是基于现有状态或计算值衍生出的值,如下面 todoList 的例子,一旦已完成事项数量改变,那么completedCount
会自动更新。2.3 reaction & autorun
autorun
接收一个函数,当这个函数中依赖的可观察属性发生变化的时候,autorun
里面的函数就会被触发。除此之外,autorun
里面的函数在第一次会立即执行一次。但是很多人经常会用错 autorun,导致属性修改了也没有触发重新执行。
常见的几种错误用法如下:
reaction
则是和autorun
功能类似,但是autorun
会立即执行一次,而reaction
不会,使用 reaction 可以在监听到指定数据变化的时候执行一些操作,有利于和副作用代码解耦。reaction 和 Vue 中的watch
非常像。看到 autorun 和 reaction 的用法后,也许你会想到,如果将其和 React 组件结合到一起,那不就可以实现很细粒度的更新了吗?
没错,Mobx-React 就是来解决这个问题的。
2.4 observer
Mobx-React 中提供了一个
observer
方法,这个方法主要是改写了 React 的render
函数,当监听到render
中依赖属性变化的时候就会重新渲染组件,这样就可以做到高性能更新。3. mobx 原理
从上面的一些用法来看,很明显实现上用到了
Object.defineProperty
或者Proxy
。当 autorun 第一次执行的时候会触发依赖属性的getter
,从而收集当前函数的依赖。在 autorun 里面相当于做了这么一件事情。
当依赖属性触发
setter
的时候,就会将所有订阅了变化的函数都执行一遍,从而实现了数据响应式。Mobx 的实现原理很简单,整体上和 Vue 比较像,简单来说就是这么几步:
Object.defineProperty
或者Proxy
来拦截observable
包装的对象属性的get/set
。autorun
或者reaction
执行的时候,会触发依赖状态的get
,此时将autorun
里面的函数和依赖的状态关联起来。也就是我们常说的依赖收集。set
,此时会通知前面关联的函数,重新执行他们。3.1 observable
observable
的源码实现在 api/observable.ts 文件中,主要是在createObservable
函数里面。这段代码里面对数据类型进行了判断,调用不同的函数,这里主要以
object
的情况为例,返回的是observable.object(v, arg2, arg3)
。observable.object
的实现在observableFactories
里面,这里有判断是否使用Proxy
,如果用Proxy
,就走asDynamicObservableObject
这个方法。这里主要看 extendObservable 方法,它在 extendobservable.ts 文件里面。
关键代码在
adm.extend_
里面,传入了对象的key
、descriptors[key]
。这里的adm
是根据 target 创建的一个ObservableObjectAdministration
实例。extend_
里面调用了annotation.extend_
方法,这个annotation
比较关键,可以看到annotation = this.defaultAnnotation_
这句,按照defaultAnnotation_
->getAnnotationFromOptions
->createAutoAnnotation
->observableAnnotation.extend_
这个链路找下去发现最后调用的是observableAnnotation.extend_
。这里就是根据
key
和value
来定义了observable
的属性,看下defineObservableProperty_
做了些什么。主要有两部分,一个是根据
key
来获取cachedDescriptor
,将其设置为defineProperty
的descriptor
,这里的get/set
就是之后 Mobx 的拦截规则。另一个是创建一个
ObservableValue
实例,将其存入this.values_
里面。这里的
cachedDescriptor.get
最终也是调用了this.values_.get(key)!.get()
,也就是ObservableValue
里面的get
方法。这个
reportObserved
最终会调到 observable.ts 文件里面,它将当前的这个ObservableValue
实例放到了derivation.newObserving_
上面,通过derivation.unboundDepsCount_
进行了映射。至此,整个
observable
的流程就分析清楚了,如下图所示:3.2 autorun
autorun
是触发get
的地方,它里面的函数会在依赖的数据发生变化的时候执行。它的源码在 autorun.ts 文件里面。可以看到,这里会创建一个
Reaction
的实例,将我们的函数view
传到 reaction.track
里面,然后一起传给Reaction
的构造函数,等待合适的时机再去执行这个函数。在 Mobx 里面其实都是通过reaction.schedule_
来调度执行的。这里将这个 Reaction 实例放到
pendingReactions
里面,然后执行了runReactions
。这里的reactionScheduler
可能是为了以后实现其他功能留下的一个口子。在
runReactionsHelper
里面会遍历我们的pendingReactions
数组,执行里面的reaction
实例的runReaction_
方法。这里面的
onInvalidate_
其实就是刚刚的reaction.track
方法。然后来看下reaction.track
的实现。它会在
trackDerivedFunction
里面调用刚刚的 view 函数(autorun
包裹的那个函数),我们知道在执行 view 函数的时候,如果里面依赖了被observable
包裹对象的属性,那么就会触发属性的 get 方法,也就回到了刚刚分析observable
的reportObserved
里面。它会将observable
挂载到derivation.newObserving_
上面。到了这里,你会发现在
newObserving
上面已经收集到了 view 函数依赖的属性,这里的derivation
实际上就是前面的那个 Reaction 实例。然后又执行了
bindDependencies
函数,它就是将 Reaction 实例和observable
关联起来的。到了这一步,我们已经可以从每个对象属性的
observers_
上面获取到需要通知变更的函数了。只要在 set 的时候从observers_
取出来、遍历、执行就行了。在 set 阶段依然走的是
reaction.schedule_
这个方法去调度的,重复我们最开始执行 autorun 的那一步。4. 从零实现
知道原理之后,那我们就可以来一步步去实现这个 Mobx,这里将会以装饰器写法为例子。
4.1 observable
前面在讲解装饰器的时候说过,装饰器函数一般接收三个参数,分别是目标对象、属性、属性描述符。
我们都知道,被 observable 包装过的对象,其属性也是可观察的,也就是说需要递归处理其属性。
其次,由于需要收集依赖的方法,某个方法可能依赖了多个可观察属性,相当于这些可观察属性都有自己的订阅方法数组。
以上面这段代码为例,可观察对象
cat
只有func1
一个订阅方法,而mice
则有func1
和func2
两个方法。可见
observable
在调用的时候,使用了类似id
之类的来对不同调用做区分。我们可以先简单地实现一下 observable 方法。首先,实现一个可以根据 id 来区分的类。
然后我们要实现一个装饰器,这个方法支持拦截属性的
get/set
。这个observable
装饰器其实就是利用了Observable
这个类。然后我们要来实现
Observable
类里面的get/set
,但这个get/set
和autorun
也是密切相关的。get/set
做了什么呢?get
会在autorun
执行的时候,将传给autorun
的函数依赖收集到id
相关的数组里面。而
set
则是会触发数组中相关函数的执行。4.2 依赖收集autorun
前面讲了,
autorun
会立即执行一次,并且会将其函数收集起来,存到和observable. id
相关的数组中去。那么autorun
就是一个收集依赖、执行函数的过程。实际上,在执行函数的时候,就已经触发了get
来做了收集。接着我们来实现一下
dependenceManager
的几个方法,首先定义一个DependenceManager
类。再来拆解一下
DependenceManager
中的方法,如下:get
的时候,根据传入的id
,将函数依赖放入数组中。set
的时候,根据id
来执行数组中的函数依赖。至此,我们已经完成了一个完整的 Mobx 骨架,可以做一些有意思的事情了。写个简单的例子来试试:
可以看到
autorun
里面的函数被执行了两次,说明我们已经实现了一个简单的 Mobx。但这个 Mobx 还有很多问题,比如不支持更深层的对象,也监听不到数组等等。4.3 对深层对象和数组的处理
在前面我们讲解
Proxy
和Object.defineProperty
的时候就已经说过,由于Object.defineProperty
的问题,无法监听到新增加的项,因此对于动态添加的属性或者下标就无法进行监听。据尤雨溪所讲,在 Vue 里面由于考虑性能就放弃了监听数组的下标变化。而在 Mobx4 中使用了比较极端的方式,那就是不管数组中有多少项,都是用一个长度 1000 的数组来存放,去监听这 1000 个下标变化,可以满足大多数场景。
但是在未来的 Vue3 和已面世的 Mobx5 中,都已经使用 Proxy 来实现对数组的拦截了。这里我们使用 Proxy 来增加对数组的监听。
现在已经可以监听到数组的变化了,那么用一个例子试试吧。
原本预想中会打印两次,结果却只打印了一个1,这是为什么?来翻查一下前面
Observable.set
的代码会发现,我们只拦截了对象最外层属性的变化,如果有更深层的就拦截不到了。如果对list
重新赋值就会生效,但修改list
中第一项就不会生效。所以我们还需要对对象深层属性做个递归包装,这也是 Mobx 中
observable
的功能之一。Observable
实例。这个
createObservable
方法,我们只需要在observable
装饰器里面执行就行了。继续使用上面那个例子,会发现成功打印出来了1、2,说明我们实现的这个简单的 Mobx 即支持拦截数组,又支持拦截深层属性。
4.4 computed
我们都知道
computed
有三个特点,分别是:computed
是个 get 方法,会缓存上一次的值computed
会根据依赖的可观察属性重新计算computed
的函数也会被重新执行其实
computed
和observable
的实现思路类似,区别在于computed
需要收集两次依赖,一次是computed
依赖的可观察属性,一次是依赖了computed
的方法。首先,我们来定义一个
computed
的,这个方法依然是个装饰器。接下来实现这个
Computed
类,这个类的实现方式和Observable
差不多。在执行 get 方法的时候,就会去收集依赖了当前
computed
的方法。我们还需要去收集当前computed
依赖的属性。这里是不是可以利用前面的
dependenceManager.beginCollect
呢?没错,我们可以用dependenceManager.beginCollect
来收集到重新计算的get
函数,并且通知依赖了computed
的方法。最终,我们的实现是这样的:
4.5 observer
而 observer 的实现比较简单,就是利用了 React 的
render
方法执行进行依赖收集,我们可以在componentWillMount
里面注册autorun
。至此,我们已经实现了一个完整的
Mobx
库,可以看到Mobx
的实现原理比较简单,使用起来也没有 Redux 那么繁琐。The text was updated successfully, but these errors were encountered: