react+mobx6

982 阅读24分钟

1、了解Mobx

1、1 Mobx是什么?

mobx是一个简单、可扩展的状态管理工具。它通过透明的函数响应式编程(transparently applying functional reactive programming - TFRP)使得状态管理变得简单和可扩展。MobX背后的哲学很简单:

  • 任何源自应用状态的东西都应该自动地获得
  • 其中包括UI、数据序列化、服务器通讯,等等。

1、2 浏览器支持情况

  • Mobx <= 4版本可在任何ES5环境(包括浏览器和NodeJS)中运行

  • Mobx >= 5版本运行在任何支持 ES6 Proxy的浏览器

    Proxy support

1、3 概念与原则

概念

  • State(状态)

状态是驱动应用程序的数据,state可以存储的数据结构包括像普通对象、数组、类、循环数据结构或引用。如果希望mobx可以追踪这些状态,我们需要将这些随时间改变的属性标记为observable

  • Actions(动作)

可以修改state的方法被称为action(动作)

  • Derivations (派生)

任何来源是(state)并且不需要进一步交互的东西就是Derivation。

Derivations 包括许多方式: 用户界面(组件)、派生数据、后端集成:比如发送改变到服务器端

Mobx区分了两种derivation:

1、 Computed values,总是可以通过纯函数(不应该修改state)从当前可观测的state中派生

2、 Reactions, 当state改变时 需要自动运行的副作用(打印日志、获取网络请求、更新UI)autorun | when | reaction

两者之间的区别:Computed values 是自动响应状态变化的Reactions 是自动响应状态变化的副作用

黄金法则: 基于当前state创建的值,始终使用computed

原则

Mobx遵循单向数据流,利用action去修改state, 进而更新所有受影响的view

image.png

2、1 Observable state

定义可变状态(state)并使其可观察。属性,完整的对象,数组,Maps和Sets都可以被转换成可观察的对象。使得对象变为可观察的基本方法可以使用makeObservable为每个属性指定注解。

1)makeObservable(target, annotations?, options?)

作用:可以捕获已经存在的对象属性并且使得它们变为可观察的

参数说明: target: 任何javascript对象或者类的实例,一般情况下 makeObservable是在类的构造函数中调用,并且第一个参数是this

annotations: 为每一个成员映射注解。注意:使用装饰器时,该参数会被忽略

mobx中的主要注解如下:

  • observable 将state状态转变为可观察
  • action 将一个方法标记为可以修改state的action
  • computed 标记一个由state派生出来的新的值并缓存其输出的getter
  • flow 创建一个flow来管理异步操作 异步代码会被自动包装成action并修改状态

options:

  • autoBind:true // 默认使用action.bound/flow.bound 而不使用action/flow, 可以自动将动作绑定到目标对象;action.bound不要和箭头函数一起使用,因为箭头函数已经是绑定过的并且不能重新绑定
  • deep:false // 默认使用observable.deep 递归的将所有属性都转换为可观察属性
  • name: // 为对象提供一个调试名称,将会出现在打印错误信息中
  • proxy: false // 迫使observable(thing)使用非Proxy的实现

2)makeAutoObservable(target,overrides?, options?)

作用:是加强版的makeObservable,默认情况下它将自动推断出所有的属性,但是可以使用overrides重写某些注解的默认行为

区别:makeAutoObservable 因为新成员不需要显式地提及 更容易维护。 然而,makeAutoObservable 不能被用于带有 super 的类或 子类。

推断规则:

  • 所有自有属性都成为 observable
  • 所有getters都成为computed
  • 所有setters都成为action
  • 所有原型中的functions都成为autoAction
  • 所有原型中的生成器函数都成为flow

3)observable(source, overrides?, options?)

作用: observable可以作为一个函数进行调用,从而一次性将整个对象变为可观察的, source对象将被克隆并且所有的成员都会变成可观察的

4)observable与make(auto)Observable区别

  • make(Auto)Observable 会修改作为第一个参数传入的对象,而observable会创建一个可观察的副本对象
  • observable会创建一个Proxy对象,这意味着之后被添加到这个对象中的属性也会被侦测并使其转为可观察对象;如果想把一个对象转变为可观察的对象,并且这个对象中所有的成员都是事先已知的,那么建议使用makeObservable,因为非代理的速度稍快一些,而且它们在调试器和console.log中更容易检查
  • 使用场景: make(Auto)Observable推荐在类的构造函数中使用
import { observable } from 'mobx'
import { observer } from 'mobx-react'
import React from 'react'
 const todos = observable( [{
    id: 1, text: 'eatting', done: true,
  }, {
    id: 2, text: 'sleep', done: false,
  },{
    id: 3, text: 'playing', done: true,
  }])

const App = observer (() => {
  const addTodo = () => {
    todos.unshift({
      id: Date.now(), text: 'coding', done: false
    })
  }
  console.log(todos)
  return (
    <div>
      <button onClick={ addTodo }>add todo</button>
     <ul>
       {
         todos.map(todo => (
         <li key={todo.id}>{ todo.text }</li>
         ))
       }
     </ul>
    </div>
  )
})

export default App

5)值类型和类的实例不会被转换成可观察对象

mobx无法使基本数据类型可观察,因为值类型在javascript中是不可变的;无法通过把类的实例传给observable将其自动转换成可观察对象,一般认为这是类构造函数的职责

import { action, makeObservable, observable, toJS } from 'mobx'
import { observer, useLocalObservable } from 'mobx-react'
import React from 'react'

class Store {
  @observable name = '蝈蝈'
  @action
  changeName = (name) => {
    this.name = name
    console.log(this.name)
  }
}


const App = observer(() => {
  const store = useLocalObservable(() => new Store())
  return (
    <div>
      { store.name  }
      <button onClick={ () => store.changeName('果果') }>change</button>
    </div>
  )
})

export default App

基本数据类型要想成为可观察的,可以使用Mobx将它们包装起来

class Store {
  name = observable.box('蝈蝈')
}

// get() 对值进行访问 set() 对值进行更新

const App = observer(() => {
  const { name } = useLocalObservable(() => new Store())
  console.log(name)
  return (
    <div>
      { name.get()  }
      <button onClick={ () => name.set('果果') }>change</button>
    </div>
  )
})

2、2 Actions

action是任意一段修改state代码的方法。原则上,actions总是为了对一件事作出响应而发生;默认情况下,不允许在actions之外修改state,这有助于在代码中清楚地对状态更新发生的位置进行定位

用法:

  • action 注解

  • action(fn)

  • action(name, fn)

    action不仅仅是注解,它也是一个高阶函数,可以使用函数将其作为一个参数来调用,推荐为被包装的函数命名,它会返回一个具有相同签名的使用action包装过的函数

    1) 异步actions

    本质来说,异步进程在Mobx中是不需要任何特殊处理的,因为不论是何时引发的所有 reactions 都将会自动更新。然而,在异步进程中更新可观察对象的每个步骤(tick)都应该被标识为 action。

    如何在action异步操作成功之后,立即修改state的几种方式

import { observable, action, makeObservable, runInAction } from 'mobx';
import { observer, useLocalObservable } from 'mobx-react';
import React, { useEffect } from 'react';
import { getAllProducts } from './data/shop';

class Store {
  products = []
  state = 'pending'
  constructor() {
    makeObservable(this, {
      products: observable,
      state: observable,
      AsyncfetchAllProducts: action.bound,
      changeProducts: action.bound
    })
  }
   async AsyncfetchAllProducts() {
    //  try {
    //    const res = await getAllProducts()
    //    // await之后,修改状态的代码被包装成 动作
    //    runInAction(() => {
    //      this.products = res
    //      this.state = 'done'
    //    })
    //  } catch(error) {
    //    runInAction(() => {
    //      this.state = 'error'
    //    })
    //  }
    getAllProducts().then(res => {
      //  直接修改store里面的数据warnning提示 不报错
      // this.products = res
      // this.state = 'done'
    }, error => {
      // this.state = 'error'
      runInAction(() => {
        this.state = 'error'
      })
    })
  }
  changeProducts(products) {
    this.products = products
    this.state = 'done'
  }
}

const App = observer(() => {
  const { products, state, AsyncfetchAllProducts } = useLocalObservable(() => new Store())

  useEffect(() => {
    AsyncfetchAllProducts()
    // const res =  AsyncfetchAllProducts()
    // console.log('res===>')
    // res.cancel()
  }, [])

  return (
    <div>
      <span>status: { state }</span>
      <ul>
        {
          products.map(item => (
          <li key={item.id}>{ item.title }</li>
          ))
        }
      </ul>
    </div>
  )
})

export default App
  1. 定义额外的action函数 将action作为唯一修改state的方法
this.changeProducts(res)

changeProducts(products) {
  this.products = products
  this.state = 'done'
}
  1. 直接调用内联的action函数, 需要对其进行命名, 缺点是TypeScript无法对其进行类型推导
action('changeProducts', (res) => {
  this.products = res
  this.state = 'done'
})
  1. runInAction工具函数,接收代码块并在异步动作中执行,鼓励不要到处写action
// 将最终的修改放在异步动作里
runInAction(() => {
  this.products = res
  this.state = 'done'
})
  1. 使用flow代替async/await flow是async/await的一个替代方案, 不需要再用action进行包装。只是使用flow包装异步函数,使用function * (生成器函数)代替async, 使用yield代替await

优点是异步代码会被自动包装成action并修改状态

class Store {
  products = []
  state = 'pending'
  constructor() {
    makeObservable(this, {
      products: observable,
      state: observable,
      AsyncfetchAllProducts: flow.bound,
    })
  }
  AsyncfetchAllProducts = function * () { // 生成器函数
    try {
      const res = yield getAllProducts()
      // 异步代码会被自动包装成action并修改状态
      this.state = 'done'
      this.products = res
    } catch(error) {
      this.state = 'error'
    }
  }
}

注意点: flow的返回值是一个Promise,里面提供了一个cancel()方法,该方法可以打断正在运行的 generator 并取消它。

默认情况下,mobx6及以上版本会要求使用action来更改state, 可以使用mobx的配置来禁用此行为

2、3 Computeds

computeds是用来计算从其他可观察对象中派生的信息。只要依赖的可观察对象发生了改变,就会自动重新计算并缓存,在不被任何值观察的时候会被暂时停用,可以帮助我们减少需要存储的状态的数量,并且是被高度优化过得,请尽可能使用它们。

用法:

  • computed 注解

  • computed(options) 注解

  • computed(fn, options?)

    computed 也可以作为一个函数直接调用,就像 observable.box 一样创建一个独立的计算值。 在返回的对象上使用 .get() 获取当前的计算值。 这种使用 computed 的形式并不常见

let name = observable.box('gl')
let upperCaseName = computed(() => {
  return name.get().toUpperCase()
})

autorun(() => {
  console.log(upperCaseName.get())
})

name.set('knj')

// const Item = observer(({ item, store }) => {
//   const isSelected = computed(() => store.isSelected(item.id)).get()
//   return (
//     <div className={isSelected ? "selected" : ""}>
//         {item.title}
//     </div>
// )})

注意:computed的setter不能用来改变computed values, 而是直接用来修改里面成员的值,从而使computed values发生变化

class Store {
  price = 2
  amount = 3
  constructor() {
    makeObservable(this, {
      price: observable,
      amount: observable,
      total: computed
    })
  }

  get total() {
    return this.price * this.amount
  }

  set total(val) {
    this.price = val
  }
}

const store = new Store()

autorun(() => {
  console.log(store.total)
})
// computed的setter 不能用来改变computed values,而是用来修改它里面的成员的值,从而使得computed values发生变化
store.total = 5

2、4 Reactions

reactions是mobx中的重要概念,可以将Mobx中所有的特性融合在一起。Reactions的目的是对自动发生的副作用作出反应;但是这些API很少会被用到,他们经常被抽象到其他库里面(比如mobx-react)

Mobx对数据作出反应的几种方式:

  • autorun

    autorun(effect: (reaction) => void)

    接收一个函数作为参数,会立即执行一次,只要依赖的可观察的值(observable或computed)发生变化,就会再次触发

  • when

    when(predicate: () => boolean, effect?: () => void, options?) 一旦表达式的返回值为true, 副作用函数就会执行,只会执行一次

    如果没有传入effect函数,when会返回一个Promise类型的dispose, 用于手动取消。

  • reaction

    reaction(() => value, (value, preValue, reaction) => { sideEffect }, options?)

    是autorun的变种,第一个回调函数的值将作为第二个回调函数的参数

    执行时机:不同于autorun 和 when reaction仅对数据函数中访问的数据作出反应,效果函数只会在表达式返回的数据发生更改时触发

    效果函数的第三个参数reaction 调用reaction.dispose() 可以手动终止reaction 有点像when,只执行一次

class Store {
 price = 2
 amount = 3
 constructor() {
   makeObservable(this, {
     price: observable,
     amount: observable,
     total: computed
   })
 }

 get total() {
   return this.price * this.amount
 }

 set total(val) {
   this.price = val
 }
}


const store = new Store()

const dispose1 = autorun(() => {
 console.log('autorun===>', store.total)
})
dispose1()
// computed的setter 不能用来改变computed values,而是用来修改它里面的成员的值,从而使得computed values发生变化
store.total = 5

when(() => {
 return store.amount > 19
}, () => {
 console.log('when===>', store.amount)
})

reaction(() => {
 return store.amount
}, (data,preData, reaction) => {
 console.log('reaction===>', data, preData)
 reaction.dispose() // 手动终止reaction
})

setInterval(action(() => {
 store.amount++
}), 1000);

注意:1) autorun,reaction 和 when一直会等待可观察对象发生新的改变,为了阻止 reactions 永远地等待下去,它们总是会返回一个 disposer 函数,该函数可以用来停止执行并且取消订阅所使用的任何可观察对象。官方强烈建议,一旦不需要这些方法中的副作用时,请务必调用它们所返回的 disposer 函数。 否则可能导致内存泄漏。

2)谨慎地使用 reactions!不会经常创建 reactions。在我们的项目中不会直接使用这些 API 中的任何一个,只会通过比如 mobx-react 中的 observer 这样间接的方式创建出 reaction

3、Mobx 和 React

Mobx可以独立于React运行,但是他们通常是结合在一起使用的。react如何对可观察的数据作出响应

用法:

import {  observer } from 'mobx-react'
// 如果是函数组件的话,推荐使用更加轻量的mobx-react-lite包
const MyComponent = observer(props => ReactElement)

什么时候需要用到mobx?

Mobx的可观察能力作为React 组件的状态补充,在以下情况下可用:

1)层级嵌套很深 2)拥有计算属性 3)需要共享状态给其它observer component

类似获取UI state 状态加载的state 选择的state等 最好还是使用 useState hook,这样可以让你使用高级的 React suspense特性。

3、1 observer组件中使用外部状态

observer: 是一个HOC会自动订阅React组件在渲染期间被使用的可观察的对象。因此,当任何可被观察的对象变化的时候,组件会自动进行重新渲染

mobx可以非常灵活的组织管理state, 从技术角度 它不关心我们如何读取可观察对象,也不关心它们来自哪里。可以通过不同的设计模式去使用被observer包裹着的组件

什么时候使用observer ?

  • observer是增强你的组件,而不是调用你的组件,通常所有的组件都能用observer,它并不会导致性能损失。相反,更多的observer组件可以使渲染更高效,因为他们更新数据的颗粒度更细。

使用props

被观察对象可以作为props传递到组件中
import { action, makeObservable, observable } from 'mobx'
import { observer } from 'mobx-react'
import React from 'react'

class Timer {
  secondsPassed = 0
  constructor() {
    makeObservable(this, {
      secondsPassed: observable,
      increaseTimer: action.bound
    })
  }
  increaseTimer() {
    this.secondsPassed += 1
  }
}

const myTimer = new Timer()

// 在组件渲染期间 任何可被观察的对象发生改变 observer都会自动订阅并作出响应
const TimerView = observer(({ timer }) => {
  return (
    <span>secondsPassed: { timer.secondsPassed  }</span>
  )
})

const App = () => {
  return (
    // 被观察对象可以作为props传递到组件中
    <TimerView timer={ myTimer }></TimerView>
  )
}

setInterval(() => {
  myTimer.increaseTimer()
}, 1000);

export default App

使用全局变量

mobx不关心是如何引用的可观察对象,我们可以使用外部作用域的可观察对象(类似于import导入)
const App = observer(() => {
 return (
   <span>secondsPassed: { myTimer.secondsPassed }</span>
 )
})
//直接使用可观察对象效果很好,但是这通常会是通过模块引入,这种写法可能会使单元测试变得复杂。 因此,我们建议使用React Context。

使用React context(推荐)

import { action, makeObservable, observable } from 'mobx'
import { observer, Observer } from 'mobx-react'
import React, { createContext, useContext } from 'react'

const myContext = createContext(null)

class Timer {
  secondsPassed = 0
  constructor() {
    makeObservable(this, {
      secondsPassed: observable,
      increaseTimer: action.bound
    })
  }
  increaseTimer() {
    this.secondsPassed += 1
  }
}

const myTimer = new Timer()

const TimerView = observer(() => {
  // 只要依赖的可观察对象的属性发生改变  组件会自动进行重新渲染
  const { secondsPassed } =  useContext(myContext)
  return (
    <div>
      <span>secondsPassed: { secondsPassed }</span>
    </div>
  )
})

const App = () => {
  const { Provider } = myContext
  return (
    <Provider value={ myTimer}>
      <TimerView />
    </Provider>
  )
}

setInterval(() => {
  myTimer.increaseTimer()
}, 1000);

export default App

3、2 observer组件中使用内部状态

使用observer的可观察对象可以来自任何地方,我们也可以使用local state(本地可观察对象)。

以可观察类的方式使用useState

这里的类指的是全局定义的Mobx state,引入并使用class声明的

const TimerView = observer(() => {
  const [ timer ] = useState(() => new Timer())
  useEffect(() => {
    const timerId = setInterval(() => {
      timer.increaseTimer()
    }, 1000);
    return () => {
      clearInterval(timerId)
    }
  }, [timer])
  return (
    <div>
      <span>Seconds passed: {timer.secondsPassed}</span>
    </div>
  )
})

以可观察对象的方式使用useState

可以用observable直接创建一个可观察的对象

  const [ timer ] = useState(() => observable({
    secondsPassed: 0,
    increaseTimer() {
      this.secondsPassed++
    }
  }))

使用useLocalObservable hook

const [store] = useState(() => observable({})) 是通用的写法,我们可以调用mobx-react-lite包中的useLocalObservable hook简化这个写法

  const timer = useLocalObservable(() => ({
    secondsPassed: 0,
    increaseTimer() {
      this.secondsPassed++
    }
  }))

3、3 mobx-react 和 mobx-react-lite

在函数组件中我们使用mobx-react-lite作为默认包,mobx-react 是全量包,也会暴露 mobx-react-lite包中的任何方法,其中包含对函数组件的支持。 如果你使用 mobx-react,那就不要添加 mobx-react-lite 的依赖和引用了。

mobx-react的附加特性有:

  1. 对于类组件的支持
  2. Provider 和 inject Mobx原有的这些东西在有 React.createContext 替代后变得不必要了
  3. 特殊的可观察对象propTypes

3、4 mobx 使用注意点

  • 尽可能晚的从对象中获取值

TimerView 组件不会响应未来变化的更新,因为.secondsPassed不是在observer组件内部读取的而是在外部读取的,因此不会被追踪到

const myTimer = new Timer()

const TimerView = observer(({ secondsPassed }) => {
  return (
    <div>
      <span>Seconds passed: {secondsPassed}</span>
    </div>
  )
})


const App = () => {
  return (
    <TimerView secondsPassed = { myTimer.secondsPassed  } />
  )
}

setInterval(() => {
  myTimer.increaseTimer()
}, 1000);

export default App
  • 不要将可观察对象传递给不是observer的组件中

如果非要传递可观察的对象到未被observer包裹的组件中,要么因为是第三方组件,要么就是需要组件对Mobx无感知,那么就必须在传递前 转换可观察对象为显式

const myTimer = new Timer()

const TimerView =({ myTimer }) => {
  return (
    <div>
      <span>Seconds passed: {myTimer.secondsPassed}</span>
    </div>
  )
}


const App = observer(() => {
  // return (
  //   <TimerView myTimer={{
  //     secondsPassed: myTimer.secondsPassed
  //   }} />
  // )
  return <TimerView myTimer={toJS(myTimer)} />
})

setInterval(() => {
  myTimer.increaseTimer()
}, 1000);

export default App
  • 回调组件可能会需要 Observer组件
const myTimer = new Timer()

const TimerView =({ render }) => {
  return (
    <div>
      <span>Seconds passed: {render()}</span>
    </div>
  )
}


const App = observer(() => {
// 不能获得myTimer中的改变 因为它不是一个观察者
//  return <TimerView render={() => <span>{ myTimer.secondsPassed }</span>} />
// 两种处理方式 1)将TimerView组件变为observer组件 2) 将回调组件通过Observer组件包裹
// 将回调组件通过Observer组件包裹会正确响应变化
 return <TimerView render={() => (
  <Observer>
    {
      () => <span>{ myTimer.secondsPassed }</span>
    }
  </Observer>
 )} />
})

setInterval(() => {
  myTimer.increaseTimer()
}, 1000);

export default App
  • observer在类组件中的使用 (class组件只能在mobx-react中得到支持)
const TimerView = observer(
  class TimerView extends React.Component {
    render() {
      const { render } = this.props
      return (
        <div>
          <span>Seconds passed: {render()}</span>
        </div>
      )
    }
  }
)
  • useEffect与可观察对象

useEffect可被用于触发需要发生的副作用,它将会被约束在React组建的生命周期中。使用useEffect需要指定详细的依赖。这对于Mobx却不是必须的,因为mobx拥有一种真正能检查到依赖发生变化的方法,autorun 结合autorun可以很轻松的在hooks中使用useEffect

const App = observer(() => {
  const timer = useLocalObservable(() => ({
    secondsPassed: 0,
    increaseTimer() {
        this.secondsPassed++
    }
 }))

 useEffect(() => {
   // 会自动检测到可观察对象的变化
   autorun(() => {
     if (timer.secondsPassed > 10) {
       console.log('effect中执行')
     }
   })
 }, []) // [timer.secondsPasses]

 useEffect(() => {
   const timerId = setInterval(timer.increaseTimer, 1000);
   return () => {
     clearInterval(timerId)
   }
 }, [])
 return (
  <div>
    <span>Seconds passed: {timer.secondsPassed}</span>
  </div>
 )
})

3、5 优化React组件渲染

mobx通常比Redux更快

  • 使用大量小组件

observer 组件将跟踪他们使用的值,只要任何可观察对象的值发生变化 组件会重新渲染。所以你的组件越小,它们重新渲染产生的变化就越小。

  • 专用组件渲染列表

React在渲染大量数据时表现非常糟糕,因为协调器必须评估每个集合变化的集合所产生的组件。因此,建议使用专门的组件来映射集合并渲染这个组件,且不再渲染其他组件

const MyComponent = observer(({ todos, user }) => (
   <div>
       {user.name}
       // 当user.name改变的时候 React会不必要地协调所有的TodoView组件 尽管它不会重新渲染,但是协调的过程本身是非常昂贵的
       <ul>
           {todos.map(todo => (
               <TodoView todo={todo} key={todo.id} />
           ))}
       </ul>
   </div>
))
// 推荐
const MyComponent = observer(({ todos, user }) => (
   <div>
       {user.name}
       < TodosView todos={todos} />
   </div>
))

const TodosView = observer(({ todos }) => {
 return (
   <ul>
   {todos.map(todo => (
    <TodoView todo={todo} key={todo.id} />
    ))}
	 </ul>
 )
})
  • 晚一点使用间接引用值

使用 mobx-react 时,推荐尽可能晚的使用间接引用值。 这是因为当使用 observable 间接引用值时 MobX 会自动重新渲染组件。 如果间接引用值发生在组件树的层级越深,那么需要重新渲染的组件就越少。

const myTimer = new Timer()

const TimerView = observer(({ secondsPassed }) => {
  console.log('TimerView组件渲染了')
  return (
    <div>
      <span>Seconds passed: {secondsPassed}</span>
    </div>
  )
})


const App = observer(() => {
  useEffect(() => {
    const timerId = setInterval(myTimer.increaseTimer, 1000);
    return () => {
      clearInterval(timerId)
    }
  }, [])
  console.log('App组件渲染了')
  return (
    // 慢的示例 改变secondsPassed属性 不仅TimerView组件重新渲染了 组件的所有者App也必须重新渲染
    <TimerView secondsPassed={myTimer.secondsPassed} />
    // 快的示例 改变secondsPassed属性 只会触发TimerView组件的重新渲染
    // <TimerView timer ={myTimer} />
  )
})

4、Tips

4、1 定义数据存储stores

Stores 的主要职责是将 逻辑(logic)状态(state) 从组件中移至一个可独立测试的单元。

UI 状态 store会存储大量关于 UI 的松散耦合的信息。因为大多数应用在开发过程中会经常改变 UI 状态。对于同构应用,你可能还想为 store 提供一个默认值,以便组件能正常渲染。 你可以通过 React context 把 UI 状态 store 在应用中传递下去。

如何组合多个stores,多个stores之间如何通信???

  • 创建一个rootStore,把所有stores实例化,并共享引用。这种模式的优点是: 1)易于设置 2)支持强类型 3)只需要实例化一个rootStore,复杂的单元测试会变得简单一点
const myContext =  createContext(null)
class RootStore {
  constructor() {
    // 将rootStore传递给子容器
    this.userStore = new UserStore(this)
    this.todoStore = new TodoStore(this)
  }
}

class UserStore {
  constructor(rootStore) {
    // 将rootStore绑定到子容器实例上
    this.rootStore = rootStore
  }
  getTodos(user) {
    // 通过rootStore来访问todoStore
    return this.rootStore.todoStore.todos.filter(todo => todo.author === user)
  }
}
class TodoStore {
  todos = []
  rootStore
  constructor(rootStore) {
    this.rootStore = rootStore
    makeAutoObservable(this, {
      rootStore: false
    })
  }
}


const App = () => {
  const { Provider } = myContext
  return (
    <Provider value={ new RootStore() }>
      <Todo></Todo>
    </Provider>
  )
}

const Todo = observer(() => {
  const { todoStore, userStore } =  useContext(myContext)
  return (
    <div>
      <ul>
        {
          todoStore.todos.map(todo => (
          <li key={todo.id}>{ todo.title }</li>
          ))
        }
      </ul>
    </div>
  )
})

export default App

使用React时, rootStore一般会通过react context插入到组件树中

4、2 Mobx对何作出响应(重点)

在某些情况下,mobx可能并没有做出我们预想的响应。面对这样的情况,我们就需要了解Mobx如何确定对哪些事物做出响应

Mobx会对跟踪函数执行时读取的任何存在的可观察的属性做出响应。

  • "读取" 使用一个对象的属性,读取有两种方式,对象的点语法 和 中括号语法
  • "跟踪函数"可以是computed表达式、作为observer的函数式组件的渲染、作为observer的类组件的render()方法,也可以是作为第一个参数传递给autorun\reaction\when的函数
  • "跟踪函数执行时"意味着只有在跟踪函数执行时读取的可观察对象才会被跟踪,这些值在跟踪函数中是直接使用还是间接使用并不重要。但是从函数“引发”出来的东西将不会被跟踪(setTimeout、promise.then、await等)

换句话说,MobX 将不会对下面的情况做出响应:

  • 从可观察对象中获取到,但是并没有在跟踪函数中使用的值
  • 在异步调用的代码块中读取的可观察值

Mobx跟踪属性的访问,而不是属性值的本身

import { autorun, getDependencyTree, makeAutoObservable, makeObservable, observable, trace } from 'mobx'
import React from 'react'

class Message {
  title = 'gg'
  author = { name: 'fgl' }
  likes =  ['eat', 'sleep']
  constructor(title, author, likes) {
    makeAutoObservable(this)
  }
  updateTitle(title) {
    this.title = title
  }
}

let message = new Message()
 
// let title = message.title // 在跟踪函数外部使用可观察属性 mobx不会响应
const disposer = autorun(() => {
  console.log(message.title)
  // trace 验证mobx进行了跟踪
  trace()
})


message.updateTitle('knj')

const App = () => {
  return (
    <div>
      aaa
    </div>
  )
}

export default App 
const disposer = autorun(() => {
  // 在跟踪函数内部使用可观察属性 author和author.name在autorun中都被使用了
  console.log(message.author.name)
  trace()
})

message.updateTitle('knj')
// 如果想在action之外修改状态 runInAction
runInAction(() => {
  message.author.name = 'kth'
})

runInAction(() => {
  message.author = { name: 'zhs' }
})
const author = message.author
const disposer = autorun(() => {
  console.log(author.name)
  trace()
})

runInAction(() => {
  message.author.name = 'kth' // message.author 和 author是同一个对象 属性.name在autorun中被使用了,可以触发响应
})

runInAction(() => {
  message.author = { name: 'zhs' } // message.author并没有被autorun跟踪, 不会触发Mobx的响应
})

Mobx不跟踪异步访问的数据

autorun(() => {
  // setTimeout(() => console.log(message.likes.join(", ")), 1000)
  console.log(message.likes.join(", "))
  trace() // trace()验证mobx是否进行了跟踪
})

runInAction(() => {
  message.likes.push("Jennifer")
})

动态添加的对象的属性如何成为响应式

  • 支持proxy的环境中
autorun(() => {
  console.log(message.author.age)
  trace()
  // console.log(message.age)
})

runInAction(() => {
  message.author.age = 10
  // message.age = 10
})

注意:这仅适用于使用 observable 或 observable.object 创建的对象。类实例对象上新增的属性不会自动变 为可观察的。

  • 不支持proxy的环境中
// 不支持代理(Proxy)的环境
autorun(() => {
  // get可以跟踪尚不存在的属性
  console.log(get(message.author, "age"))
  trace()
})

runInAction(() => {
  set(message.author, "age", 10)
})

4、3 configure配置

MobX提供了一系列的配置项,绝大部分配置项都可以使用 configure方法控制。

  • useProxies

    默认情况下,Mobx使用proxy代理来让数组以及对象可观察。Proxy能够提供最佳的性能表现以及在不同环境下大多数行为的一致性。但是如果目标环境不支持proxy, 可以通过配置将proxy关闭,这种情况大部分是由于需要支持IE或在没有使用Hermes引擎的React Native环境中开发。

    取值:

    1. 'always' 默认值 MobX 只能运行在支持 Proxy的环境中,如果环境不支持 Proxy 将报错。

    2. 'never' Proxy将不会被使用,Mobx降级到non-proxy替代方案。兼容ES5的环境,可能会带来一些限制。(可观察的数组不再是真正的数组,传递给其他模块时,需要先使用slice创建一个浅拷贝)

    3. 'ifavailable' (实验阶段)如果环境支持则启用Proxy ,否则降级到non-proxy替代方案。

  • enforceActions

    取值:

    1. 'observed' 默认值 可观察状态必须通过actions来修改,这是默认选项,对于复杂的应用来说这是推荐的严格模式

    2. 'never' 状态可以在任何地方被修改

    3. 'always' 任何状态都只能通过actions来修改

    always理论上是最优的,但是在极少数情况下,我们可能惰性创建了某些可观察的对象,比如在一个计算属性中,我们可以使用runInAction将创建的可观察的高阶函数包起来

  • computedRequiresReaction

    禁止在action或者reaction之外,直接获取未被观察的计算属性的值, 默认值false

  • observableRequiresReaction

当未被观察的对象以可观察方式访问时 发出警告 默认值是false, 可以帮助我们发现组件遗漏了observer函数

5、mobx版本升级

在Mobx6之前,Mobx鼓励使用ES.next中的decorators,将某个对象标记为observable,computed,action 。因为装饰器语法尚未定案以及未被纳入ES标准,标准化的过程还需要很长时间,且未来定制的标准可能与之前的装饰器实现方案有所不同。处于兼容性的考虑,Mobx6舍弃了装饰器语法,并建议使用makeObservable / makeAutoObservable代替

为了兼容之前的写法,mobx6允许我们在任何可以使用observable action 和 computed注解的地方,也可以使用decorator

版本6之前的Mobx,不需要在构造函数中调用makeObservable(this)。在版本6中,为了让装饰器的实现更简单以及保证装饰器的兼容性,必须在构造函数中调用makeObservable(this)。Mobx可以根据 makeObservable第二个参数提供的装饰器信息,将实例设置为observable。

5、1 升级类以使用makeObservable

  • 最小的成本是保留decorators, 并在constructor中调用makeObservable(this)

  • 移除所有的decorators,并在constructor中调用makeAutoObservable(this)

makeAutoObservable 将使用新的装饰器autoAction的标记方法,只有当方法不在 derivation context 时,它才会应用action。这使得从计算属性中调用自动装饰的方法也很安全。

但是,迁移带有许多类的代码库工作量是很大的,我们可以使用code-mod自动完成上述过程!!

5、2 使用mobx-undecorate codemod 升级代码

mobx-undecorate包提供了一个codemod可以自动更新你的代码,使其更加符合MobX 6。此包无需安装;你只需要下载并使用npx工具,如果没有npx工具你必须首先安装。

  • 如果要弃用mobx装饰器的使用,并将它们替换为等效的makeObservable调用,在项目根目录 运行 npx mobx-undecorate
  • 想要Mobx继续支持装饰器语法,想保留它们并只在需要的地方引入makeObservable(this), 可以使用 --keepDecorators选项:npx mobx-undecorate --keepDecorators

6、mobx优劣势

优点:

  • 学习成本小: 不同于redux需要配置store, reducer action 如果涉及异步任务,还需要引入redux-thunk或redux-saga编写额外代码,Mobx流程相比就简单很多,并且不需要额外异步处理库;
  • 面向对象编程:Mobx支持面向对象编程,我们可以使用@observable and @observer,以面向对象编程方式使得JavaScript对象具有响应式能力;而Redux最推荐遵循函数式编程,当然Mobx也支持函数式编程;
  • 模版代码少:相对于Redux的各种模版代码,如,actionCreater,reducer,saga/thunk等,Mobx则不需要编写这类模板代码;
  • 对Typescript支持友好

缺点:

  • 过于自由,MobX提供的约定及模版代码很少,如果团队不做一些约定,容易导致团队代码风格不统一。
  • 无法支持 IE,proxy 无法在低版本语法里完整模拟,defineProperty 要求 property 必须先存在,所以不能动态增加对象 key,数组的很多原型方法也无法被劫持到等等
  • 调试困难,自动化就意味着黑盒化,意味着不太受掌控,改了数据会影响哪些组件,组件是由于哪个数据导致的更新,相对来说排查Bug更复杂一点
  • mobx对于数组的处理,会将它转换成observableArray,它不是一个数组类型,需要进行数组转换(如slice)

文档链接:

mobx6官网