Наблюдатель
Pattern Observer широко используется в JS. document.addEventListener
— это пример шаблона Observer в чистом JS, где вы подписываетесь на события и обрабатываете их с помощью прослушивателей событий:
// Observer 1 function handleClick1() { console.log('Button clicked (Observer 1)'); } // Observer 2 function handleClick2() { console.log('Button clicked (Observer 2)'); } // Subscribe observers to the click event document.addEventListener('click', handleClick1); document.addEventListener('click', handleClick2);
В шаблоне Observer объект
document
действует как субъект (или источник события), и вы можете зарегистрировать несколько прослушивателей событий (наблюдателей) для различных событий (например, щелчок, нажатие клавиши, прокрутка и т. д.). Когда происходит событие, субъект (документ) уведомляет всех подписанных прослушивателей событий (наблюдателей), вызывая связанные с ними функции обратного вызова.
Другой пример шаблона Observer — MutationObserver. Это позволяет вам наблюдать и реагировать на изменения в DOM (объектной модели документа), подписываясь на определенные мутации DOM.
const targetNode = document.getElementById('target'); // Observer: Handle DOM mutations const observer = new MutationObserver((mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { console.log('DOM Mutation occurred - childList'); } else if (mutation.type === 'attributes') { console.log('DOM Mutation occurred - attributes'); } } }); // Options for the observer (specify what to observe) const config = { childList: true, attributes: true }; // Start observing the target node observer.observe(targetNode, config); // Trigger some DOM mutations for demonstration purposes targetNode.appendChild(document.createElement('span')); targetNode.setAttribute('data-custom-attr', 'custom-value');
MutationObserver действует как субъект (источник события), и вы можете зарегистрировать несколько объектов-наблюдателей (наблюдателей), которые будут получать уведомления при возникновении мутаций DOM. Когда происходит мутация, MutationObserver запускает функцию обратного вызова каждого зарегистрированного наблюдателя, предоставляя им информацию об изменениях, произошедших в DOM.
Мы можем считать, что WebSocket также является примером шаблона Observer.
На стороне сервера:
//Server const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); const subscribers = new Set(); wss.on('connection', (ws) => { // Add new client to the subscribers set subscribers.add(ws); // Observer: Handle incoming messages from clients ws.on('message', (data) => { console.log(`Received message from client: ${data}`); }); // Notify observer about an event (e.g., every 5 seconds) setInterval(() => { broadcast('Server sending data to all clients'); }, 5000); // Remove the client from subscribers when disconnected ws.on('close', () => { subscribers.delete(ws); }); }); function broadcast(message) { // Send message to all connected clients (subscribers) for (const client of subscribers) { client.send(message); } }
Сервер выступает в роли субъекта (источника события), а подключенные клиенты выступают в роли наблюдателей. Когда сервер отправляет данные подключенным клиентам, они получают уведомление и могут соответствующим образом реагировать, выполняя функции обратного вызова для обработки входящих данных.
Сторона клиента:
const ws = new WebSocket('ws://localhost:8080'); // Observer: Handle incoming messages from the server ws.onmessage = (event) => { console.log(`Received from server: ${event.data}`); };
В React типичным случаем использования шаблона Observer является управление состоянием. Популярные библиотеки управления состоянием, такие как Redux и MobX, используют шаблон Observer для управления состоянием приложения. Компоненты подписываются на хранилище состояний, и когда состояние обновляется, все подписанные компоненты уведомляются и перерисовываются с последним состоянием.
// Redux-like store const createStore = (reducer) => { let state; let subscribers = []; const getState = () => state; const dispatch = (action) => { state = reducer(state, action); subscribers.forEach((subscriber) => subscriber()); }; const subscribe = (subscriber) => { subscribers.push(subscriber); return () => { subscribers = subscribers.filter((sub) => sub !== subscriber); }; }; dispatch({}); // Initialize state return { getState, dispatch, subscribe }; }; // Reducer const counterReducer = (state = 0, action) => { switch (action.type) { case 'INCREMENT': return state + 1; case 'RESET': return 0; default: return state; } }; // Create a store using the reducer const store = createStore(counterReducer); // React Counter Component subscribing to the store function Counter() { const [count, setCount] = useState(store.getState()); useEffect(() => { const unsubscribe = store.subscribe(() => { setCount(store.getState()); }); return () => { unsubscribe(); }; }, []); const handleIncrement = () => { store.dispatch({ type: 'INCREMENT' }); }; const handleReset = () => { store.dispatch({ type: 'RESET' }); }; return ( <div> <h2>Counter</h2> <p>Count: {count}</p> <button onClick={handleIncrement}>Increment</button> <button onClick={handleReset}>Reset</button> </div> ); }
Система обработки событий React также использует шаблон Observer. Компоненты регистрируют прослушиватели событий для определенных событий так же, как и в чистом JS, и когда эти события происходят, подписанные компоненты получают обновления и реагируют соответствующим образом.
Прокси
В чистом JS есть объект Proxy
, который позволяет вам создать объект, который можно использовать вместо исходного объекта.
function expensiveFunction(n) { console.log(`Computing for ${n}...`); // Simulating some expensive computation return n * 2; } function createMemoizationProxy(targetFunction) { const cache = new Map(); return new Proxy(targetFunction, { apply(target, thisArg, args) { const key = args.toString(); if (cache.has(key)) { console.log(`Returning cached result for ${args}`); return cache.get(key); } const result = target.apply(thisArg, args); cache.set(key, result); return result; }, }); } const memoizedExpensiveFunction = createMemoizationProxy(expensiveFunction); console.log(memoizedExpensiveFunction(5)); console.log(memoizedExpensiveFunction(10)); console.log(memoizedExpensiveFunction(5));
Мы можем использовать шаблон Proxy для создания прокси-сервера мемоизации, который автоматически кэширует результаты функции и возвращает кэшированный результат, если те же аргументы предоставляются снова.
В React хорошим примером паттерна Proxy может быть Debounce:
import React, { useState, useEffect } from 'react'; // Simulating backend request for data function fetchData(selectedCheckboxes) { return new Promise((resolve) => { setTimeout(() => { const data = selectedCheckboxes.map((checkbox) => ({ name: `Data for checkbox ${checkbox}`, })); resolve(data); }, 500); }); } function Checkbox({ label, checked, onChange }) { return ( <div> <input type="checkbox" checked={checked} onChange={onChange} /> <label>{label}</label> </div> ); } function App() { const [selectedCheckboxes, setSelectedCheckboxes] = useState([]); const [data, setData] = useState([]); // Debounce function to optimize backend calls const debounce = (func, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func(...args), delay); }; }; // Fetch data when selectedCheckboxes change useEffect(() => { const fetchDataDebounced = debounce(() => { const promises = selectedCheckboxes.map((checkboxId) => fetchData(checkboxId)); Promise.all(promises).then((responseData) => { setData(responseData); }); }, 100); // Debounce the request for 100ms }, [selectedCheckboxes]); const handleCheckboxChange = (event, checkbox) => { const isChecked = event.target.checked; if (isChecked) { setSelectedCheckboxes([...selectedCheckboxes, checkbox]); } else { setSelectedCheckboxes(selectedCheckboxes.filter((cb) => cb !== checkbox)); } }; return ( <div> <Checkbox label="Checkbox 1" checked={selectedCheckboxes.includes(1)} onChange={(e) => handleCheckboxChange(e, 1)} /> <Checkbox label="Checkbox 2" checked={selectedCheckboxes.includes(2)} onChange={(e) => handleCheckboxChange(e, 2)} /> <Checkbox label="Checkbox 3" checked={selectedCheckboxes.includes(3)} onChange={(e) => handleCheckboxChange(e, 3)} /> <Checkbox label="Select All" checked={selectedCheckboxes.length === 3} onChange={(e) => e.target.checked ? setSelectedCheckboxes([1, 2, 3]) : setSelectedCheckboxes([]) } /> <div> {data.map((item, index) => ( <p key={index}>{item.name}</p> ))} </div> </div> ); } export default App;
Например, у нас есть три флажка, которые отправляют запросы данных при нажатии, и вы хотите оптимизировать внутренние вызовы с помощью прокси-запроса, который ожидает 100 мс, чтобы объединить несколько флажков в один запрос, вы можете использовать хуки React useState и useEffect для управления состояние флажка и шаблон устранения отказов для прокси-запроса.
Фасад
Шаблон Facade обычно используется в реальных сценариях, где необходимо упростить и предоставить унифицированный интерфейс для сложной системы или набора API.
// Complex API class API { static fetchData(endpoint) { console.log(`Fetching data from ${endpoint}`); return fetch(endpoint).then((response) => response.json()); } } // Facade for the API class APIFacade { static getUserData() { return API.fetchData('/api/users'); } static getProductData() { return API.fetchData('/api/products'); } } // Usage APIFacade.getUserData().then((userData) => console.log(userData)); APIFacade.getProductData().then((productData) => console.log(productData));
В этом сценарии у нас есть сложный API, представленный классом
API
, который извлекает данные из разных конечных точек API.APIFacade
выступает в роли фасада, предоставляющего более простой интерфейс для получения пользовательских данных и данных о продуктах с помощьюAPI
.
В React шаблон Facade часто используется для предоставления простого интерфейса сложной подсистеме компонентов. Этот шаблон способствует простоте и уменьшает зависимости между компонентами.
import React, { useState } from 'react'; // Subcomponents that handle specific aspects of the form import NameInput from './NameInput'; import EmailInput from './EmailInput'; import AgeInput from './AgeInput'; import SubmitButton from './SubmitButton'; // Facade Component function Form() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [age, setAge] = useState(''); const handleSubmit = () => { console.log('Submitted Form Data:', { name, email, age }); }; return ( <div> <h2>Form</h2> <NameInput value={name} onChange={setName} /> <EmailInput value={email} onChange={setEmail} /> <AgeInput value={age} onChange={setAge} /> <SubmitButton onClick={handleSubmit} /> </div> ); } export default ComplexForm;
В этом примере компонент
Form
действует как фасад и инкапсулирует сложную логику формы и несколько полей ввода (NameInput, EmailInput, AgeInput) и SubmitButton. Другие компоненты приложения могут использовать компонентForm
, не беспокоясь о внутренних деталях работы формы или об обработке данных.
Заключение
Я попытался показать варианты использования шаблонов проектирования в чистом JS и React. Как некоторые методы используют шаблоны под капотом. Я надеюсь, что это было полезно.
Если у вас есть какие-либо предложения, не стесняйтесь добавлять комментарии.