MobX 是一个用于管理应用程序状态的库,它的核心是透明的函数式响应编程,即当数据变化时,相关的视图或计算会自动更新。
MobX 使用单向数据流,其中操作改变状态,进而更新所有受影响的视图。
前置知识 修饰器 Decorator 类的修饰
修饰器(Decorator)函数,用来修改类的行为
修饰器是一个对类进行处理的函数,修饰器函数的第一个参数,就是索要修饰的目标类
修饰器本质就是编译时执行的函数
如果想添加实例属性,可以通过目标类的 prototype 对象操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Calculator{ @logger add(a,d){ return a+b; } } function logger(target,name,descriptor){ let oldValue = descriptor.value descriptor.value = function(){ console.log(`${name}(${Array.prototype.join.call(arguments,',')})`) return oldValue.apply(this,arguments) } } let c1 = new Calculator() c1.add(1,2)
Proxy
proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截。因此提供了一种机制,可以对外界的访问进行过滤和改写。
get 方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身。
set 方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 proxy 实例本身。
基本用法
Proxy 对象接受两个参数:目标对象(target)和处理器对象(handler)。
target:这是要被代理的对象。 handler:这是一个对象,其属性是当执行操作时定义的函数(称为“陷阱”),例如 get, set, has, deleteProperty, apply, 等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // 创建一个目标对象 let target = {}; // 创建一个处理器对象,定义对目标对象的拦截操作 let handler = { // 当读取属性时触发 get: function(obj, prop) { return prop in obj ? obj[prop] : `Property ${prop} not found`; }, // 当设置属性时触发 set: function(obj, prop, value) { obj[prop] = value; console.log(`Property ${prop} set to ${value}`); return true; } }; // 创建一个代理对象 let proxy = new Proxy(target, handler); // 测试代理对象 console.log(proxy.name); // 输出:Property name not found proxy.name = 'Kimi'; // 设置属性,并输出:Property name set to Kimi console.log(proxy.name); // 输出:Kimi
Observables(可观察对象) 可观察对象是 MobX 6 中的基础元素。它们可以是普通的 JavaScript 变量、对象、数组等。当这些可观察对象的值发生变化时,MobX 会自动通知依赖于这些值的 reactions。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { makeAutoObservable } from "mobx"; class Todo { title = ""; finished = false; constructor(title) { this.title = title; makeAutoObservable(this); } toggle() { this.finished = !this.finished; } }
Reactions(反应) 反应是指那些会对可观察对象的变化做出响应的函数。常见的反应有 autorun 和 reaction。 它们很少被直接使用到,通常被封装在其他库中(比如 mobx-react)或者是特定于您的应用程序的抽象。
使用 autorun autorun 会在其首次创建时运行一次,并在任何依赖的可观察对象发生变化时再次运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 import { autorun } from "mobx"; const todo = new Todo("Learn MobX"); autorun(() => { console.log(`Todo: ${todo.title}, Finished: ${todo.finished}`); }); // 输出: Todo: Learn MobX, Finished: false todo.toggle(); // 输出: Todo: Learn MobX, Finished: true
mobx-react 的 Observer 函数 它是一个高阶组件(Higher-Order Component,HOC),用于将 React 组件转换为响应式组件。当与 MobX 一起使用时,Observer 可以使组件在 MobX 状态发生变化时自动重新渲染。
工作原理
监视 MobX store:observer 函数会使得组件在渲染过程中自动订阅使用的所有 MobX observable。
自动重新渲染:当任何一个被订阅的 observable 发生变化时,组件会自动重新渲染。
实现一个简易的响应式系统 Mobx 是一个典型的观察-反应模型。这个模型可以理解为观察者模式,但是这个观察者模式的重点在于订阅,而这个模型的重点在于依赖收集。
实现 observable 熟悉 Vue 或者 ES6 的同学都知道如何检测到一个普通对象的读取,无非是 Object.defineProperty 或者 Proxy。借助其中一个 api,便可以实现简单的 observable 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function observable(target) { const _target = {...target} let proxy = {}; Object.keys(target).forEach(key => { Object.defineProperty(proxy, key, { get() { const value = _target[key] console.log(`read ${key} = ${value}`) return value }, set(value) { console.log(`set ${key} = ${value}`) _target[key] = value; return value; } }) }) return proxy; }
依赖收集与反应:从实现 autorun 开始 autorun 接收一个函数做参数,并且有个特性:它会立即执行参数 fn。
当 fn 函数执行的时候,如果依赖到了某个 observable,则必定会触发它的某个 key 的 getter,而此时也就可以确定,fn 依赖了 observable 的这个 key。
1 2 3 4 5 let watcher = null; function autorun(fn) { watcher = fn; fn() }
完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 // 定义一个全局变量 watcher,用于保存当前的观察函数 let watcher = null // autorun 函数用于注册一个观察函数 fn function autorun(fn) { // 将全局 watcher 设置为传入的函数 watcher = fn // 立即执行传入的函数 fn() } // observable 函数用于将一个普通对象转换为可观察对象 function observable(target) { // 创建对象的浅拷贝 const _target = { ...target } // 创建一个空对象 proxy,将作为返回的代理对象 let proxy = {} // 创建一个 Map,用于存储各个属性的观察者集合 let subs = new Map() // 遍历 target 对象的所有属性 Object.keys(target).forEach((key) => { // 为每个属性定义 getter 和 setter Object.defineProperty(proxy, key, { get() { // 如果存在全局 watcher,则将其添加到当前属性的观察者集合中 if (watcher) { let watchers = subs.get(key) if (!watchers) { // 如果当前属性还没有观察者集合,则创建一个新的集合 subs.set(key, new Set([watcher])) } else { // 否则将当前 watcher 添加到现有集合中 watchers.add(watcher) } } // 获取属性值 const value = _target[key] // 输出读取日志 // console.log(`read ${key} = ${value}`) return value }, set(value) { // 输出设置日志 // console.log(`set ${key} = ${value}`) // 更新属性值 _target[key] = value // 获取当前属性的观察者集合 let watchers = subs.get(key) // 如果存在观察者,则逐一执行观察者 if (watchers) { watchers.forEach((watcher) => { watcher() }) } // 返回设置的值 return value }, }) }) // 返回代理对象 return proxy } // 创建一个可观察对象 data,初始值为 { age: 10, name: "Zchary" } const data = observable({ age: 10, name: "Zchary" }) // 注册一个 autorun 函数,用于自动运行并输出 data 对象的状态 autorun(() => { console.log(`autorun: ${data.name} is ${data.age} years old`) }) // 通过定时器每隔 1 秒递增 data.age 属性的值,共执行 5 次 for (let i = 0; i < 5; i++) { setTimeout(() => { data.age++ }, 1000 * (i + 1)) }