Proxy
拦挡形式
Mobx 裸露的拦挡的 API 有多种,概括来说能够分为装璜器式和基于 observable 办法调用。
装璜器
对装璜器不太明确的同学,能够见我以往一篇文章:装璜器原理探索,通过剖析转译后的 ES 代码得出装璜器的行为。
因为装璜器在 ES 里还处于提案中且各阶段的装璜器行为不统一,故 mobx 6.x 起就淘汰了装璜器的写法(也能够手动开启),本文的源码剖析基于 mobx 5.x 版本(所述原理与 6.x 截然不同),此时装璜器基于 babel
{
"plugins": [["@babel/plugin-proposal-decorators", { "legacy": true}],
]
}
该配置下实现,应用历史遗留(stage 1)的装璜器中的语法和行为。
import {observable} from 'mobx';
class A {@observable a = 1;}
此处 @observable
装璜器的行为其实就是在实例化返回 A 的原型上挂 getter setter。
{
configurable: true,
enumerable: enumerable,
get: function() {initializeInstance(this)
return this[prop]
},
set: function(value) {initializeInstance(this)
this[prop] = value
}
}
在实例化时会执行 instance.a = 1
赋值操作,触发 setter,走到 mobx 解决类实例的逻辑:
- 往实例上挂 对象治理类(adm)
- 递归包装 value,并收集在 adm
- 为实例上的 key (a) 挂 getter setter
{
configurable: true,
enumerable: true,
get() {
// 收集依赖
return this[$mobx].read(propName)
},
set(v) {
// 触发更新
this[$mobx].write(propName, v)
}
}
宏观来讲,尔后拜访装璜的属性就会走到 this[$mobx].read(propName)
收集副作用,当属性扭转就走到 this[$mobx].write(propName, v)
执行副作用。
observable 命令式调用
命令式调用就是如下这种:
const xxx = observable(xxx) || observable.xx(xxx);
肯定会有一个返回值,当咱们操作返回值的时候,就会做收集 | 执行副作用的行为。
上面会挨个解析各个类型的拦挡状况。
先阐明个概念,在 Mobx 里,所有须要被察看的 value,除了数组、Set,都会被 ObservableValue
类包装(为了不便之后对其实例简称 OV),做的工作就是:
(1)应用 enhancer 解决 value
(2)治理(1)中包装后的 value (读写、收集依赖等)
enhancer 有多种,若用户不作额定配置,Mobx 里默认对每个 value 应用 deepEnhancer
进行包装,其实就是递归对这个 value 做 observable 命令式调用 的操作。
primitive value
对于原始类型 value,Mobx 里只反对应用 observable.box(val)
这个 API 进行拦挡,其实外部就是返回了个 OV。
如果读写别离用 OV 裸露 get
set
API。
object
应用 observable.object(val)
进行拦挡,外部做了三件事:
- 新建一个空对象 {}, 并给 {} 挂上 对象治理类(adm)
- Proxy 拦挡 {},并把代理对象保留在 adm.proxy
-
遍历 val 的 keys:
- 递归包装 value,并收集 OV 在 adm
- 在 {} 挂上每个 key 的 getter setter(同装璜器挂的 getter setter 一样)
- 返回代理对象
Proxy 的 handlers 有:
// 临时只关注读写
{get(target: IIsObservableObject, name: PropertyKey) {
// ... 疏忽临时无关代码
const adm = getAdm(target)
// 拿到 OV
const observable = adm.values.get(name)
if (observable instanceof Atom) {// 此处等同于调用 adm.read(propName)
const result = (observable as any).get()
// ...
return result
}
// ...
},
set(target: IIsObservableObject, name: PropertyKey, value: any) {if (!isPropertyKey(name)) return false
set(target, name, value)
// 这个 set 办法针对对象最终执行如下
// // ...
// if (isObservableObject(obj)) {// const adm = ((obj as any) as IIsObservableObject)[$mobx]
// // 拿到 OV
// const existingObservable = adm.values.get(key)
// if (existingObservable) {// adm.write(key, value)
// }
// // ...
// }
// //...
return true
},
}
由上可知,此处的读取解决最终也是和装璜器形式润饰的对象属性的读写解决雷同。
array
应用 observable.array(val)
进行拦挡,外部做了五件事:
- 初始化 数组治理类 (adm),挂载在 [] 上,再把 [] 挂载在 adm.values
- Proxy 拦挡 [],并把代理对象挂载在 adm.proxy
- 遍历 val,递归包装每个元素
- 更新 3 中一个个 OV 在 adm.values 里
- 返回代理对象
Handler 如下:
get(target, name) {if (name === $mobx) return target[$mobx]
if (name === "length") return target[$mobx].getArrayLength()
if (typeof name === "number") {return arrayExtensions.get.call(target, name)
}
if (typeof name === "string" && !isNaN(name as any)) {return arrayExtensions.get.call(target, parseInt(name))
}
if (arrayExtensions.hasOwnProperty(name)) {
// arrayExtensions 捕捉数组办法
return arrayExtensions[name]
}
return target[name]
},
set(target, name, value): boolean {if (name === "length") {target[$mobx].setArrayLength(value)
}
if (typeof name === "number") {arrayExtensions.set.call(target, name, value)
}
if (typeof name === "symbol" || isNaN(name)) {target[name] = value
} else {
// numeric string
arrayExtensions.set.call(target, parseInt(name), value)
}
return true
},
以读取为例阐明,在 arrayExtensions 里是这样的:
get(index: number): any | undefined {const adm: ObservableArrayAdministration = this[$mobx]
if (adm) {if (index < adm.values.length) {adm.atom.reportObserved()
return adm.dehanceValue(adm.values[index])
}
// ...
}
return undefined
},
set(index: number, newValue: any) {const adm: ObservableArrayAdministration = this[$mobx]
const values = adm.values
if (index < values.length) {
// update at index in range
checkIfStateModificationsAreAllowed(adm.atom)
const oldValue = values[index]
// ...
// 新的被 enhancer 包装过的 value
newValue = adm.enhancer(newValue, oldValue)
const changed = newValue !== oldValue
if (changed) {values[index] = newValue // 扭转 adm 里收集的旧 value
// 告诉更新
adm.notifyArrayChildUpdate(index, newValue, oldValue)
}
}
// ...
}
之前说过,数组不会被 ObservableValue 包装,因为在其治理类外面,曾经实现了 ObservableValue 的工作,也就是:
(1)应用 enhancer 解决 value
(2)治理(1)中包装后的 value (读写、收集依赖等)
其实,arrayExtensions 里的操作,外围也是收集依赖和触发更新。
map
应用 observable.map(val)
进行拦挡,外部做了三件事:
- 初始化 map 治理类
- 遍历 val,挨个 ObservableValue 包装 value,收集在治理类的
this._data
上 - 返回 map 治理类实例
返回的实例,有 Map 的 API 办法,以读写为例:
get(key: K): V | undefined {// this._data.get(key)!.get() 等同于调用对象 adm 的 adm.read(propName)
// 收集依赖
if (this.has(key)) return this.dehanceValue(this._data.get(key)!.get())
return this.dehanceValue(undefined)
}
set(key: K, value: V) {const hasKey = this._has(key)
// ...
if (hasKey) {this._updateValue(key, value)
} else {this._addValue(key, value)
}
return this
}
_updateValue(key: K, newValue: V | undefined) {
// 拿到 ObservableV
const observable = this._data.get(key)!
// enhancer 新 value,而后比照旧 value 是否相等
newValue = (observable as any).prepareNewValue(newValue) as V
if (newValue !== globalState.UNCHANGED) {
// ...
// 更新并告诉更新
observable.setNewValue(newValue as V)
// ...
}
}
set
应用 observable.set(val)
进行拦挡,外部做了三件事:
- 初始化 set 治理类
- 遍历 val,挨个 enhancer 包装 value,收集在治理类的
this._data
上 - 返回 set 治理类实例
和 Map 一样,返回的 set 治理类也有 Set 的相干 API,以获取所有 values 和 add 为例:
add(value: T) {
// ...
if (!this.has(value)) {transaction(() => {
// 往本地缓存的 _data 里新增 enhancer 后的 value
this._data.add(this.enhancer(value, undefined))
// 告诉依赖更新
this._atom.reportChanged()})
// ...
}
return this
}
keys(): IterableIterator<T> {return this.values()
}
values(): IterableIterator<T> {
// 告诉收集依赖
this._atom.reportObserved()
const self = this
let nextIndex = 0
const observableValues = Array.from(this._data.values())
// 在 for of 中挨个读 _data 的值
return makeIterable<T>({next() {
return nextIndex < observableValues.length
? {value: self.dehanceValue(observableValues[nextIndex++]), done: false }
: {done: true}
}
} as any)
}
由上述可知,其实就是对于不同的数据结构,解决的外围的就是拦挡被观察者 getter setter 或相干 API,达到在读取时 收集依赖 ,变动时 告诉依赖更新 的目标。
Derivation
Mobx 里有派生的概念,相似于观察者。在 Derivation 内应用了 Proxy 的产物,每当产物有变动时则派生(告诉)了 Derivation(观察者)。
一些概念:
transaction
援用了数据库事务的概念,Mobx 中的事务用于批量解决 Reaction(Derivation 管理者)的执行,防止不必要的从新计算。Mobx 的事务实现比较简单,应用 startBatch 和 endBatch 来开始和完结一个事务:
function startBatch() {
// 通过一个全局的变量 inBatch 标识事务嵌套的层级
globalState.inBatch++
}
function endBatch() {
// 最外层事务完结时,才开始执行从新计算
if (--globalState.inBatch === 0) {
// 执行所有 Reaction
runReactions()
// 解决不再被察看的 ObservableV
const list = globalState.pendingUnobservations
for (let i = 0; i < list.length; i++) {const observable = list[i]
observable.isPendingUnobservation = false
if (observable.observers.length === 0) {observable.onBecomeUnobserved()
}
}
globalState.pendingUnobservations = []}
}
例如,一个 Action 开始和完结时同时随同着事务的启动和完结,确保 Action 中(可能屡次)对状态的批改只触发一次 Reaction 的从新执行。
function startAction() {
// ...
startBatch()
// ...
}
function endAction() {
// ...
endBatch()
// ...
}
Reaction
Reaction 就是 Derivation 的管理者,实现了 Derivation 的接口:
interface IDerivation extends IDepTreeNode {
// 依赖数组
observing: IObservable[]
// 每次执行收集到的新依赖数组
newObserving: null | IObservable[]
// 依赖的状态
dependenciesState: IDerivationState
// 每次执行都会有一个 uuid,配合 Observable 的 lastAccessedBy 属性做简略的性能优化
runId: number
// 执行时新收集的未绑定依赖数量
unboundDepsCount: number
// 依赖过期时执行
onBecomeStale()}
Derivation 状态机
Derivation 通过 dependenciesState 属性标记依赖的四种状态:
- NOT_TRACKING:在执行之前,或事务之外,或未被察看 (计算值) 时,所处的状态。此时 Derivation 没有任何对于依赖树的信息。枚举值 -1
- UP_TO_DATE:示意所有依赖都是最新的,这种状态下不会从新计算。枚举值 0
- POSSIBLY_STALE:计算值才有的状态,示意深依赖产生了变动,但不能确定浅依赖是否变动,在从新计算之前会查看。枚举值 1
- STALE:过期状态,即浅依赖产生了变动,Derivation 须要从新计算。枚举值 2
任何状态都趋于 UP_TO_DATE。
————————- 2 ————————- STALE
↓
————-↓— 1 —————— POSSIBLY_STALE
↓ ↓
——- 0 ——– UP_TO_DATE
↑
-1— NOT_TRACKING
状态机的法则是:
- 初始都是 NOT_TRACKING,绑定起依赖和派生关系后个体变为 U_T_D。
解绑则回退为 NOT_TRACKING。- 某收集的依赖发生变化时,其本身依赖状态和 Derivation (onBecomeStale 后)都变为 STALE。
在 Derivation 重新处理后,其本身和收集的依赖都变为 U_T_D。计算属性计算后(含第一次),本身派生状态、收集的依赖状态都变为 U_T_D。(合乎 2 第二句 Derivation 重新处理后,其本身和收集的依赖都变为 U_T_D)在第一次被绑定后,合乎 1。
若计算属性收集的某依赖 A 状态发生变化时,将 A 状态和 计算属性派生状态(onBecomeStale 后) 为 STALE(合乎 2 第一句),并且把 计算属性依赖状态、计算属性派生的 Derivation 置为 P_STALE(区别)。在计算属性从新计算后本身派生状态、收集的所有依赖状态变更为 U_T_D(合乎 2 第二句),若计算结果无变更,把计算属性依赖状态、计算属性派生的 Derivation 变回 U_T_D。若有变更,则把 计算属性派生的 Derivation 变为 STALE,接着重新处理 计算属性派生的 Derivation,把其和其收集的依赖(含计算属性作为依赖)状态 变为 U_P_D。
上面以 AutoRun、Computed Value、React Render 为例剖析 Derivation 的源码。
AutoRun
流程
惯例用法是:
autorun(cb)
首先,会为 AutoRun 这个 Derivation 初始化一个 Reaction 用于治理:
function autorun(view: (r: IReactionPublic) => any, // cb
opts: IAutorunOptions = EMPTY_OBJECT // 疏忽
): IReactionDisposer {const name: string = (opts && opts.name) || (view as any).name || "Autorun@" + getNextId()
const runSync = !opts.scheduler && !opts.delay
let reaction: Reaction
if (runSync) {
// normal autorun
// 用一个 reaction 来治理该 autorun
reaction = new Reaction(
name,
function(this: Reaction) {this.track(reactionRunner)
},
opts.onError,
opts.requiresObservable
)
}
function reactionRunner() {view(reaction)
}
// 将该 reaction 列入计划表
reaction.schedule()
// 返回销毁办法
return reaction.getDisposer()}
计划表保护了一个全局的数组,外面存的 Reactions 就是该 batch(批次)中须要执行的 Reaction。
schedule() {
// Reaction 曾经在从新计算的计划表内,间接返回
if (!this._isScheduled) {
this._isScheduled = true
// 该 Reaction 退出全局的待从新计算数组中
globalState.pendingReactions.push(this)
runReactions()}
}
export function runReactions() {
// 惰性更新,若此时处于事务中,inBatch > 0,会间接返回
if (globalState.inBatch > 0 || globalState.isRunningReactions) return
reactionScheduler(runReactionsHelper)
}
function runReactionsHelper() {
globalState.isRunningReactions = true
// 取出以后批次收集的所有 Reaction
const allReactions = globalState.pendingReactions
let iterations = 0
// 当执行 Reaction 时,可能触发新的 Reaction(Reaction 内容许设置 Observable 的值),退出到 pendingReactions 中
while (allReactions.length > 0) {
// 设定 Reaction 计算的最大迭代次数,防止造成死循环
if (++iterations === MAX_REACTION_ITERATIONS) {
// ... error
allReactions.splice(0) // clear reactions
}
let remainingReactions = allReactions.splice(0)
for (let i = 0, l = remainingReactions.length; i < l; i++)
remainingReactions[i].runReaction()}
globalState.isRunningReactions = false
}
接下来就是执行 Reaction 的逻辑了,次要目标是运行 cb,收集用到的 OV。
runReaction() {if (!this.isDisposed) {
// 开启一个事务处理,因为运行 cb 的过程中可能会再加 Reaction 到计划表(比方依赖更新)startBatch()
this._isScheduled = false
// 判断 Reaction 收集的依赖状态
// 如状态机所示,只有在 NO_TRACKING | STALE | 判断 COMPUTED 值变动时才会执行 Reaction
if (shouldCompute(this)) {
this._isTrackPending = true
try {
// 解决 cb
this.onInvalidate()
// ...
} catch (e) {this.reportExceptionInDerivation(e)
}
}
endBatch()}
}
this.onInvalidate
这里就开始解决 cb 了,外围逻辑是:
function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
// ...
// 把 Reaction 和之前收集的被观察者状态都置为 UP_TO_DATE
changeDependenciesStateTo0(derivation)
derivation.newObserving = new Array(derivation.observing.length + 100)
// 记录新的依赖的数量
derivation.unboundDepsCount = 0
// 每次执行都调配一个 uid
derivation.runId = ++globalState.runId
// 以后 Derivation 记录到全局的 trackingDerivation 中,这样被察看的 Observable 在其 reportObserved 办法中就能获取到该 Derivation
const prevTracking = globalState.trackingDerivation
globalState.trackingDerivation = derivation
let result
if (globalState.disableErrorBoundaries === true) {
// debug 环境不 catch 异样,若出错堆栈清晰
result = f.call(context)
} else {
try {
// 执行响应函数 cb,收集应用到的所有依赖,退出 newObserving 数组中
result = f.call(context)
} catch (e) {result = new CaughtException(e)
}
}
globalState.trackingDerivation = prevTracking
// 比拟新旧依赖,更新依赖
bindDependencies(derivation)
// 如果配置了 requiresObservable 然而 cb 内没援用 OV 的话,报正告
warnAboutDerivationWithoutDependencies(derivation)
// ...
}
getter 里干了啥?(追踪依赖)
执行 cb 的时候,读取到 observable 的值,以装璜器润饰形式为例,会走到:
read(key: PropertyKey) {return this.values.get(key)!.get()}
this.values.get(key) 拿到的就是 OV,OV 的 get:
public get(): T {this.reportObserved()
return this.dehanceValue(this.value)
}
function reportObserved(observable: IObservable): boolean {
// ...
const derivation = globalState.trackingDerivation
if (derivation !== null) {
// 防止反复收集 OV
if (derivation.runId !== observable.lastAccessedBy) {
observable.lastAccessedBy = derivation.runId
derivation.newObserving![derivation.unboundDepsCount++] = observable
if (!observable.isBeingObserved) {
observable.isBeingObserved = true
observable.onBecomeObserved() // 触发监听钩子}
}
return true
} else if (observable.observers.size === 0 && globalState.inBatch > 0) {
// 如果 OV 没有 derivation 察看了,筹备革除 Observable
queueForUnobservation(observable)
}
return false
}
其实就是把 OV 收集在 Reaction 的 newObserving 上,至此追踪依赖就完结了。
解决依赖
接着就是解决收集到的依赖:
- 替换 Derivation 的依赖数组为新收集的依赖
- 找出新旧依赖数组不相交的元素,解绑旧依赖数组中不相交的 OV 与该 Derivation 的关系(OV 不再收集 Derivation),绑定新依赖数组中不相交的 OV 与该 Derivation 的关系
function bindDependencies(derivation: IDerivation) {// invariant(derivation.dependenciesState !== IDerivationState.NOT_TRACKING, "INTERNAL ERROR bindDependencies expects derivation.dependenciesState !== -1");
const prevObserving = derivation.observing
const observing = (derivation.observing = derivation.newObserving!)
// 记录更新依赖过程中,新察看的 Derivation 的最新状态
let lowestNewObservingDerivationState = IDerivationState.UP_TO_DATE
// Go through all new observables and check diffValue: (this list can contain duplicates):
// 0: first occurrence, change to 1 and keep it
// 1: extra occurrence, drop it
// 遍历新的 observing 数组,应用 diffValue 这个属性来辅助 diff 过程:// 所有 Observable 的 diffValue 初值都是 0(要么刚被创立,继承自 BaseAtom 的初值 0;// 要么通过上次的 bindDependencies 后,置为了 0)// 如果 diffValue 为 0,保留该 Observable,并将 diffValue 置为 1
// 如果 diffValue 为 1,阐明是反复的依赖,忽视掉
let i0 = 0,
l = derivation.unboundDepsCount // 新收集的 ObservableValue 数量
for (let i = 0; i < l; i++) {const dep = observing[i]
if (dep.diffValue === 0) {
// 这次此次 Reaction 最新收集的依赖
dep.diffValue = 1
// i0 不等于 i,即后面有反复的 dep 被忽视,顺次往前移笼罩
if (i0 !== i) observing[i0] = dep
i0++
}
// Upcast is 'safe' here, because if dep is IObservable, `dependenciesState` will be undefined,
// not hitting the condition
if (((dep as any) as IDerivation).dependenciesState > lowestNewObservingDerivationState) {lowestNewObservingDerivationState = ((dep as any) as IDerivation).dependenciesState
}
}
observing.length = i0 // 只保留最新一次追踪 Reaction 收集的依赖
derivation.newObserving = null // newObserving shouldn't be needed outside tracking (statement moved down to work around FF bug, see #614)
// Go through all old observables and check diffValue: (it is unique after last bindDependencies)
// 0: it's not in new observables, unobserve it
// 1: it keeps being observed, don't want to notify it. change to 0
// 遍历 prevObserving 数组,查看 diffValue:(通过上一次的 bindDependencies 后,该数组中不会有反复)
// 如果为 0,阐明没有在 newObserving 中呈现,调用 removeObserver 将 dep 和 derivation 间的分割移除
// 如果为 1,仍然被察看,将 diffValue 置为 0(在上面的循环有用途)l = prevObserving.length
while (l--) {const dep = prevObserving[l]
if (dep.diffValue === 0) {removeObserver(dep, derivation)
}
dep.diffValue = 0
}
// Go through all new observables and check diffValue: (now it should be unique)
// 0: it was set to 0 in last loop. don't need to do anything.
// 1: it wasn't observed, let's observe it. set back to 0
// 再次遍历新的 observing 数组,查看 diffValue
// 如果为 0,阐明是在下面的循环中置为了 0,即是原本就被察看的依赖,什么都不做
// 如果为 1,阐明是新增的依赖,调用 addObserver 新增依赖,并将 diffValue 置为 0,为下一次 bindDependencies 做筹备
while (i0--) {const dep = observing[i0]
if (dep.diffValue === 1) {
dep.diffValue = 0
addObserver(dep, derivation)
}
}
// Some new observed derivations may become stale during this derivation computation
// so they have had no chance to propagate staleness (#916)
// 某些新察看的 Derivation 可能在依赖更新过程中过期
// 防止这些 Derivation 没有机会流传过期的信息(#916)if (lowestNewObservingDerivationState !== IDerivationState.UP_TO_DATE) {
derivation.dependenciesState = lowestNewObservingDerivationState
derivation.onBecomeStale()}
}
下面用了 diffValue 标记位,升高奢侈算法的工夫复杂度为线性,给个例子吧:
const a = {};
const b = {};
const c = {};
const prev = [a, b];
const curr = [b, c];
// 找出不相交的 a, c 并做一些解决你会怎么做?// 奢侈算法的解决就是 O(n^2)
prev.forEach((p, ip) => {curr.forEach((c, ic) => {// includes 工夫复杂度为 O(n),假如用户用 set,has 是常数级的,暂且视此处也为常数级
if (p !== c && prev.includes(c)) {// 解绑}
if (p !== c && !prev.includes(c)) {// 绑定}
})
})
如果加个 diffValue 作为标记的话,算法就为:
const a = {d: 0};
const b = {d: 0};
const c = {d: 0};
const prev = [a, b];
const curr = [b, c];
curr.forEach(c => c.d = 1);
prev.forEach(p => {if (p.d === 0) {// 解绑}
p.d = 0;
})
curr.forEach(c => {if (c.d === 1) {// 绑定}
c.d = 0;
})
至此,依赖关系解决完了,该 Derivation 上收集了应用的 OV,每个 OV 也收集了派生的 Derivation。并且把该 Derivation、之前收集的依赖的状态置为了 UP_TO_DATE。
derivation.dependenciesState = IDerivationState.UP_TO_DATE
OV.lowestObserverState = IDerivationState.UP_TO_DATE
新绑定的依赖状态为 NOT_TRACKING | UP_TO_DATE。
setter 里干了啥?
同样以装璜器润饰的属性为例:
write(key: PropertyKey, newValue) {
const instance = this.target
// 拿到 OV
const observable = this.values.get(key)
// 解决计算值状况
if (observable instanceof ComputedValue) {observable.set(newValue)
return
}
// enhance 新值,Object.is 比照新旧值
newValue = (observable as any).prepareNewValue(newValue)
if (newValue !== globalState.UNCHANGED) {
// 值变动
// ...
(observable as ObservableValue<any>).setNewValue(newValue)
// ...
}
}
setNewValue(newValue: T) {
const oldValue = this.value
this.value = newValue
this.reportChanged()
// ...
}
reportChanged() {startBatch()
// 告诉变动
propagateChanged(this)
endBatch()}
告诉变动其实做了三件事件:
- 把 OV 的状态变为 STALE
- 遍历 OV 绑定的所有 Derivation,并解决
- 解决完一个 Derivation 则变更其状态为 STALE
export function propagateChanged(observable: IObservable) {// invariantLOS(observable, "changed start");
if (observable.lowestObserverState === IDerivationState.STALE) return
observable.lowestObserverState = IDerivationState.STALE
// Ideally we use for..of here, but the downcompiled version is really slow...
// 如果被解除 observableValue 和 Observer 的绑定关系,这里就不会遍历到。observable.observers.forEach(d => {if (d.dependenciesState === IDerivationState.UP_TO_DATE) {
// ...
// 遍历 OV 绑定的所有 Derivation,并解决
d.onBecomeStale()}
d.dependenciesState = IDerivationState.STALE
})
// invariantLOS(observable, "changed end");
}
d.onBecomeStale() 干了啥呢?
其实就是再把该 Derivation 退出计划表,排期执行 Reaction,反复咱们下面的流程。
onBecomeStale() {this.schedule()
}
Reaction 流程概览
Computed Value
CV 是比拟非凡的存在,即作为依赖,也作为派生。它是用它的副作用里的依赖,是它外部依赖的派生。
流程
在 Mobx 里也是用一个类 ComputedValue 来治理:
class ComputedValue {
dependenciesState = IDerivationState.NOT_TRACKING // 作为派生的初始状态
lowestObserverState = IDerivationState.UP_TO_DATE // 作为依赖的初始状态
observing: IObservable[] = [] // CV 作为派生,收集的所有依赖
newObserving = null // 每 batch 执行中新收集的依赖
observers = new Set<IDerivation>() // CV 作为依赖,收集的所有派生
// ...
constructor(options: IComputedValueOptions<T>) {
// 检错机制,参数必须含 get
invariant(options.get, "missing option for computed: get")
// getter 回调作为外部依赖的派生
this.derivation = options.get!
this.name = options.name || "ComputedValue@" + getNextId()
// 解决 setter
// ...
// 对于新旧计算结果的比照办法,默认 Object.is
this.equals =
options.equals ||
((options as any).compareStructural || (options as any).struct
? comparer.structural
: comparer.default)
// getter 回调计算的上下文
this.scope = options.context
// 是否必须要求在副作用内应用计算属性
this.requiresReaction = !!options.requiresReaction
// 是否始终强制绑定计算属性以及外部依赖。(默认当计算属性没被用时,会同步解绑计算属性与其外部依赖)this.keepAlive = !!options.keepAlive
}
}
每次当计算属性被拜访时,会触发外部 get 办法,次要做两件事:
- 告诉被察看
- 评估是否须要计算,若须要,则解决一些状态扭转。
public get(): T {if (this.isComputing) fail(`Cycle detected in computation ${this.name}: ${this.derivation}`)
if (globalState.inBatch === 0 && this.observers.size === 0 && !this.keepAlive) {
// 在非副作用里拜访,简略计算出返回值
if (shouldCompute(this)) {this.warnAboutUntrackedRead()
startBatch() // See perf test 'computed memoization'
this.value = this.computeValue(false)
endBatch()}
} else {
// 在副作用里拜访
// 告诉被察看,退出 Reaction.newObserving,之后会建设起计算属性与其派生的绑定关系
reportObserved(this)
// 评估作为 Derivation 是否须要计算
// 若须要,从新计算完后,本身作为 D 的状态变为 U_T_D。依赖状态变更为 U_T_D
// 若值有扭转,则扭转本身作为 OV 的状态为 STALE,收集的观察者(第一次读取时没有)的状为 STALE
if (shouldCompute(this)) if (this.trackAndCompute()) propagateChangeConfirmed(this)
}
const result = this.value!
if (isCaughtException(result)) throw result.cause
return result
}
评估计算
第一次拜访必定须要计算的,咱们来看下评估计算的办法:
export function shouldCompute(derivation: IDerivation): boolean {switch (derivation.dependenciesState) {
case IDerivationState.UP_TO_DATE:
return false
case IDerivationState.NOT_TRACKING: // 第一次拜访时
case IDerivationState.STALE:
return true
case IDerivationState.POSSIBLY_STALE: {// 临时跳过}
}
}
在晓得容许计算后,就开始计算和追踪依赖了
private trackAndCompute(): boolean {
// ...
const oldValue = this.value
// 有没有解除计算属性与其外部依赖的绑定关系,第一次必定是没有绑定关系的
const wasSuspended =
/* see #1208 */ this.dependenciesState === IDerivationState.NOT_TRACKING
// 新计算的值
const newValue = this.computeValue(true)
const changed =
wasSuspended ||
isCaughtException(oldValue) ||
isCaughtException(newValue) ||
!this.equals(oldValue, newValue)
if (changed) {
// 若有扭转则赋新值
this.value = newValue
}
return changed
}
computeValue(track: boolean) {
this.isComputing = true
globalState.computationDepth++
let res: T | CaughtException
if (track) {
// 不仅计算、也追踪外部依赖
res = trackDerivedFunction(this, this.derivation, this.scope)
} else {
// 简略从新计算
if (globalState.disableErrorBoundaries === true) {res = this.derivation.call(this.scope)
} else {
try {res = this.derivation.call(this.scope)
} catch (e) {res = new CaughtException(e)
}
}
}
globalState.computationDepth--
this.isComputing = false
return res
}
trackDerivedFunction 很相熟了,在解说 AutoRun 时剖析过了,次要就是干了三件事,其实就是计算和追踪依赖:
- 因为派生行将执行,所以扭转派生与依赖的状态为 U_T_D
- 执行派生
- 建设派生与依赖的绑定关系
如果后果有扭转的话,就执行 propagateChangeConfirmed(this),也就是扭转 CV 作为依赖、以及其派生的状态为 STALE 了。
export function propagateChangeConfirmed(observable: IObservable) {// invariantLOS(observable, "confirmed start");
// 让 computedValue 作为 OV,扭转本身状态与其收集的 Derivation 都为不稳固
if (observable.lowestObserverState === IDerivationState.STALE) return
observable.lowestObserverState = IDerivationState.STALE
// 第一次拜访计算属性时,还未建设起派生与计算属性的绑定关系,所以 observers 为空
// 之后拜访的状况下,就会把派生的状态由 P_S 转为 STALE 了
observable.observers.forEach(d => {if (d.dependenciesState === IDerivationState.POSSIBLY_STALE)
d.dependenciesState = IDerivationState.STALE
else if (d.dependenciesState === IDerivationState.UP_TO_DATE // this happens during computing of `d`, just keep lowestObserverState up to date.) // 当派生曾经开始重新处理时会遇到这个状况,此时不须要扭转计算属性作为 OV 的状态和派生的状态了,因为派生曾经重新处理了,并且也会拿到最新的计算值,此时间接把计算属性作为 OV 的状态设为 U_T_D 就好
// 比方,计算属性的派生是与依赖 A 与计算属性绑定的
// 某个 action 外面先扭转了计算属性的深依赖值,再扭转依赖 A 的值
// 此时派生的状态会先变 P_S,再变为 STALE,// 在一轮 batch 完结后,重新处理派生 Reaction,会间接从新计算计算属性的值,走到这个判断条件内,不须要再管派生应不应该重新处理了,人家曾经由依赖 A 的变动确定要解决了。observable.lowestObserverState = IDerivationState.UP_TO_DATE
})
// invariantLOS(observable, "confirmed end");
}
之后在计算属性的派生接着解决,就会把计算属性作为依赖的状态和派生本人的状态变为 U_T_D,等着下一次依赖扭转再次解决了。
计算属性的派生内其余依赖扭转
这种状况会再次读取计算属性的值,但因为 shouldCompute 会评估计算属性的派生状态为 U_T_D,也就是其深依赖没有扭转,所以会间接取上一次计算的后果来应用,不会再有其余任何解决。
计算属性的依赖扭转
这种状况下就会按依赖变动的失常流程走,AutoRun 里讲过,触发深依赖的 setter,扭转深依赖和派生(计算属性)的状态为 STALE,而后执行派生的 onBecomeStale() 办法。
onBecomeStale() 办法对于 Reaction 而言就是退出计划表,期待 batch 完结对立再次解决一遍 Reaction。对于计算属性而言略微有点变动:
- 会扭转计算属性作为 OV 的状态为 P_S,扭转计算属性的派生的状态为 P_S
- 把计算属性的派生列入计划表
onBecomeStale() {propagateMaybeChanged(this)
}
export function propagateMaybeChanged(observable: IObservable) {// invariantLOS(observable, "maybe start");
if (observable.lowestObserverState !== IDerivationState.UP_TO_DATE) return
observable.lowestObserverState = IDerivationState.POSSIBLY_STALE
observable.observers.forEach(d => {if (d.dependenciesState === IDerivationState.UP_TO_DATE) {
d.dependenciesState = IDerivationState.POSSIBLY_STALE
if (d.isTracing !== TraceMode.NONE) {logTraceInfo(d, observable)
}
// 将 Reaction 退出计划表,期待重新处理
d.onBecomeStale()}
})
// invariantLOS(observable, "maybe end");
}
而后等深依赖的 batch 完结,就会在计划表取出 Reaction 做解决,回到 Autorun 里的逻辑:
runReaction() {if (!this.isDisposed) {
// 开启一个事务处理,因为运行 cb 的过程中可能会再加 Reaction 到计划表(比方依赖更新)startBatch()
this._isScheduled = false
// 判断 Reaction 收集的依赖状态
// 如状态机所示,只有在 NO_TRACKING | STALE | 判断 COMPUTED 值变动时才会执行 Reaction
if (shouldCompute(this)) {
this._isTrackPending = true
try {
// 解决 cb
this.onInvalidate()
// ...
} catch (e) {this.reportExceptionInDerivation(e)
}
}
endBatch()}
}
评估计算
此时派生的状态就是 P_S,评估计算时就会走到上面的逻辑:
- 找到派生依赖的计算属性,并从新计算,扭转计算属性深依赖和本身作为派生的状态为 U_T_D
- 若从新计算值有变动,则会扭转计算属性作为 OV 的状态和计算属性派生的状态为 STALE,接着解决派生,让派生回调从新执行,从新建设依赖绑定关系。
- 若从新计算值没变动,则间接返回旧值,扭转派生和计算属性作为 OV 的状态为 U_T_D,阻止派生持续解决。
export function shouldCompute(derivation: IDerivation): boolean {switch (derivation.dependenciesState) {
case IDerivationState.UP_TO_DATE:
return false
case IDerivationState.NOT_TRACKING:
case IDerivationState.STALE:
return true
case IDerivationState.POSSIBLY_STALE: {
// state propagation can occur outside of action/reactive context #2195
const prevAllowStateReads = allowStateReadsStart(true)
// 此处对 CV 的 get 不须要 reportObserved (untrackedStart 的作用),之后会再执行进行收集
// 这里的次要目标是:判断从新计算的值有没有扭转,而后依据后果做一些状态变更
const prevUntracked = untrackedStart() // no need for those computeds to be reported, they will be picked up in trackDerivedFunction.
const obs = derivation.observing, // 拿到所有 OV
l = obs.length
for (let i = 0; i < l; i++) {const obj = obs[i]
// 找到 CV 的 OV
if (isComputedValue(obj)) {if (globalState.disableErrorBoundaries) {obj.get()
} else {
try {
// 再次调用 get 从新计算,具体逻辑下面剖析过
obj.get()} catch (e) {
// we are not interested in the value *or* exception at this moment, but if there is one, notify all
// 如果 CV getter 执行异样,那就默认让副作用继续执行一次
untrackedEnd(prevUntracked)
allowStateReadsEnd(prevAllowStateReads)
return true
}
}
// if ComputedValue `obj` actually changed it will be computed and propagated to its observers.
// and `derivation` is an observer of `obj`
// invariantShouldCompute(derivation)
// 若从新计算有变动了,其派生的状态会变成 STALE
if ((derivation.dependenciesState as any) === IDerivationState.STALE) {untrackedEnd(prevUntracked)
allowStateReadsEnd(prevAllowStateReads)
// 容许派生运行
return true
}
}
}
// 如果从新计算值没有变动,则重置派生与计算属性作为依赖的状态为 U_T_D
changeDependenciesStateTo0(derivation)
untrackedEnd(prevUntracked)
allowStateReadsEnd(prevAllowStateReads)
// 不容许派生持续运行
return false
}
}
}
以上,就达到了 计算属性依赖无变动时间接利用旧计算值 (防止多余计算)、 计算属性依赖变动且从新计算值变动时才会重新处理副作用(防止有效副作用)的目标。
React render
类组件
咱们能够用 @observer 去装璜一个组件:
import {observer} from 'mobx-react';
@observer
class AComponent {render() {return ...;};
}
理论 observer 做的外围工作就把 render 函数作为派生(用一个派生包住),而后每次 track 从新执行 render 的行为来收集依赖,当依赖扭转的时候,就触发 React.Component.prototype.forceUpdate()
去强制从新执行 render 收集依赖、更新视图。
N.B. observer 装璜后,React 的一些 lifecycle 钩子无奈触发,所以其实外部还做了一些伪造钩子的操作比方 shouldUpdate、willUnMount 以及一些优化和 fix bug 的操作,对于这些操作这里跳过,只讲外围原理代码。
咱们来看看源码里是怎么实现的:
export function observer<T extends IReactComponent>(component: T): T {
// ... 错误操作的报警
// ... 解决 ForwardRef
// 解决 Function component 暂且跳过
if (
typeof component === "function" &&
(!component.prototype || !component.prototype.render) &&
!component["isReactClass"] &&
!Object.prototype.isPrototypeOf.call(React.Component, component)
) {return observerLite(component as React.StatelessComponent<any>) as T
}
// Class Component
return makeClassComponentObserver(component as React.ComponentClass<any, any>) as T
}
export function makeClassComponentObserver(componentClass: React.ComponentClass<any, any>): React.ComponentClass<any, any> {
// 组件原型
const target = componentClass.prototype
if (componentClass[mobxObserverProperty]) {
// 错误操作报警
const displayName = getDisplayName(target)
console.warn(`The provided component class (${displayName})
has already been declared as an observer component.`
)
} else {
// 示意组件已被 Mobx 作为观察者
componentClass[mobxObserverProperty] = true
}
// 谬误报警
if (target.componentWillReact)
throw new Error("The componentWillReact life-cycle event is no longer supported")
// 实现 shouldComponentUpdate
if (componentClass["__proto__"] !== PureComponent) {if (!target.shouldComponentUpdate) target.shouldComponentUpdate = observerSCU
else if (target.shouldComponentUpdate !== observerSCU)
// n.b. unequal check, instead of existence check, as @observer might be on superclass as well
throw new Error("It is not allowed to use shouldComponentUpdate in observer based components.")
}
// 将 Props、State 包装成 OV
makeObservableProp(target, "props")
makeObservableProp(target, "state")
// 原始 render
const baseRender = target.render
// 被拦挡的 render,只首次 mount 会调这个
target.render = function () {
// 原始 render 内部包了一层派生
return makeComponentReactive.call(this, baseRender)
}
patch(target, "componentWillUnmount", function () {if (isUsingStaticRendering() === true) return
// 组件卸载时,解绑派生与依赖的绑定,防止内存透露
this.render[mobxAdminProperty]?.dispose()
this[mobxIsUnmounted] = true
if (!this.render[mobxAdminProperty]) {
// Render may have been hot-swapped and/or overriden by a subclass.
const displayName = getDisplayName(this)
console.warn(`The reactive render of an observer class component (${displayName})
was overriden after MobX attached. This may result in a memory leak if the
overriden reactive render was not properly disposed.`
)
}
})
return componentClass
}
function makeComponentReactive(render: any) {if (isUsingStaticRendering() === true) return render.call(this)
// 解决 forceUpdate 带来的副作用 ...
const initialName = getDisplayName(this)
const baseRender = render.bind(this)
let isRenderingPending = false
// 创立一个派生,带来的副作用就是第二个回调参数
const reaction = new Reaction(`${initialName}.render()`, () => {if (!isRenderingPending) {// N.B. Getting here *before mounting* means that a component constructor has side effects (see the relevant test in misc.js)
// This unidiomatic React usage but React will correctly warn about this so we continue as usual
// See #85 / Pull #44
isRenderingPending = true
if (this[mobxIsUnmounted] !== true) {
let hasError = true
try {
// 解决 forceUpdate 带来的副作用 ...
// forceUpdate 强制重渲染
if (!this[skipRenderKey]) Component.prototype.forceUpdate.call(this)
hasError = false
} finally {
// 解决 forceUpdate 带来的副作用 ...
if (hasError) reaction.dispose()}
}
}
})
reaction["reactComponent"] = this
reactiveRender[mobxAdminProperty] = reaction
// 之后 forceUpdate 的时候,从新执行的 render 都只是 reactiveRender
this.render = reactiveRender
function reactiveRender() {
isRenderingPending = false
let exception = undefined
let rendering = undefined
// 为该 reaction 派生收集原 render 函数内的依赖
reaction.track(() => {
try {
// 执行原 render 函数,拿到虚构节点
rendering = _allowStateChanges(false, baseRender)
} catch (e) {exception = e}
})
if (exception) {throw exception}
// 返回虚构节点给 React
return rendering
}
return reactiveRender.call(this)
}
函数组件
函数组件要达到的目标和类组件是统一的,都是 rerender 从新收集依赖,依赖变动触发 rerender。然而函数组件不能用 forceUpdate 这个 API,所以 Mobx 外部用了 React hooks 的小 trick 去实现了 forceUpdate 的成果。
因为这种小 trick 带来的副作用更多,所以这部分 mobx-react-light 里的解决很冗余,提炼代码来做解说:
function observerLite(baseFuncComponent) {return function(props) {const [tick, setTick] = useState(0);
// 利用 useState 伪造 forceUpdate
function forceUpdate() {setTick(tick + 1);
}
// 造一个函数组件的派生,useMemo 保障派生不会 rebuild
// 组件内依赖变动时,invoke forceUpdate,rerender
const r = useMemo(() => new Reaction('包裹函数组件的派生', forceUpdate), []);
// 组件卸载时解绑派生与依赖,防止内存透露
useEffect(() => () => r.dispose(), []);
let vnodes = null;
// 每轮 rerender 从新执行函数组件,追踪依赖
r.track(() => {vnodes = baseFuncComponent({...props, tick});
})
// 返回虚构节点给 React
return vnodes;
}
}
其余 API
action
以装璜器 action 润饰函数 fn 为例,其实就是重写了 fn 的描述符,把函数体由 createAction
包了一层:
return {
// name 函数名、descriptor.value 函数体
value: createAction(name, descriptor.value),
enumerable: false,
configurable: true, // See #1477
writable: true // for typescript, this must be writable, otherwise it cannot inherit :/ (see inheritable actions test)
}
export function createAction(actionName: string, fn: Function, ref?: Object): Function & IAction {
// ...
// 内部调用 fn 时,真正执行的是这个办法
const res = function() {
// executeAction 外部外围工作就是让 fn 的执行处于一轮事务当中
return executeAction(actionName, fn, ref || this, arguments)
}
;(res as any).isMobxAction = true
// ...
return res as any
}
export function executeAction(actionName: string, fn: Function, scope?: any, args?: IArguments) {const runInfo = _startAction(actionName, scope, args)
try {return fn.apply(scope, args)
} catch (err) {
runInfo.error = err
throw err
} finally {_endAction(runInfo)
}
}
// startAction 除了 startBatch 以外的操作,都是为了的确新开启一轮事务的污浊性,不被之前上下文的操作所影响。export function _startAction(actionName: string, scope: any, args?: IArguments): IActionRunInfo {
// ...
let startTime: number = 0
// 在 action 里,对 OV 的读取不收集办法 fn。因为 action 办法并不是副作用,而是要扭转依赖的动作。const prevDerivation = untrackedStart()
startBatch() // 开启一轮新事务
// 容许对依赖写
const prevAllowStateChanges = allowStateChangesStart(true)
// 容许对依赖读
const prevAllowStateReads = allowStateReadsStart(true)
// 记录该轮事务的一些信息,不便 endAction 时回退,放弃开启事务前的状态污浊。const runInfo = {
prevDerivation,
prevAllowStateChanges,
prevAllowStateReads,
notifySpy,
startTime,
actionId: nextActionId++,
parentActionId: currentActionId
}
currentActionId = runInfo.actionId
return runInfo
}
// 除了完结事务的操作,其余都是依据记录的该轮事务的一些信息,回退放弃开启事务前的状态污浊。export function _endAction(runInfo: IActionRunInfo) {if (currentActionId !== runInfo.actionId) {fail("invalid action stack. did you forget to finish an action?")
}
currentActionId = runInfo.parentActionId
if (runInfo.error !== undefined) {globalState.suppressReactionErrors = true}
// 回退开启事务前依赖的扭转权限
allowStateChangesEnd(runInfo.prevAllowStateChanges)
// 回退开启事务前依赖的读取权限
allowStateReadsEnd(runInfo.prevAllowStateReads)
// 完结事务,筹备批量解决收集的 Reaction
endBatch()
// 回退开启事务前的派生追踪
untrackedEnd(runInfo.prevDerivation)
// ...
globalState.suppressReactionErrors = false
}
其实看源码高深莫测了,次要目标就是让 action 的函数执行身处于一轮新的事务中,益处就是为了屡次扭转某 Derivation 的依赖时,只解决一次。回归上文讲得 transaction 的概念:
一个 Action 开始和完结时同时随同着事务的启动和完结,确保 Action 中(可能屡次)对状态的批改只触发一次 Reaction 的从新执行。
额定 API
额定的 API 在相熟了上文的所有内容后,浏览起来应该比较简单了,鉴于 API 太多,不一一做剖析,感兴趣自行开掘。
Mobx 设计思维
Mobx 作者 Michel Weststrate 有在一篇推文中论述过 Mobx 设计理念,然而有点过于细节,不相熟 Mobx 机制的同学可能不太看得懂。以下,在基于这篇推文联合上述源码,我用中文提炼一下,感兴趣能够去看原文。
对状态扭转作出反应永远好过于对状态扭转作出动作
针对这点其实与 Vue 响应式 or Redux 传递的理念雷同,就是 数据驱动。
再剖析这句话,“作出反应”意味着状态与副作用的绑定关系由框架(库)给你做好,状态扭转主动告诉到副作用,不必使用者(开发者)人为地解决。
“作出动作”则是在使用者已知状态更改的状况下,手动去告诉副作用更新。这起码就有一个操作是使用者必做的:手动在副作用内订阅状态的变动,这至多带来两个缺点:
- 无奈保障订阅量的冗余性,可能订阅多了可能少了,导致利用呈现不合乎预期的状况。
- 会让业务代码变得更 dirty,不好组织
最小的、统一的订阅集
以 render 作为副作用举例,如果 render 里有条件语句:
render() {if (依赖 A) {return 组件 1;}
return 依赖 B ? 组件 2 : 组件 3;}
首先,如果交给用户手动订阅,必须只能依赖 A、B 的状态一起订阅才行,如果订阅少了无奈呈现预期的 re-render。
而后交给框架去做解决怎么才好?依赖 A、B 一起订阅当然没故障,然而假如依赖 A、B 初始化时都有值,咱们有必要让 render 订阅依赖 B 的状态吗?
没必要,为什么?想一想如果此时依赖 B 的状态变动了 re-render 出现的成果会有什么不同吗?
所以在初始化时就订阅所有的状态是冗余的,如果应用程序简单、状态多了,没必要的内存调配就会更多,对性能有损耗。
故 Mobx 实现了 运行时解决依赖 的机制,保障副作用绑定的是最小的、统一的订阅集。源码参见上述“getter 里干了啥?”与“解决依赖”章节。
派生计算的合理性
说人话就是:杜绝失落计算、冗余计算。
失落计算:Mobx 的策略是引入状态机的概念去治理依赖与派生,让数学的逻辑性保障不会失落计算。
冗余计算:
- 对于非计算属性状态,引入事务概念,保障同一批次中所有对状态的同步更改,状态对应的派生只计算一次。
- 对于计算属性,计算属性作为派生时,当其依赖变动,计算属性不会立刻从新计算,会等到计算属性本身作为状态所绑定的派生再次用到计算属性值时才去从新计算。并且计算出雷同值会阻止派生持续解决。
通用性(笔者补充)
就 Mobx 库自身,与 UI render 没有绑定关系,与 event loop 中异步机制没绑定关系。
所以 Mobx 不像 Vue 2.x 响应式解决一样,须要收集 Wachter 而后赶在 ui render 前异步迭代解决 Wachter 对应的副作用。更新粒度也不一样,Vue 2.x 是组件,Mobx 就是副作用,副作用能够但不仅是组件。
不晓得 Vue 3.x 把响应式抽成一个 package 后还是不是这样,没研读过其源码了。(不过 Vue 2.x 源码记得过后钻研了很久,当初也忘得差不多了,当初再去捡起来又感觉耗时且带来的收益不大,可恶又无奈的学习边际效应 -_-||)
故:Mobx 实用于任一应用 ES 语法的场景。
最初
以往文章大多记录在 github,之后会尝试在社区输入,感觉有帮忙的同学能够关注,共同进步。
预报下一篇文章内容:mutable 与 immutable 的理念区别,Flux 思维,单 / 双向数据流的各自劣势与痛点,mobx 与 redux、recoil 及一些新生代状态治理库的横向比照。
不过下一篇可能更新得比较慢,有个想做的工具要先写,下一篇文章的内容也有在群里跟神光光神提过,可能光神先产出我就不公布了(逃