상태 관리는 웹 애플리케이션에서 가장 중요한 부분 중 하나입니다. 전역 변수 사용부터 React 후크, MobX, Redux 또는 XState와 같은 타사 라이브러리 사용에 이르기까지 3가지 이름만 지정하는 것은 가장 많은 토론을 불러일으키는 주제 중 하나입니다. 안정적이고 효율적인 애플리케이션입니다.
오늘은 Observable의 개념을 바탕으로 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를 포함한 특정 작업을 가로채는 중개자를 만들 수 있습니다. 우리의 경우 상태 객체의 입력이 State 인스턴스에 직접 속한 것처럼 노출할 수 있는 첫 번째 getter를 노출하는 프록시를 생성합니다.
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 중국어 웹사이트의 기타 관련 기사를 참조하세요!