В этой статье я попытаюсь:
Начнем рассмотрение с самого простого и типичного приложения Vue.js
var vm=new Vue({ el: '#app', data: { price: 5.00, quantity: 2 }, computed: { totalPriceWithTax() { return this.price * this.quantity * 1.03 } } })
Соответствующий шаблон будет таким:
<div id="app"> <div>Price: ${{ price }}</div> <div>Total: ${{ price * quantity }} </div> <div>Taxes: ${{ totalPriceWIthTax }}</div> </div>
Как мы можем видеть переменные объявленные в коде просто отображаются в шаблоне. При этом при изменение значения переменных в коде они автоматически будут обновляется в шаблоне. Такой механизм и называется реактивность Vue.js. Возникает резонный вопрос как Vue.js понимает что значение переменной изменилось и его нужно обновить в шаблоне.
В этой статье мы построим систему реактивности с нуля что бы понять как это работает во Vue.js
Начнем с рассмотрения простого скрипта JavaScript:
let price = 5 let quantity = 2 let total = price * quantity console.log(`total is ${total}` \\ total = 10 \\ Далее если мы изменим price price = 20 \\ и снова выведем значение total console.log(`total is ${total}` \\ оно не измениться и будет по прежнему равно 10
Возникает вопрос как нам сохранить вычисление total таким образом что бы мы могли снова его запустить когда значения price или quantity изменяться. Нам надо каким то образом сохранить код
let total = price * quantity в хранилище, а затем запускать его при необходимости. Для этого добавим функцию target
let price = 5 let quantity = 2 let total = 0 let target = null // целевая функция вычисления target = () => { total = price * quantity }
Далее добавим механизм сохранения функции вычисления:
let price = 5 let quantity = 2 let total = 0 let target = null let storage = [] // переменная для хранения // целевая функция вычисления target = () => { total = price * quantity } // функция сохранения record = () => { storage.push(target) } // перезапускаем ранее сохраненный код replay = () => { storage.forEach(run => run()) } record() target() console.log(total) // 10 price = 20 console.log(total) // 10 replay() console.log(total) // 40 // Теперь значение total изменилось
И там мы рассмотрели общий принцип идеи реализации реактивности на самом простом примере.
Теперь давайте сделаем наше решение более масштабируемым, создадим класс для сохранения наших зависимостей. Мы сделаем наш класс на основе шаблона Observer
// Dep сокращение от Dependency (Зависимость) class Dep { constructor() { // инициализируем наше хранилище this.subscribers = [] } // функция сохранения наших зависимостей depend() { if (target && !this.subscribers.includes(target)) { this.subsribers.push(target) } } // функция запуска всех зависимостей notify() { this.subscribers.forEach(sub => sub()) } }
Кстати во Vue.js то же есть класс Dep который по своему предназначению напоминает созданный нами класс.
Запустим наш код
const dep = new Dep() let price = 5 let quantity = 2 let total = 0 let target = () => { total = price * quantity } dep.depend() target() console.log(price, total) // 5 10 price = 20 console.log(price, total) // 20 10 dep.notify() console.log(price, total) // 20 40
Сейчас мы отслеживаем изменения значения только у конкретной выбранной переменной.
Далее нам нужно будет сделать так что бы у каждой отслеживаемой переменной был экземпляр Dep класса. Для этого нам нужно как то соединить все переменные которые мы хотим отслеживать и класс Dep.
Код который мы использование ранее
let target = () => {total = price * quantity } dep.depend() target()
Вместо него нам нужно функция watcher
watcher(() => { total = price * quantity})
Более универсальная функция:
function watcher(myFunc) { target = myFunc dep.depend() target() target = null }
Вернемся к нашему примеру и добавим в него функцию watcher
... function watcher(myFunc) { ... } watcher(() => { total = price * quantity }) console.log(total) // 10 price = 20 // значение total пока еще не изменилось console.log(total) // 10 dep.notify() // теперь значение обновилось console.log(total) // 40
Как я заметил ранее нам нужно что бы у каждой отслеживаемой переменной был экземпляр Dep класса, что бы при изменений значения перезапускалось зависимые функции .
let data = { price: 5, quantity: 2 }
watcher(() => { total = data.price * data.quantity})
но может быть другой watcher
watcher(() => { salePrice = data.price * 0.9 })
Так же нам необходимо каким то образом определять что внутри wacher функции что бы подключать Dep класс. Таким образом что бы при вызове первого watcher dep.depend() вызывался при обновление переменной price и quantity а во втором watcher dep.depend() вызывался только при обновление price.
Для этого нам нужен способ благодаря которому мы можем понять что значение переменной было изменено, и тогда мы можем вызвать в этот момент функцию обновления dep.notify()
Для этого во Vue.js 2.X используется Object.defineProperty()
Обратите внимание что Object.defineProperty() используется только в в первой и второй версии Vue.js В третьей версии для этих целей используется другой механизм основанный на Proxy.
Функция Object.defineProperty() — позволяет определять геттер и сеттер функции на любые свойства объекта
let data = { price: 5, quantity: 2 } Object.defineProperty(data, "price", { get() { console.log('Получение доступа к переменной' }, set() { console.log('Изменения значения переменной' } }) data.price // Получение доступа к переменной data.price = 20 // Изменения значения переменной
Обратите внимание что данный пример в реальности ничего не меняет, что бы это происходило нам нужно реализовать этот функционал:
let data = { price: 5, quantity: 2 } let internalValue = data.price Object.defineProperty(data, "price", { get() { console.log(`Получаем значение ${internalValue}` return internalValue }, set(newVal) { console.log(`Устанавливаем значение price ${newVal}`) internalValue = newVal } }) data.price // Получаем значение 5 data.price = 20 // Устанавливаем значение price 20
Далее нам нужно вызывать defineProperty для каждого значения в data
let data = { price: 5, quantity: 2 } Object.keys(data).forEach(key => { // устанавливаем начальное значение let internalValue = data[key] Object.defineProperty(data, key, { get() { console.log(`Получаем значение ${key}: ${internalValue}`) return internalValue }, set(newVal) { console.log(`Устанавливаем для ${key} значение ${newVal}`) internalValue = newVal } }) }) total = data.price * data.quantity // Получаем значение price: 5 // Получаем значение quantity: 2 data.price = 20 // Устанавливаем для price значение 20
Теперь объединим рассмотренные выше две идеи watcher плюс класс Dep и функции defineProperty
let data = { price: 5, quantity: 2 } Object.keys(data).forEach(key => { // устанавливаем начальное значение let internalValue = data[key] // у каждой переменной будет экземпляр класса Dep const dep = new Dep() Object.defineProperty(data, key, { get() { // при получение значения мы добавляем текущий target к subsribers dep.depend() return internalValue }, set(newVal) { internalValue = newVal // запускаем все наши subsribers dep.notify() } }) })
Теперь взглянем на то как наш пример должен выглядеть целиком
let data = { price: 5, quantity: 2 } let target, total, salePrice class Dep { constructor() { this.subscribers = [] } depend() { if (target && !this.subscribers.includes(target)) { this.subscribers.push(target) } } notify() { this.subscribers.forEach(sub => sub()) } } Object.keys(data).forEach(key => { let internalValue = data[key] const dep = new Dep() Object.defineProperty(data, key, { get() { dep.depend() return internalValue }, set(newVal) { internalValue = newVal dep.notify() } }) }) function watcher(myFunc) { target = myFunc target() target = null } watcher(() => { total = data.price * data.quantity }) wacher(() => { salePrice = data.price * 0.9 }) // Поэкспериментируем с нашим кодом total // 10 salePrice // 4.5 data.price = 20 // Упс все поменялось total // 40 salePrice // 18 data.quantity = 10 total // 200
Теперь подумаем как это может быть реализовано во Vue.js. Рассмотрим классическую картинку из документации Vue показывающее устройство реактивности во Vue
Здесь у нас есть Data, Геттеры/Сеттеры, Watcher. Объект Data для своей работы использует Object.defineProperty. Когда геттер задействуется запускается метод dep.depend() класса Dep. А когда сеттер задействуется вызывается метод dep.notify() так же класса Dep. Тут так же есть Watcher устройство которого более подробно мы рассмотрим в другой моей статье
Рассмотрим с помощью нашего примера реализацию реактивности в 3 версии Vue.js. Как уже было сказано оно реализуется через объект Proxy. Этот объект оборачивается вокруг другое объекта и позволяет перехватывать все действия в ним.
Используем Proxy в нашем примере:
let data = { price: 5, quantity: 2 } let target = null class Dep { constructor() { this.subscribers = [] } depend() { if (target && !this.subscribers.includes(target)) { this.subscribers.push(target) } } notify() { this.subscribers.forEach(sub => sub()) } } let deps = new Map() Object.keys(data).forEach(key => { deps.set(key, new Dep()) }) let data_without_proxy = data data = new Proxy(data_without_proxy, { get(obj, key) { deps.get(key).depend(); return obj[key] }, set(obj, key, newVal) { obj[key] = newVal deps.get(key).notify() return true } }) function watcher(myFunc) { target = myFunc target() target = null } let total = 0 watcher(() => { total = data.price * data.quantity }) // Поэкспериментируем с нашим кодом console.log(total) // 10 data.price = 20 // Упс все поменялось console.log(total) // 40
Теперь мы можем добавить новое реактивное свойство:
deps.set('discount', new Dep()) data['discount'] = 5 let salePrice = 0 watcher(() => { salePrice = data.price - data.discount }) console.log(salePrice) \\ 15 data.discount = 7.5 console.log(salePrice) \\ 12.5
Внимание вопрос: Зачем понадобилось в 3 версии Vue.js переходить от defineProperty к Proxy?
На самом деле defineProperty не позволяет создавать полноценные прозрачные реактивные системы. При определенных условиях например в коллекциях измененные значения не обнаруживаются реактивной системой и поэтому значения внутри массива нужно менять используя метод $set. Объект Proxy не имеет таких недостатков.
В этом уроке я попытался рассказать о том как:
Надеюсь моя статья поможет вам разобраться с тем как работает Vue.js немного лучше.
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
Обратите внимание у вас в коде ошибка. Первый скрин ошибка в названии totalPriceWithTax записана так: "totalPriceWithTax()", а в блоке так: <div>Taxes: ${{ totalPriceWIthTax }}</div>. Слово With записано неккоректно. Буква i идёт заглавной, т.е записано как totalPriceWIthTax, а должно быть так totalPriceWithTax