#javascript #reactjs #design-patterns #observable
Вопрос:
Вот ссылка для codesandbox
Привет, друзья, я изучаю наблюдаемый шаблон проектирования. Я пытаюсь отобразить обновление цены биткойна, но в настоящее время я получаю price
as undefined
. У меня вызывается API, а также создаются наблюдаемые методы для subscribe
и notify
. У меня есть наблюдатель, создаваемый как конструктор из наблюдаемого и пытающийся обновить состояние цены из API. Но цена указана как неопределенная. Не могли бы вы, пожалуйста, помочь мне, где я ошибаюсь ? Спасибо
export function Observable() {
this.observers = []; //array of observer functions
}
Observable.prototype = {
subscribe: function (fn) {
this.observers.push(fn);
},
fire: function () {
this.observers.forEach((fn) => {
fn.call();
});
}
};
import React from "react";
import { getBitcoinPrice } from "./api";
import { Observable } from "./observable";
const CryptoPrice = () => {
const [price, setPrice] = React.useState(0);
React.useEffect(() => {
const bitcoinObservable = new Observable();
console.log({ bitcoinObservable });
let livePrice = bitcoinObservable.subscribe(getBitcoinPrice);
setPrice(livePrice);
console.log({ livePrice });
let newPrice = bitcoinObservable.fire();
console.log({ newPrice });
}, [price]);
return (
<>
<h3>{price ? price : "undefined"}</h3>
</>
);
};
export default CryptoPrice;
Ответ №1:
С кодом было несколько проблем. Позвольте мне начать с реализации наблюдаемых:
- Огонь не передает значение наблюдателям! Их уведомляют только о том, что «что — то произошло», но не о том, что именно, — чтобы наблюдатели никогда не услышали фактическое
price
. - Вам определенно нужен
unsubscribe
метод!
Так:
export function Observable() {
this.observers = []; //array of observer functions
}
Observable.prototype = {
subscribe: function (fn) {
this.observers.push(fn);
},
unsubscribe: function (fn) {
const index = this.observers.findIndex(fn);
if (index >= 0) {
this.observers.splice(index, 1);
}
},
fire: function (x) {
this.observers.forEach((fn) => {
fn(x);
});
}
};
Несколько вещей с getBitcoinPrice
:
const data = await response.data;
это излишне. Выawait
на обещаниях и толькоaxios.get(url)
на обещаниях. После этогоresponse.data
это реальный объект, просто используйте его. Никакого вреда, однако, не причинено, если вы ждете невыполнения обещания, оно просто запускается немедленно.- Я увидел, что на самом
data.ticker.price
деле это строка, поэтому изменил ееdata.ticker.price
на число. Опять же, в этом приложении нет вреда, но я думаю, что это лучше в контексте более крупного приложения.
Так:
export const getBitcoinPrice = async () => {
const url = `https://api.cryptonator.com/api/ticker/btc-usd`;
try {
const response = await axios.get(url);
console.log('[getBitcoinPrice]', response.data.ticker.price);
return response.data.ticker.price;
} catch (error) {
console.log("[getBitcoinPrice] error: ", error);
}
};
Теперь перейдем к сути, к CryptoPrice
компоненту. Следующий способ-это один из способов сделать это, я уверен, что есть лучшие. В любом случае, я разделил обработку на 2 этапа: один-периодическая выборка цены и запуск наблюдаемого. Другой на самом деле наблюдает за изменением и производит эффект (здесь обновляется дисплей). Обратите внимание на использование функции очистки React useEffect
— никогда не забывайте освобождать ресурсы!
const CryptoPrice = () => {
const [price, setPrice] = React.useState(0);
const [bitcoinObservable] = React.useState(() => new Observable());
// this effect is responsible for triggering the price load periodically
React.useEffect(() => {
const interval = setInterval(async () => {
const x = await getBitcoinPrice();
bitcoinObservable.fire(x);
}, 5000);
// the cleanup function of the effect, never forget!!!
return () => {
clearInterval(interval);
};
}, [bitcoinObservable]);
// this effect is responsible for the subscription
React.useEffect(() => {
const handler = function(price) {
console.log('[CryptoPrice]', price);
setPrice(price);
};
bitcoinObservable.subscribe(handler);
return () => {
bitcoinObservable.unsubscribe(handler);
};
}, [bitcoinObservable]);
return (
<>
<h3>{typeof price === 'number' ? price : "undefined"}</h3>
</>
);
};
И последнее, ноль является ложным в Javascript, поэтому price ? price : 'undefined'
будет давать неопределенную начальную цену 0
. Отсюда и проверка типа typeof price === 'number' ? ... : ...
.
Еще пара вещей/указателей:
- Я бы предпочел иметь «1-ю фазу», т. е. создание «наблюдаемой цены биткойна» за пределами компонента, возможно, в сервисе. В основном разделение забот.
- Если вы используете инфраструктуру управления состоянием, такую как Redux, «служба» выше будет своего рода промежуточным программным обеспечением.
- Очень интересным обобщением обещаний и наблюдаемой закономерности являются реактивные потоки. Поначалу может быть трудно понять, но в долгосрочной перспективе они очень сильны.
- Если вы объедините Redux RxJS, проверьте redux-наблюдаемый.
Комментарии:
1. В вашем ответе содержится много информации, о которой я не знал. Спасибо. Но у меня есть несколько вопросов 1. Почему мы устанавливаем государство таким образом ? Я не вижу здесь Сетстейта.
const [bitcoinObservable] = React.useState(() => new Observable());
2. Добавляясимвол к
data.ticker.price
тому, как он становится числом ? 3. Используем ли мыsetInterval
для моделирования наблюдаемого поведения ? или это то, как наблюдаемые тоже работают в реальном времени, как вызов через некоторый интервал? Разве это не должно называться только тогда, когда есть изменения ?2. И тебе тоже спасибо. 1. Это стандартно, но используется реже: меня интересует только начальное состояние, поэтому я выбрасываю
..., setBitcoinObservable]
часть массива. И, используя функцию в качестве инициализатора, я гарантирую, что за время существования этого компонента будет создан только 1 наблюдаемый объект —setState
не будет вызывать функцию инициализатора при последующих отображениях. Я делаю это, потому что не хочу создаватьObservable
приложение, чтобы просто выбрасывать его при каждом рендеринге.3. 2. Это стандартный «трюк» JS для преобразования числовой строки в фактическое число. Например, см. MDN . 3. Я использовал
setInterval
, потому что предполагал, что вы хотите, чтобы цена периодически обновлялась. Я предположил это, потому что это причина для фактического использования наблюдаемого, чтобы наблюдать изменения. Наблюдаемое, которое никогда не меняет значения, было бы действительно печальным наблюдаемым 😀4. Спасибо вам за разъяснения. Но разве наблюдаемое уже не делает этого, например, обновляет цену всякий раз, когда происходит изменение, вместо того, чтобы явно использовать setInterval ? или это просто для того, чтобы пользовательский интерфейс видел новые цены ?
5. Нет, я думаю, вы неправильно поняли 🙂
useEffect
Зависит от цены и получает новую цену. «Зависит» означает, что приCryptoPrice
каждом отображении React будет сравнивать текущую цену с предыдущей. Если изменить, онuseEffect
снова запускает тело. Начальная цена равна нулю,а при первомgetBitcoinPrice
возврате она ненулевая.useEffect
чувствует это изменение иgetBitcoinPrice
снова срабатывает. Если во второй раз цена останется прежней (а это так и есть), дальнейшие обновления не будут запущены!
Ответ №2:
Прежде всего, я предполагаю, что вы намеренно создаете свою собственную наблюдаемую , чтобы чему-то научиться, вместо того, чтобы использовать подобную библиотеку rxjs
, поэтому я сосредоточусь на том, почему ваша текущая реализация не работает. Для производственного использования я бы настоятельно рекомендовал использовать проверенную в бою библиотеку для наблюдаемых объектов вместо создания собственной.
Теперь перейдем к вашему коду.
1. Вы перепутали подписку и эмиссию ( fire
в вашем случае)
Прямо сейчас ваш наблюдаемый будет выполнять все функции, переданные subscribe
при вызове fire
. Но ни один из наблюдателей никогда не получит результат API, потому что вы его не передадите fire
. Вы должны изменить свой наблюдаемый следующим образом:
export function Observable() {
this.observers = []; //array of observer functions
}
Observable.prototype = {
subscribe: function (fn) {
this.observers.push(fn);
},
// add parameter value
fire: function (value) {
this.observers.forEach((fn) => {
// call the observer callbacks directly with value
fn(value);
});
}
};
2. Вы никогда не «запускаете» свой наблюдаемый результат с помощью API.
Вместо этого вы передали функции Обещание, которое возвращает цену биткойна subscribe
. Как было сказано ранее, таким образом, ни один другой наблюдатель никогда не сможет получить результат API.
Вы должны изменить свой useEffect
таким образом:
React.useEffect(() => {
const bitcoinObservable = new Observable();
bitcoinObservable.subscribe((livePrice) => {
console.log("sub!", { livePrice });
setPrice(livePrice);
});
getBitcoinPrice().then((livePrice) => {
console.log('fire!', { livePrice });
bitcoinObservable.fire(livePrice);
});
}, [])
Теперь ваш пример песочницы кода уже работает нормально. Конечно, создание наблюдаемого и подписка на него в рамках одного и того же useEffect
вызова не имеет большого смысла. Но если вы хотите использовать свой наблюдаемый объект более чем в одном месте, вам следует сделать еще кое-что:
3. Добавьте логику отмены подписки, чтобы избежать утечек памяти
Компоненты React постоянно монтируются и размонтируются, и логика использования влияет на них. Если вы не отпишетесь от наблюдаемого, это может привести к утечке памяти. Поэтому вам следует добавить функцию отмены подписки, подобную этой, в свой наблюдаемый:
Observable.prototype = {
subscribe: function (fn) {
this.observers.push(fn);
// return an object with the unsubscribe function
return {
unsubscribe: () => {
this.observers.splice(this.observers.indexOf(fn));
}
};
},
// ...
};
Затем в своем useEffect
, вы можете отказаться от подписки в рамках обратного вызова очистки эффектов:
React.useEffect(() => {
// ...
// save the return object of the subscribe call
const subscription = bitcoinObservable.subscribe((livePrice) => {
console.log("sub!", { livePrice });
setPrice(livePrice);
});
// ...
// add cleanup logic
return () => {
subscription.unsubscribe();
};
}, []);
Это приведет к очистке ваших обратных вызовов подписки, если компонент будет уничтожен. Это будет необходимо, как только вы используете свой наблюдаемый в нескольких местах, а не в одном.
Если вы хотите узнать больше о наблюдаемых объектах, я рекомендую такие статьи, как эта: https://indepth.dev/posts/1155/build-your-own-observable-part-1-arrays
Комментарии:
1. Спасибо @maximillian за ответ