状態管理は、Web アプリケーションの最も重要な部分の 1 つです。 React フックへのグローバル変数の使用から、これら 3 つだけを挙げると、MobX、Redux、XState などのサードパーティ ライブラリの使用に至るまで、このトピックは最も議論が活発になるトピックの 1 つです。これは、信頼性が高く効率的なアプリケーション。
今日は、オブザーバブルの概念に基づいて、50 行未満の JavaScript でミニ状態管理ライブラリを構築することを提案します。これは小規模なプロジェクトには確かにそのまま使用できますが、この教育的な演習を超えて、実際のプロジェクトではより標準化されたソリューションを使用することをお勧めします。
新しいライブラリ プロジェクトを開始するときは、技術的な実装の詳細を考える前に、コンセプトを固定して開発をガイドするために、最初から API がどのようなものかを定義することが重要です。実際のプロジェクトの場合、TDD アプローチに従って作成されたライブラリの実装を検証するために、この時点でテストの作成を開始することも可能です。
ここでは、State と呼ぶ単一のクラスをエクスポートします。このクラスは、初期状態を含むオブジェクトと、オブザーバーで状態の変更をサブスクライブできる単一の Observ メソッドを含むオブジェクトでインスタンス化されます。これらのオブザーバーは、依存関係の 1 つが変更された場合にのみ実行する必要があります。
状態を変更するには、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
まず、コンストラクターで初期状態を受け入れ、後で実装する Observ メソッドを公開する State クラスを作成しましょう。
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; } observe(observer) { this.observers.push(observer); } }
ここでは、状態値を保持できる内部中間状態オブジェクトを使用することを選択します。また、この実装を完了するときに役立つ内部オブザーバー配列にオブザーバーを保存します。
これら 2 つのプロパティはこのクラス内でのみ使用されるため、接頭辞 # を付けてクラスに初期宣言を追加することで、少し構文を工夫してプライベートとして宣言できます。
class State { #state = {}; #observers = []; constructor(initialState = {}) { this.#state = initialState; this.#observers = []; } observe(observer) { this.#observers.push(observer); } }
原則として、これは良い習慣ですが、次のステップでプロキシを使用します。プロキシはプライベート プロパティと互換性がありません。詳細には立ち入りませんが、この実装を簡単にするために、ここではパブリック プロパティを使用します。
このプロジェクトの仕様の概要を説明するとき、内部状態オブジェクトへのエントリとしてではなく、クラス インスタンスの状態値に直接アクセスしたいと考えました。
このために、クラスが初期化されるときに返されるプロキシ オブジェクトを使用します。
その名前が示すように、プロキシを使用すると、ゲッターやセッターを含む特定の操作をインターセプトするオブジェクトの仲介者を作成できます。私たちのケースでは、最初のゲッターを公開するプロキシを作成します。これにより、状態オブジェクトの入力を、あたかも 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 をインスタンス化するときに初期状態オブジェクトを定義し、そのインスタンスからその値を直接取得できるようになりました。次に、そのデータを操作する方法を見てみましょう。
ゲッターを追加したので、次の論理的なステップは、状態オブジェクトを操作できるようにするセッターを追加することです。
最初にキーがこのオブジェクトに属していることを確認し、次に不必要な更新を防ぐために値が実際に変更されたことを確認し、最後に新しい値でオブジェクトを更新します。
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
これでデータの読み取りと書き込みの部分は完了です。状態値を変更して、その変更を取得できます。これまでのところ、私たちの実装はあまり役に立たないので、ここでオブザーバーを実装しましょう。
インスタンス上で宣言されたオブザーバー関数を含む配列がすでにあるので、値が変更されるたびにオブザーバー関数を 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!
わかりました。現在、データの変更に対応しています!
小さな問題ですが。ここまで注意していただいた方のために、私たちは当初、オブザーバーの依存関係の 1 つが変更された場合にのみオブザーバーを実行したいと考えていました。ただし、このコードを実行すると、状態の一部が変更されるたびに各オブザーバーが実行されることがわかります。
では、これらの関数の依存関係をどのように特定すればよいのでしょうか?
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 中国語 Web サイトの他の関連記事を参照してください。