Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >实现简版 react 状态管理器 mobx

实现简版 react 状态管理器 mobx

原创
作者头像
测不准
发布于 2022-08-07 10:39:11
发布于 2022-08-07 10:39:11
1.5K00
代码可运行
举报
文章被收录于专栏:与前端沾边与前端沾边
运行总次数:0
代码可运行

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 解析,所以我们需要配置下开发环境。安装包如下:

代码语言:shell
AI代码解释
复制
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
AI代码解释
复制
"start": "webpack -w" 边修改边打包

配置 webpack.config.js

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 相信大家都了解不多介绍了
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 使用事例

  1. observableimport {observable} from 'mobx' const obj = {name: 'obj', age: {val: 99}} const o = observable(obj) console.log(o)
image.png
image.png

由打印结果可知,mobx 是基于 Proxy 实现的数据监听,对于对象来说可以实现深度监听

  1. 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
image.png
image.png

由上图可知,autorun 默认会执行一次,当监听的对象的属性改变时,会自动触发 autorun 的执行函数。这里是函数和函数内部的变量有绑定关系,如果我们在 autorun 外面使用 console.log(o.name) 就不会触发回调执行。

实现 observable & autorun

observable 代理实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// ./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 方法,但这里的触发是没有必要的影响性能。所以我们从叶子结点开始处理,向上进行赋值。

这里是我们自己的实现,可以看到已经实现了递归代理

image.png
image.png

autorun 关联函数和依赖值

第一次执行我们可以很容易地写出,直接执行就好,那怎么关联函数和依赖的属性值呢?用过 vue3 的朋友应该了解,effect 函数也是和内部的属性进行关联的,我们可以定义一个全局变量存储,当执行 autorun 的函数时,对该变量进行赋值,同时我们可以通过拦截的 get 方法对属性和全局的值进行关联。所以这里我们还需要创建一个处理类进行操作。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// ./mobx/autorun.js

const autorun = (handler) => {
  Reaction.start(handler) // 全局赋值函数
  handler() // 第一次自动执行,触发 get
  Reaction.end(handler) // 执行完清空全局变量
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// ./mobx/reaction.js
let nowFn = null // 全局变量

class Reaction {
  // start 和 end 仅仅做了变量处理
  static start(handler) {
    nowFn = handler
  }
  static end() {
    nowFn = null
  }
}

还记得我们上面代理使用函数返回形式就是为了这里进行数据处理

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// ./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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
AI代码解释
复制
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'
image.png
image.png

实现 observable 装饰器

可以分为类装饰器,属性装饰器,方法装饰器,我们这里只简单实现下 observable 装饰器。装饰器知识感兴趣的小伙伴可自行查阅资料哈。

装饰器事例

代码语言:txt
AI代码解释
复制
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()
image.png
image.png

属性装饰器对应三个参数,我们改造下 observable 函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
// ./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
image.png
image.png

本小节我们了解了 mobx 两个属性的实现原理,以及结合 react 实现刷新的机制。mobx 还有很多其他属性,感兴趣的小伙伴可以自行查阅资料学习。如果有问题,欢迎交流学习!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
使用df和du命令检查linux中的磁盘空间
目录 使用 df 命令检查 Linux 中的磁盘空间 以人类可读的格式显示磁盘空间使用情况 检查特定文件系统磁盘空间 查看输出中的特定字段 检查 Linux 上的 inode 使用情况 使用 du 命令检查 Linux 中的磁盘空间 检查文件磁盘使用情况 检查目录磁盘使用情况 这 df 命令代表 disk filesystem. 它用于获取Linux 系统上文件系统的可用和已用磁盘空间使用情况的完整摘要。 这 du 命令,简称 disk usage, 用于估计文件空间使用情况。该du命令可用于跟踪占用硬盘驱
入门笔记
2022/06/02
2.4K0
【linux学习指南】磁盘分区挂载到目录,形成文件系统挂载点
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被 划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设 定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的
学习起来吧
2024/11/09
7670
【linux学习指南】磁盘分区挂载到目录,形成文件系统挂载点
linux中du,df查看磁盘空间大小还不一样
Linux查看磁盘空间一般可以用du,df,但是有些时候两个得到的结果却不一样. 分别用du,df查看根分区的大小 > root# du -k -d 1 / 628 /run 41736 /etc 0 /dev 6761392 /root 6905636 /var 4 /media 4 /mnt 206096 /boot 2247520 /opt 30812 /home 0 /proc 16 /lost+found 10319996
入门笔记
2022/06/02
1.8K0
linux中检查磁盘空间的12个有用的df命令
1. 检查文件系统磁盘空间使用情况 这 df 命令显示文件系统上的设备名称、总块数、总磁盘空间、已用磁盘空间、可用磁盘空间和挂载点信息。 [root@local ~]# df Filesystem 1K-blocks Used Available Use% Mounted on /dev/cciss/c0d0p2 78361192 23185840 51130588 32% / /dev/cciss/c0d0p5 24797380 22273432
入门笔记
2022/06/03
1K0
Ubuntu 为主分区扩容 – 命令行
事情是这样的,服务器系统盘是块 120GB 的 SSD,当时装系统的时候只给了 50GB,还剩下 70GB 的剩余容量,那么现在由于东西越来越多,需要把剩下的 70GB 容量也用上,先是去百度了一下,奇葩的事情发生了,全是 Ubuntu 图形界面的教程,史上第一次,震撼。
Balliol Chen
2022/04/23
7.9K0
Linux命令(14)——df命令
用于查看Linux文件系统的磁盘空间占用情况。可以利用该命令来获取硬盘被占用了多少空间,以及剩余空间等信息。
恋喵大鲤鱼
2018/08/03
3.3K0
Linux之df命令
原文链接:https://rumenz.com/rumenbiji/linux-df.html
入门笔记
2021/07/18
2.1K0
每天学一个 Linux 命令(47):df
df 命令用于显示磁盘的相关信息。df(Disk Free)的首字母组合,用来显示文件系统磁盘空间的使用情况。
民工哥
2021/03/15
1.4K0
Linux下GPT分区扩容
GPT分区不能使用gropwpart进行扩容分区,需要删除源有GPT分区,再次新建分区 1、查看当前分区状态 #查看分区是否是GPT [root@master ~]# fdisk -lu Disk /dev/vda: 64.4 GB, 64424509440 bytes, 125829120 sectors Units = sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O siz
用户6792968
2022/08/30
4.9K0
Linux 格式化和挂载数据盘 转
本文描述如何用一个新的数据盘创建一个单分区数据盘并挂载文件系统。本文仅适用于使用 fdisk 命令对一个不大于 2 TB 的数据盘执行分区操作。如果需要分区的数据盘大于 2 TB,请参考 32TB 块存储分区。
wuweixiang
2018/08/14
3.7K0
Linux 磁盘管理
Linux磁盘管理好坏管理直接关系到整个系统的性能问题。 Linux磁盘管理常用三个命令为df、du和fdisk。
小小工匠
2021/08/16
6.3K0
你知道du和df的统计结果为什么不一样
我们常常使用du和df来获取目录或文件系统已占用空间的情况。但它们的统计结果是不一致的,大多数时候,它们的结果相差不会很大,但有时候它们的统计结果会相差非常大。
杰哥的IT之旅
2021/01/06
1.4K0
逻辑卷迁移指南
2.通过上面的信息,我们看到当前系统内存在逻辑卷lv_test,即目标迁移逻辑卷。为了更好的体现迁移成功与否,我们向该逻辑卷写入500MB的文件f1
用户1456517
2019/03/05
1.2K0
制作属于自己的Linux系统
自制Linux首先得满足一定的条件,除了物理主机的配置外,我们还需要准备一块干净的磁盘。这里,为了更好更直观地体现实验效果,笔者使用VMWare做实验,并准备了1块名为"LinuxDIY"的虚拟磁盘,磁盘大小为10GB。关于VMware的使用及系统安装,可以参看这篇文章。
用户1456517
2019/03/05
3.5K0
制作属于自己的Linux系统
Linux类型虚拟机磁盘扩容
在虚拟机操作系统内的命令行终端上再次执行“fdisk -l”,发现虚拟磁盘总共有416101个柱面,但只使用了其中的208051个柱面,未被使用的柱面就是扩容之后的磁盘,下面需要为未被使用的柱面创建分区。
匿名用户的日记
2021/12/14
1.9K0
创建LV磁盘并扩容
基本的逻辑卷管理概念: PV(Physical Volume)- 物理卷 物理卷在逻辑卷管理中处于最底层,它可以是实际物理硬盘上的分区,也可以是整个物理硬盘,也可以是raid设备。 VG(Volumne Group)- 卷组 卷组建立在物理卷之上,一个卷组中至少要包括一个物理卷,在卷组建立之后可动态添加物理卷到卷组中。一个逻辑卷管理系统工程中可以只有一个卷组,也可以拥有多个卷组。 LV(Logical Volume)- 逻辑卷 逻辑卷建立在卷组之上,卷组中的未分配空间可以用于建立新的逻辑卷,逻辑卷建立后可以动态地扩展和缩小空间。系统中的多个逻辑卷可以属于同一个卷组,也可以属于不同的多个卷组。
jwangkun
2021/12/23
4.1K0
创建LV磁盘并扩容
Linux基础:磁盘分区管理
目录树的不同目录,可以挂载(mount)到不同的分区(partition),不同的分区可以有不同的文件格式。
腾讯IVWEB团队
2020/06/28
3.8K0
【Linux入门】查看磁盘容量
本文主要介绍在 CentOS 7.x 下如何查看磁盘整体容量、具体目录及文件磁盘容量占用情况。
参谋带个长
2023/12/18
3.2K0
RHCSA认证考试
在 mars.domain250.example.com 上执行以下任务。 ○ 复查 ○ 完成 配置网络设置 ○ 复查 ○ 完成 配置您的系统以使用默认存储库 ○ 复查 ○ 完成 调试 SELinux ○ 复查 ○ 完成 创建用户帐户 ○ 复查 ○ 完成 配置 cron 作业 ○ 复查 ○ 完成 创建协作目录 ○ 复查 ○ 完成 配置 NTP ○ 复查 ○ 完成 配置 autofs ○ 复查 ○ 完成 配置 /var/tmp/fstab 权限 ○ 复查 ○ 完成 配置用户帐户 ○ 复查 ○ 完成 查找文件 ○ 复查 ○ 完成 查找字符串 ○ 复查 ○ 完成 创建存档 在 venus.domain250.example.com 上执行以下任务。 ○ 复查 ○ 完成 设置 root 密码 ○ 复查 ○ 完成 配置您的系统以使用默认存储库 ○ 复查 ○ 完成 调整逻辑卷大小 ○ 复查 ○ 完成 添加交换分区 ○ 复查 ○ 完成 创建逻辑卷 ○ 复查 ○ 完成 创建 VDO 卷 ○ 复查 ○ 完成 配置系统调优 ○ 复查 ○ 完成 配置容器
Alone-林
2022/11/14
3.2K0
RHCSA认证考试
Linux 磁盘管理命令
df命令参数功能:检查文件系统的磁盘空间占用情况。可以利用该命令来获取硬盘被占用了多少空间,目前还剩下多少空间等信息。
用户4988085
2021/07/19
2.8K0
相关推荐
使用df和du命令检查linux中的磁盘空间
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验