[К содержанию](../readme.md#введение-в-web-разработку) # Vue.js практика: computed, watch ## #17 Криптономикон: рефакторинг * [YouTube](https://www.youtube.com/watch?v=_esgbWGiP3c) * [RuTube](https://rutube.ru/video/84b87c1a5285bb57d7771e67e9436fc7/?r=wd&t=613) **Содержание урока:** * [формирование списка проблем для рефакторинга](#список-проблем-существующего-кода) * [наличие в состоянии зависимых данных (computed)](https://www.youtube.com/live/_esgbWGiP3c?t=1980) **Материалы к изучению:** - [вычисляемые свойства](https://ru.vuejs.org/guide/essentials/computed.html) ## Расшифровка скринкаста ### Список проблем существующего кода * [x] одинаковый код в наблюдателях * [ ] при удалении остается подписка (таймер) * [ ] количество запросов (в АПИ есть возможность получить данные по списку тикеров) * [ ] запросы к АПИ внутри компонента * [ ] обработка ошибок АПИ * [x] наличие состояния зависимых данных * [ ] график переполняется * [x] при удалении тикера не меняется LocalStorage * [ ] LocalStorage и анонимные вкладки (LocalStorage может быть недоступен) * [ ] магические строки и числа (url, 5000 мсек, ключ localstorage, размер страницы) **Параллельно** * [x] график сломан, если везде одинаковые значения * [x] при удалении тикера остается выбор ### Наличие состояния зависимых данных Метод **filteredTickers** зависит от страниц и массива тикеров, в таком случае нужно использовать [вычисляемые свойства](https://ru.vuejs.org/guide/essentials/computed.html) В _Composition API_ у вычисляемых свойств другой синтаксис: ```js const calculatedProperty = computed(() => { // тут какие-то вычисления на основе состояний return result }) ``` Вычисляемые свойства кешируются, т.е. будут вычисляться только при изменнии используемых реактивных переменных (состояний). В нашем случае это "номер страницы", "строка фильтра" и "список тикеров" При использовании в шаблоне надо помнить, что **computed** это свойство, а не функция. Он не может принимать аргументы (не нужно указывать круглые скобки) Если установлен линтер, то он может показать ошибку, что присваиванию _hasNextPage_ не место в **computed** Т.к. это тоже вычисляемое свойство, то его тоже можно завернуть в **computed** >Попутно решили, что start и end тоже можно вынести в **computed** для единообразия В итоге получились такие вычисляемые свойства: ```js const startIndex = computed(() => { return (page.value - 1) * 6 }) const endIndex = computed(() => { return page.value * 6 }) // это объясняется чуть позже const filteredTickers = computed(() => { return tickers.value.filter( t => t.name.includes(filter.value.toUpperCase())) }) // не забудьте поменять в шаблоне filteredTickers на paginatedTickers const paginatedTickers = computed(() => { return filteredTickers.value.slice(startIndex.value, endIndex.value) }) // не забудьте удалить переменную hasNextPage const hasNextPage = computed(() => { // я уже упоминал, что computed возвращает реактивный объект, т.е. к его значению надо обращаться через свойство .value return filteredTickers.value.length > endIndex.value }) ``` Как понять что нужно использовать **computed**? Очень просто. Если нет прямого изменения состояния (присваивания), а только вычисление по другим реактивным переменным (в лекции сказали ещё "если результат используется в шаблоне", но это не так, в этом же примере _startIndex_ и _endIndex_ сделаны вычисляемыми, но они используются только в коде). Смотрим, что ещё можно поменять на **computed**? * **normalizeGraph** - переделайте в **computed** самостоятельно, заодно переименуйте в _normalizedGraph_ (прилагательное вместо глагола, т.е. у нас не действие, а свойство) * разбить **filteredTickers** на два вычисляемых свойства (выше уже есть код) ### Починка графика Если минимальное значение равно максимальному, то возвращать 50%, чтобы было ровное плато посередине. ```js const normalizedGraph = computed(() => { const maxValue = Math.max(...graph.value) const minValue = Math.min(...graph.value) if (maxValue == minValue) { return graph.value.map(() => 50) } return graph.value.map( price => 5 + ((price - minValue) * 95) / (maxValue - minValue) ) }) ``` ### Применение "наблюдателей" В коде часто встречаются условия "если изменилось что-то, то сделай то-то". Это значит, что можно использовать **watch** Рассматривается пример с удалением тикеров на второй странице. В результате мы остаемся на пустой странице. Вариантов решения два: * в методе удаления тикеров проверить длину массива _paginatedTickers_ и если он пустой, то уменьшить номер страницы * использовать наблюдатели >Обратите внимание, я использую синтаксис с параметрами. Первым параметром в функции приходит новое значение переменной. ```js watch(paginatedTickers, (value) => { if (value.length === 0 && page.value > 1) page.value-- }) ``` ### при удалении тикера остается выбор В метод **handleDelete** добавляем логику проверки и очистки выделенного тикера ```js if (sel.value == tickerToRemove) { sel.value = null } ``` Заодно поменяйте _sel_ на человеко-понятное наименование (названия переменных должны быть самоочевидны) ### Вынос сброса графика в методе **select** в watch Напоминаю текущий код: ```js function select (ticker) { sel.value = ticker graph.value = [] } ``` Он соответствует правилу "если изменилось что-то, то сделай то-то", соответственно можно его перенести очистку графика в наблюдатель (сделайте самостоятельно) ### Вынос логики сохранения тикеров в LocalStorage в наблюдатель (заодно исправится бага, когда список не сохранялся при удалении тикера) Наблюдатель реализуйте сами. И не забудьте изменить добавление тикеров в массив (иначе на сработает **watch** на массив) ```js tickers.value = [...tickers.value, newTicker] ``` Что делает этот код? Выражение `...array` делает декомпозицию массива, т.е. извлекает все его элементы и вставляет в новый массив. В итоге в массив тикеров записывается новый массив, содержащий все старые элементы и новый. ### Исправление дублирования логики в наблюдателях **page** и **filter** 1. Создаем вычисляемое свойство _pageStateOptions_, которое будет хранить и фильтр и страницу: ```js const pageStateOptions = computed(() => { return { filter: filter.value, page: page.value } }) ``` 1. И вешаем на него наблюдателя ```js watch(pageStateOptions, (value) => { window.history.pushState( null, document.title, `${window.location.pathname}?filter=${value.filter}&page=${value.page}` ) }) ``` --- **Задание** * [Назад](./web_08.md) | [Дальше](./web_10.md)