状态管理是 Web 应用程序最重要的部分之一。从使用全局变量到 React hooks,再到使用 MobX、Redux 或 XState 等第三方库(仅举这三个),它是引发最多讨论的主题之一,因为掌握它以设计一个可靠且高效的应用程序。
今天,我建议基于可观察量的概念,用不到 50 行 JavaScript 构建一个迷你状态管理库。这个当然可以按原样用于小型项目,但除了这个教育练习之外,我仍然建议您为实际项目转向更标准化的解决方案。
当开始一个新的库项目时,重要的是从一开始就定义它的 API 是什么样子,以便在考虑技术实现细节之前冻结它的概念并指导它的开发。对于真正的项目,甚至可以在此时开始编写测试来验证库的实现,因为它是根据 TDD 方法编写的。
在这里,我们想要导出一个类,我们将其称为 State,该类将使用包含初始状态的对象和单个观察方法进行实例化,该观察方法允许我们使用观察者订阅状态更改。仅当这些观察者的依赖项之一发生更改时才应执行。
要更改状态,我们希望直接使用类属性,而不是通过像 setState 这样的方法。
因为一个代码片段胜过一千个单词,所以我们的最终实现在使用中可能如下所示:
const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.count += 1; state.text = 'Hello, world!'; state.count += 1; // Output: // Count changed 1 // Text changed Hello, world! // Count changed 2
让我们首先创建一个 State 类,该类在其构造函数中接受初始状态,并公开我们稍后将实现的观察方法。
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; } observe(observer) { this.observers.push(observer); } }
这里我们选择使用内部中间状态对象,它允许我们保留状态值。我们还将观察者存储在内部观察者数组中,当我们完成此实现时,该数组将很有用。
由于这两个属性仅在此类中使用,因此我们可以通过在它们前面添加 # 前缀并在类上添加初始声明来使用一些语法糖将它们声明为私有:
class State { #state = {}; #observers = []; constructor(initialState = {}) { this.#state = initialState; this.#observers = []; } observe(observer) { this.#observers.push(observer); } }
原则上这是一个很好的实践,但我们将在下一步中使用代理,它们与私有属性不兼容。在不深入细节的情况下,为了使实现更容易,我们现在将使用公共属性。
当我们概述该项目的规范时,我们希望直接在类实例上访问状态值,而不是作为其内部状态对象的条目。
为此,我们将使用一个代理对象,该对象将在类初始化时返回。
顾名思义,代理允许您为对象创建一个中介来拦截某些操作,包括其 getter 和 setter。在我们的例子中,我们创建一个代理,暴露第一个 getter,它允许我们暴露状态对象的输入,就好像它们直接属于 State 实例一样。
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); console.log(state.count); // 0
现在我们可以在实例化 State 时定义一个初始状态对象,然后直接从该实例中检索其值。现在让我们看看如何操作它的数据。
我们添加了一个 getter,因此下一个逻辑步骤是添加一个 setter,允许我们操作状态对象。
我们首先检查键是否属于该对象,然后检查值确实已更改以防止不必要的更新,最后用新值更新对象。
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; } } else { target[prop] = value; } }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); console.log(state.count); // 0 state.count += 1; console.log(state.count); // 1
现在我们已经完成了数据读写部分。我们可以更改状态值,然后检索该更改。到目前为止,我们的实现还不是很有用,所以现在让我们实现观察者。
我们已经有一个数组,其中包含在实例上声明的观察者函数,因此我们所要做的就是每当值发生变化时一一调用它们。
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; this.observers.forEach((observer) => { observer(this.state); }); } } else { target[prop] = value; } }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.count += 1; state.text = 'Hello, world!'; // Output: // Count changed 1 // Text changed // Count changed 1 // Text changed Hello, world!
太好了,我们现在正在对数据更改做出反应!
不过是个小问题。如果您到目前为止一直在关注,我们最初只想仅在观察者的依赖项之一发生更改时才运行观察者。但是,如果我们运行此代码,我们会看到每次状态的一部分发生更改时每个观察者都会运行。
那么我们如何识别这些函数的依赖关系呢?
Once again, Proxies come to our rescue. To identify the dependencies of our observer functions, we can create a proxy of our state object, run them with it as an argument, and note which properties they accessed.
Simple, but effective.
When calling observers, all we have to do is check if they have a dependency on the updated property and trigger them only if so.
Here is the final implementation of our mini-library with this last part added. You will notice that the observers array now contains objects allowing to keep the dependencies of each observer.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; this.observers.forEach(({ observer, dependencies }) => { if (dependencies.has(prop)) { observer(this.state); } }); } } else { target[prop] = value; } }, }); } observe(observer) { const dependencies = new Set(); const proxy = new Proxy(this.state, { get: (target, prop) => { dependencies.add(prop); return target[prop]; }, }); observer(proxy); this.observers.push({ observer, dependencies }); } } const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.observe((state) => { console.log('Count or text changed', state.count, state.text); }); state.count += 1; state.text = 'Hello, world!'; state.count += 1; // Output: // Count changed 0 // Text changed // Count or text changed 0 // Count changed 1 // Count or text changed 1 // Text changed Hello, world! // Count or text changed 1 Hello, world! // Count changed 2 // Count or text changed 2 Hello, world!
And there you have it, in 45 lines of code we have implemented a mini state management library in JavaScript.
If we wanted to go further, we could add type suggestions with JSDoc or rewrite this one in TypeScript to get suggestions on properties of the state instance.
We could also add an unobserve method that would be exposed on an object returned by State.observe.
It might also be useful to abstract the setter behavior into a setState method that allows us to modify multiple properties at once. Currently, we have to modify each property of our state one by one, which may trigger multiple observers if some of them share dependencies.
In any case, I hope that you enjoyed this little exercise as much as I did and that it allowed you to delve a little deeper into the concept of Proxy in JavaScript.
以上是用 JavaScript 代码编写状态管理库的详细内容。更多信息请关注PHP中文网其他相关文章!