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
配置启动命令
"start": "webpack -w" 边修改边打包
配置 webpack.config.js
// 相信大家都了解不多介绍了
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 代理实现
// ./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) // 执行完清空全局变量
}
// ./mobx/reaction.js
let nowFn = null // 全局变量
class Reaction {
// start 和 end 仅仅做了变量处理
static start(handler) {
nowFn = handler
}
static end() {
nowFn = null
}
}
还记得我们上面代理使用函数返回形式就是为了这里进行数据处理
// ./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
验证下我们写的方法
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
装饰器。装饰器知识感兴趣的小伙伴可自行查阅资料哈。
装饰器事例
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
就可以实现页面重绘。
计数器事例
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 方法
// ./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
还有很多其他属性,感兴趣的小伙伴可以自行查阅资料学习。如果有问题,欢迎交流学习!
原文地址:https://cloud.tencent.com/developer/article/2067951
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。