mobx
是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react
就一直使用 mobx
库,上手简单不复杂。
mobx vs redux
mobx
学习成本更低,性能更好的的状态解决方案(小编这里没有使用过 redux
,但是看过使用 redux
的状态管理代码,确实使用起来比较复杂)
- 开发难度低,书写简单
- 开发代码量少,清晰易读
- 渲染性能好,副作用自动执行
核心思想
状态变化引起的副作用应该被自动触发
- 应用逻辑只需要修改状态数据即可,
mobx
回自动渲染UI
,无需人工干预 - 数据变化只会渲染对应的组件
mobx
提供机制来存储和更新应用状态供React
使用react
通过提供机制把应用状态转换为可渲染组件树并对其进行渲染
这里配上官网的 mobx
执行流程图
页面的状态存储在 mobx
中,通过事件触发 mobx 的方法函数,改变状态,如果有计算属性(类似 vue
)依赖了 state
,计算属性的值也会改变, mobx
监听到了 react render
中的变量修改,重新执行 render
实现渲染。
mobx 使用
环境配置
因为 mobx
中使用了装饰器,还有需要对 jsx
解析,所以我们需要配置下开发环境。安装包如下:
npm i webpack webpack-cli babel-core(babel 核心模块) babel-loader(解析 js) @babel/preset-env(转 es5) @babel/preset-react"(解析 react) @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators(解析装饰器) mobx mobx-react react react-dom
配置启动命令
代码语言:txt复制"start": "webpack -w" 边修改边打包
配置 webpack.config.js
代码语言:javascript复制// 相信大家都了解不多介绍了
module.exports = {
entry: "./src/index.js",
output: {
filename: 'bundle.js',
path: require('path').resolve(__dirname, 'dist')
},
mode: "development",
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env','@babel/preset-react'],
plugins: [
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-class-properties"
]
}
}
}
]
}
}
mobx 使用事例
- observableimport {observable} from 'mobx' const obj = {name: 'obj', age: {val: 99}} const o = observable(obj) console.log(o)
由打印结果可知,mobx
是基于 Proxy
实现的数据监听,对于对象来说可以实现深度监听
- autorunimport {observable, autorun} from 'mobx' const obj = {name: 'obj', age: {val: 99}} const o = observable(obj) // 自动运行 默认先运行一次 autorun(() => { console.log(o.name) }) o.name = 'hello' // 对应一个 autorun
由上图可知,autorun
默认会执行一次,当监听的对象的属性改变时,会自动触发 autorun
的执行函数。这里是函数和函数内部的变量有绑定关系,如果我们在 autorun
外面使用 console.log(o.name)
就不会触发回调执行。
实现 observable & autorun
observable 代理实现
代码语言:javascript复制// ./mobx/observable.js
const observable = (target) => {
// 需要将 target 进行代理,创建可观察对象
return createObservable(target)
}
function craeteObservable(val) {
const handler = () => {
// 可以进行数据操作,方便拓展
return {
get(target, key) {
// Proxy 配合 Reflect 使用
return Reflect.get(target, key)
},
set(target, key, value) {
const val = Reflect.set(target, key, value)
return val // set 必须有返回值,语法规定
}
}
}
// 由上面可知我们需要对属性递归判断,对象都进行代理
return deepProxy(val, handler)
}
function deepProxy(val, handler) {
if (typeif val!== 'object') {
// 如果是基本类型直接返回
return val
}
// 这里我们要对属性值为对象的属性进行递归处理
for(const key in val) {
val[key] = deepProxy(val[key], handler)
}
return new Proxy(val, handler())
}
我们注意下
deepProxy
中的递归处理,我们不是如果这个值为对象就进行代理,而是如果值为对象接着递归遍历,这是因为我们如果对根结点进行代理了,当他属性值为对象时,我们在进行重新赋值回触发set
方法,但这里的触发是没有必要的影响性能。所以我们从叶子结点开始处理,向上进行赋值。
这里是我们自己的实现,可以看到已经实现了递归代理
autorun 关联函数和依赖值
第一次执行我们可以很容易地写出,直接执行就好,那怎么关联函数和依赖的属性值呢?用过 vue3
的朋友应该了解,effect
函数也是和内部的属性进行关联的,我们可以定义一个全局变量存储,当执行 autorun
的函数时,对该变量进行赋值,同时我们可以通过拦截的 get
方法对属性和全局的值进行关联。所以这里我们还需要创建一个处理类进行操作。
// ./mobx/autorun.js
const autorun = (handler) => {
Reaction.start(handler) // 全局赋值函数
handler() // 第一次自动执行,触发 get
Reaction.end(handler) // 执行完清空全局变量
}
代码语言:javascript复制// ./mobx/reaction.js
let nowFn = null // 全局变量
class Reaction {
// start 和 end 仅仅做了变量处理
static start(handler) {
nowFn = handler
}
static end() {
nowFn = null
}
}
还记得我们上面代理使用函数返回形式就是为了这里进行数据处理
代码语言:javascript复制// ./mobx/observable.js. createObservable
function createObservable(val) {
// 声明一个装门用来 代理的对象
let handler = () => {
// 每个属性都对应一个新的 reaction 实例
let reaction = new Reaction() // 每个属性都有自己对应的那个 reaction
return {
set(target, key, value) {
const val = Reflect.set(target, key, value)
// 当属性值改变的时候,我们依次执行该属性依赖的函数。放在 set 改值之后执行,这样 autorun 函数中就能拿到最新的属性值
reaction.run()
return val
},
get(target, key) {
// 获取对象属性时,进行依赖函数的收集,一个属性可以对多个函数
reaction.collect()
return Reflect.get(target, key)
}
}
}
...
改造 Reaction
类
let nowFn = null; // 当前的 autorun 方法
// 每个属性对应一个实例,每个实例有自己的 id 区分
let counter = 0
class Reaction {
constructor() {
this.id = counter
this.store = {} // 存储当前可观察对象对应的 nowFn {id: [nowFn, nowFn]}
}
static start(handler) {
nowFn = handler
}
static end() {
nowFn = null
}
collect() {
// 当前有需要绑定的函数 在 autorun 里,如果在 autorun 外使用不做关联
if (nowFn) {
this.store[this.id] = this.store[this.id] || []
this.store[this.id].push(nowFn)
}
}
run() {
// 依次执行
this.store[this.id]?.forEach(handler => {
handler()
})
}
}
// 收集 autorun方法, 帮我们创建 当前属性和autorun的关系
export default Reaction
验证下我们写的方法
代码语言:txt复制import {observable, autorun} from './mobx'
const obj = {name: 'obj', age: {val: 99}}
const o = observable(obj)
autorun(() => {
console.log(o.name)
})
o.name = 'hello'
o.name = 'emily'
实现 observable 装饰器
可以分为类装饰器,属性装饰器,方法装饰器,我们这里只简单实现下 observable
装饰器。装饰器知识感兴趣的小伙伴可自行查阅资料哈。
装饰器事例
代码语言:txt复制class Store {
@observable name = 'emily'
@observable age = 18
get allName() {
return this.name this.age
}
add = () => {
this.age =1
}
}
const store = new Store()
autorun(() => {
console.log(store.allName)
})
store.add()
store.add()
属性装饰器对应三个参数,我们改造下 observable
函数
const observable = (target, key, descritor) => {
// 如果不是装饰器,只有第一个参数就可以了,我们这里简单用第二个参数判断
if(typeof key === 'string') {
// 是通过装饰器实现的,先把装饰的对象就行深度代理
let v = descritor.initializer() // 获取原始值
v = createObservable(v)
let reaction = new Reaction()
return {
enumerable: true,
configurable: true,
// 处理同 Proxy
get() {
reaction.collect()
return v
},
set(value) {
v = value
reaction.run()
}
}
}
...
实现 react 的更新渲染
上面的事例只是介绍了 mobx
怎么进行数据拦截和触发执行的,那么怎么和 react
结合实现触发的呢?我们知道 autorun
会自动收集内部函数中使用的属性进而绑定关联,那我们在函数的 render
方法中使用了 store
的数据,当属性改变时,就会触发 autorun
,我们在 autorun
中重新渲染 react
就可以实现页面重绘。
计数器事例
代码语言:javascript复制import React from 'react'
import ReactDOM from 'react-dom/client'
import { observable, observer } from './mobx'
class Store {
@observable count = 0
get type() {
return this.count % 2 ? 'odd' : 'event'
}
add = () => {
this.count = 1
}
}
const store = new Store()
@observer
class Counter extends React.Component {
constructor(props) {
super(props)
}
// 我们可以看到 render 中依赖了 store 属性
render() {
const {store} = this.props
return <div>
<p>{store.count}</p>
<button onClick={() => {
store.add()
}}> </button>
<p>{store.type}</p>
</div>
}
}
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<Counter store={store} />)
实现 observer 方法
代码语言:javascript复制// ./mobx/observer.js
import autorun from "./autorun"
const observer = (target) => {
let cwm = target.prototype.componentWillMount
// 我们在 componentWillMount 中实现收集和重绘
target.prototype.componentWillMount = function() {
cwm && cwm.call(this)
autorun(() => {
// 只要依赖的数据更新了就重新执行
this.render() //收集依赖
this.forceUpdate() // 强制刷新
})
}
}
export default observer
本小节我们了解了 mobx
两个属性的实现原理,以及结合 react
实现刷新的机制。mobx
还有很多其他属性,感兴趣的小伙伴可以自行查阅资料学习。如果有问题,欢迎交流学习!