浅析mobx原理并 仿写自己的数据管理库

栏目: JavaScript · 发布时间: 5年前

内容简介:mobx 是一个非常优雅的状态管理库,具有相当大的自由度,并且使用非常简单。 另一方面,太自由有时候会导致滥用,或者使用不当,导致行为不符合自己的预期,比如我一开始在使用的时候就有困惑如下的:首先还是丢出github的地址:

mobx 是一个非常优雅的状态管理库,具有相当大的自由度,并且使用非常简单。 另一方面,太自由有时候会导致滥用,或者使用不当,导致行为不符合自己的预期,比如我一开始在使用的时候就有困惑如下的:

  • action到底有什么用?
  • autorun怎么知道我使用了observable数据的?
  • 这个autorun的行为怎么如此怪,不符合预期,不是说声明了的值改变了就会自动执行?
  • ……

二、分析

首先还是丢出github的地址:

github.com/mobxjs/mobx

1、action的作用

这个问题的关键就在core/action目录下 我们用action装饰了之后,执行的方法被这么包装

浅析mobx原理并 仿写自己的数据管理库

startAction的代码如下

浅析mobx原理并 仿写自己的数据管理库
spy是什么呢?看下 官方说明

,简而言之,spy是一个全局的监控,监控每一个action行为,并统筹全局的state变更的状态,避免在一个action多次变更同一个state导致的多次reaction行为被调用,影响性能,举个例子:

import { observable } from 'mobx'
class Store {
  @observable a = 0

  test() {
    this.a = 1
    this.a = 2
    this.a = 3
  }
}

const store = new Store()

autorun(() => {
   console.log(a)
})

store.test()
// 0
// 1
// 2
// 3
复制代码

可以看到autorun除了初始化时执行了一次之外在每一次变更都被执行了一次 如果我们给test加上action

import { observable, action } from 'mobx'
class Store {
  @observable a = 0

  @action
  test() {
    this.a = 1
    this.a = 2
    this.a = 3
  }
}


const store = new Store()

autorun(() => {
   console.log(a)
})

store.test()
// 0
// 3
复制代码

可以看到在一次加了action之后,在一次action中不会多次调用autorun,更符合我们的预期行为(看需求),同时性能得到提升

PS:在react中,同步操作的视图更新会合并成一个事件,所以有没有加action在视图更新层面来说都是一次,但Reaction类的行为会多次执行

如果你看了mobx的代码,可以看到mobx的代码中充满了 if (notifySpy && process.env.NODE_ENV !== "production") 这也是spy的另外一个作用,就是帮助我们debug,借助mobx-react-devtools,我们可以清晰的看到数据变动,但是由于mobx太自由的写法,有些项目到处都是修改state的入口,会导致这个功能形同虚设:joy:

2、mobx的执行时收集依赖

mobx还有一个很唬的能力就是执行时的依赖收集,他能知道你在autorun,computed中使用了哪些数据,并在数据变动后触发执行。 如果是刚开始接触,就会觉得不可思议,mobx明明是一个运行时使用的数据管理的库,他又和我编写时没有关系,为什么会知道我的函数里使用了哪些变量呢?但仔细想想,他的这些监控都需要我们先运行一遍函数才行,可能是在这个地方动了手脚,翻开代码core/reaction

浅析mobx原理并 仿写自己的数据管理库
首尾有两个可疑的方法 startBatch() endBatch()

看下这俩的代码

浅析mobx原理并 仿写自己的数据管理库

可以初步判断mobx是通过globalState.inBatch来标记依赖收集的开始和结束, 接下来看下trackDerivedFunction

浅析mobx原理并 仿写自己的数据管理库

可以看到这一步主要是修改全局的状态,实际执行实际执行收集依赖的动作应该不在这个方法,应该和observableValue的get有关系

浅析mobx原理并 仿写自己的数据管理库

看下reportObserved方法

浅析mobx原理并 仿写自己的数据管理库

可以看到当前的observable被存进derivation中,自身也被标记为isBeingObserved。 至此我们可以知道,我们可以回答后面的两个问题:

  • mobx如何收集依赖? 当mobx开始收集依赖时,会先标记一个收集状态,然后在执行包含需要被观测的observableValue的数据的方法,在observableValue的get方法中执行收集,最后再把收集状态关闭。

  • 为什么autorun的行为不符合预期? autorun收集的依赖是在运行时可以被访问到的observableValue所以如下的用法是使用不当:

autorun(() => {
    if (...) {
        // ...
    } else {
        // ...
    }
})
复制代码

被监控到的值是可以被访问到的数据,所以必定只会对if中中或者else中的变化作出反应,还有一个就是加了@action之后一次action只会执行一次autorun(可能就不想预期一样可以监控每一次变化)

二、仿写

1、Derivation

这个类相当于一个依赖收集器,负责收集observable对应reaction

const trackWatches = [] // 存放reaction的栈,处理嵌套

class Derivation {

  constructor() {
    this.mEvents = new Map() // observable映射到的reaction
    this.reactionMap = new WeakMap() // reaction映射到的observable
    this.collecting = false // 是否在收集依赖
    this.reId = null // reaction的Id
  }

  beginCollect(reaction) {
    this.collecting = true
    if (reaction) {
      trackWatches.push(reaction)
      this.currentReaction = reaction
      this.reId = reaction.id
    }
  }

  endCollect() {
    trackWatches.pop()
    this.currentReaction = trackWatches.length ? trackWatches[trackWatches.length - 1] : null
    this.currentReaction ? this.reId = this.collectReaction.id : null
    if (!this.currentReaction) {
      this.collecting = false
      this.reId = null
    }
  }

  collect(id) {
    if (this.collecting) {
      // 收集reaction映射到的observable
      const r = this.reactionMap.get(this.currentReaction)
      if (r && !r.includes(id)) r.push(id)
      else if (!r) this.reactionMap.set(this.currentReaction, [id])

      // 收集observable映射到的reaction
      const mEvent = this.mEvents.get(id)
      if (mEvent && !mEvent.watches.some(reaction => reaction.id === this.reId)) {
        mEvent.watches.push(this.currentReaction)
      } else {
        this.mEvents.set(id, {
          watches: [this.currentReaction]
        })
      }
    }
  }

  fire(id) {
    const mEvent = this.mEvents.get(id)
    if (mEvent) {
      mEvent.watches.forEach((reaction) => reaction.runReaction())
    }
  }

  drop(reaction) {
    const relatedObs = this.reactionMap.get(reaction)
    if (relatedObs) {
      relatedObs.forEach((obId) => {
        const mEvent = this.mEvents.get(obId)
        if (mEvent) {
          let idx = -1
          if ((idx = mEvent.watches.findIndex(r => r === reaction)) > -1) {
            mEvent.watches.splice(idx, 1)
          }
        }
      })
      this.reactionMap.delete(reaction)
    }
  }
}

const derivation = new Derivation()
export default derivation
复制代码

这里简单实现,把所有回调行为都当作是一个reaction,相当于一个eventBus但是,key是obId,value就是reaction,只是省去了注册事件的步骤

2、Observable

首先实现observable,这里因为主要是以实现功能为主,不详细(只监控原始类型)

import derivation from './m-derivation'

let OBCount = 1
let OB_KEY = Symbol()

class Observable {

  constructor(val) {
    this.value = val
    this[OB_KEY] = `ob-${OBCount++}`
  }

  get() { // 在开启收集依赖时会被derivation收集
    derivation.collect(this[OB_KEY])
    return this.value
  }

  set(value) { // 设置值时触发
    this.value = value
    derivation.fire(this[OB_KEY])
    return this.value
  }
}

export default Observable
复制代码

根据Observable简单封装一下,监控原始数据类型

// 暴露的接口
import Observable from '../core/m-observable'

const PRIMITIVE_KEY = 'value'
export const observePrimitive = function(value) {
  const data = new Observable(value)
  return new Proxy(data, {
    get(target, key) {
      if (key === PRIMITIVE_KEY) return target.get()
      return Reflect.get(target, key)
    },
    set(target, key, value, receiver) {
      if (key === PRIMITIVE_KEY) return target.set(value)
      return Reflect.set(target, key, value, receiver) && value
    }
   })
}
复制代码

3、Reaction

实际被调用的一方,当observable的数据发生变化时会通过Derivation调用相应的reaction

import derivation from './m-derivation'

let reId = 0

class Reaction {
  constructor(obCollect, handle, target) {
    this.id = `re-${reId++}`
    this.obCollect = obCollect
    this.reactHandle = handle
    this.target = target
    this.disposed = false // 是否不再追踪变化
  }

  track() {
    if (!this.disposed) {
      derivation.beginCollect(this, this.reactHandle)
      const value = this.obCollect()
      derivation.endCollect()
      return value
    }
  }

  runReaction() {
    this.reactHandle.call(this.target)
  }

  dispose() {
    if (!this.disposed) {
      this.disposed = true
      derivation.beginCollect()
      derivation.drop(this)
      derivation.endCollect()
    }
  }
}

export default Reaction
复制代码

再把Reaction封装一下,暴露出autorun和reaction

import Reaction from '../core/m-reaction'

export const autorun = function(handle) {
  const r = new Reaction(handle, handle)
  r.track()
  return r.dispose.bind(r)
}

export const reaction = function(getObData, handle) {
  let prevVal = null // 数据变化时调用
  const wrapHandle = function() {
    if (prevVal !== (prevVal = getObData())) {
      handle()
    }
  }

  const r = new Reaction(getObData, wrapHandle)
  prevVal = r.track()
  return r.dispose.bind(r)
}
复制代码

4、测试autorun和reaction

import { observePrimitive, autorun, reaction } from './m-mobx'

class Test {

  constructor() {
    this.a = observePrimitive(0)
  }

  increase() {
    this.a.value++
  }
}

const test = new Test()

autorun(() => {
  console.log('@autorun a:', test.a.value)
})

window.dis = reaction(() => test.a.value,
() => {
  console.log('@reaction a:', test.a.value)
})

window.test = test
复制代码
浅析mobx原理并 仿写自己的数据管理库

5、Computed

computed类型数据乍看之下和get没有什么不同,但computed的特殊之处在于他即是观察者同时又是被观察者,所以我也把它当成一个reaction来实现,mobx的computed还提供了一个observe的钩子,其内部实现其实也是一个autorun

import derivation from './m-derivation'
import { autorun } from '../m-mobx'

/**
 * observing observed
 */

let cpId = 0

class ComputeValue {
  constructor(options) {
    this.id = `cp-${cpId++}`
    this.options = options
    this.value = options.get()

  }

  get() { // 收集cp的依赖
    derivation.collect(this.id)
    return this.value
  }

  computedValue() { // 收集ob依赖
    this.value = this.options.get()
    return this.value
  }

  track() { // 收集ob
    derivation.beginCollect(this)
    this.computedValue()
    derivation.endCollect()
  }

  observe(fn) {
    if (!fn) return
    let prevValue = null
    let firstTime = true
    autorun(() => {
      const newValue = this.computedValue()
      if (!firstTime) {
        fn({ prevValue, newValue })
      }
      prevValue = newValue
      firstTime = false
    })
  } 

  runReaction() {
    this.computedValue()
    derivation.fire(this.id)
  }
}

export default ComputeValue
复制代码

所以他的流程是这样的:

  • 在调用computed的时候先收集observaleValue对应的computedValue
  • 在computed.observe的时候则是直接收集observableValue对应reaction
  • 在autorun中收集computed依赖实际上手机的事computedValue对应的observableValue

6、测试computed

import { observePrimitive, autorun, reaction, computed } from './m-mobx'

class Test {

  constructor() {
    this.a = observePrimitive(0)
    this.b = computed(() => {
      return this.a.value + 10
    })
    this.b.observe((change) => console.log('@computed b:', change.prevValue, change.newValue))
  }

  increase() {
    this.a.value++
  }
}

const test = new Test()

reaction(() => {
  console.log('@reaction a:', test.a.value)
})

autorun(() => {
  console.log('@autorun b:', test.b.get())
})

window.test = test
复制代码
浅析mobx原理并 仿写自己的数据管理库

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First HTML and CSS

Head First HTML and CSS

Elisabeth Robson、Eric Freeman / O'Reilly Media / 2012-9-8 / USD 39.99

Tired of reading HTML books that only make sense after you're an expert? Then it's about time you picked up Head First HTML and really learned HTML. You want to learn HTML so you can finally create th......一起来看看 《Head First HTML and CSS》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具