Отзывчивость — одна из самых больших особенностей Vue. Это также одно из самых загадочных мест, если вы не знаете, что происходит за кулисами. Например, почему его нельзя использовать для объектов и массивов, таких какlocalStorageчто-то такое?
Давайте ответим на вопрос, при решении этой проблемы сделать Vue реактивным с помощьюlocalStorageиспользовать вместе.
Если вы запустите следующий код, вы увидите, что счетчик отображается как статическое значение и не изменяется, как мы ожидали, это потому, что setIntervallocalStorageизменил это значение в .
new Vue({
el: "#counter",
data: () => ({
counter: localStorage.getItem("counter")
}),
computed: {
even() {
return this.counter % 2 == 0;
}
},
template: `<div>
<div>Counter: {{ counter }}</div>
<div>Counter is {{ even ? 'even' : 'odd' }}</div>
</div>`
});
// some-other-file.js
setInterval(() => {
const counter = localStorage.getItem("counter");
localStorage.setItem("counter", +counter + 1);
}, 1000);
Хотя в экземпляре VuecounterСвойство является адаптивным, но оно не работает, потому что мы изменили его вlocalStorageисточник в .
Есть несколько решений, возможно, лучше всего использовать Vuex и сохранять значение с помощьюlocalStorageСинхронизировать. Но что, если нам нужно что-то столь же простое, как этот пример? Мы собираемся глубоко погрузиться в то, как работает система реактивности Vue.
Отзывчивость в Vue
Когда Vue инициализирует экземпляр компонента, он будет учитывать параметр данных. Это означает, что он будет перебирать все свойства в данных и использоватьObject.definePropertyПреобразуйте их в геттеры/сеттеры. Имея собственный установщик для каждого свойства, Vue может знать, когда свойство изменяется, и может уведомлять зависимости, которые должны реагировать на изменение. Как он узнает, какие иждивенцы зависят от свойства? При доступе к геттерам его можно зарегистрировать, когда вычисляемые свойства, функции наблюдателя или функции рендеринга обращаются к свойствам данных.
// core/instance/state.js
function initData () {
// ...
observe(data)
}
// core/observer/index.js
export function observe (value) {
// ...
new Observer(value)
// ...
}
export class Observer {
// ...
constructor (value) {
// ...
this.walk(value)
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
export function defineReactive (obj, key, ...) {
const dep = new Dep()
// ...
Object.defineProperty(obj, key, {
// ...
get() {
// ...
dep.depend()
// ...
},
set(newVal) {
// ...
dep.notify()
}
})
}
Так почемуlocalStorageНе отвечает?потому что это не объект со свойствами.
Но подождите, мы также не можем определять геттеры и сеттеры с массивами, но массивы в Vue все еще реактивны. Это потому, что массивы — это особый случай в Vue. Чтобы иметь адаптивные массивы, Vue за кулисами переопределяет методы массивов и работает с адаптивной системой Vue.
мы можемlocalStorageСделать что-то подобное?
Переопределить функцию localStorage
Сначала попытайтесь исправить первоначальный пример, переопределив метод localStorage, чтобы отслеживать, какие экземпляры компонентов запрашивают элементы localStorage.
// LocalStorage项目键与依赖它的Vue实例列表之间的映射。
const storeItemSubscribers = {};
const getItem = window.localStorage.getItem;
localStorage.getItem = (key, target) => {
console.info("Getting", key);
// 收集依赖的Vue实例
if (!storeItemSubscribers[key]) storeItemSubscribers[key] = [];
if (target) storeItemSubscribers[key].push(target);
// 调用原始函数
return getItem.call(localStorage, key);
};
const setItem = window.localStorage.setItem;
localStorage.setItem = (key, value) => {
console.info("Setting", key, value);
// 更新相关Vue实例中的值
if (storeItemSubscribers[key]) {
storeItemSubscribers[key].forEach((dep) => {
if (dep.hasOwnProperty(key)) dep[key] = value;
});
}
// 调用原始函数
setItem.call(localStorage, key, value);
};
new Vue({
el: "#counter",
data: function() {
return {
counter: localStorage.getItem("counter", this) // 我们现在需要传递“this”
}
},
computed: {
even() {
return this.counter % 2 == 0;
}
},
template: `<div>
<div>Counter: {{ counter }}</div>
<div>Counter is {{ even ? 'even' : 'odd' }}</div>
</div>`
});
setInterval(() => {
const counter = localStorage.getItem("counter");
localStorage.setItem("counter", +counter + 1);
}, 1000);
В этом примере мы переопределяемgetItemа такжеsetItem, чтобы собирать и уведомлять зависимостиlocalStorageКомпоненты проекта. в новомgetItem, мы замечаем, какой компонент запросил какой элемент, вsetItems, мы связываемся со всеми компонентами, запрашивающими элемент, и переопределяем их свойства данных.
Чтобы приведенный выше код работал, мы должныgetItemПередача ссылки на экземпляр компонента изменяет сигнатуру его функции. Мы также больше не можем использовать стрелочные функции, потому что в противном случае у нас не было бы правильногоthisстоимость.
Если мы хотим добиться большего, мы должны копнуть глубже. Например, как мы можем отслеживать зависимости без явных транзитивных зависимостей?
Как Vue собирает зависимости
Для вдохновения мы можем вернуться к системе реактивности Vue. Ранее мы видели, что при доступе к свойству данных свойство данныхgetterподпишет вызывающего абонента на дальнейшие изменения этого свойства. Но как узнать, кто звонил? Когда мы получаем атрибут данных, егоgetterФункция не имеет никаких входных данных о вызывающем абоненте. Функция получения не имеет входных данных, как она узнает, кого зарегистрировать в качестве иждивенца?
Каждое свойство данных поддерживает список зависимостей, на которые необходимо ответить в классе Dep. Если мы углубимся в этот класс, мы увидим, что зависимости уже определены в статических целевых переменных всякий раз, когда они регистрируются. Эта цель определяется очень загадочным классом Watche. На самом деле, эти наблюдатели будут фактически уведомлены об изменении свойства данных, и они будут инициировать повторную визуализацию компонента или повторное вычисление вычисляемого свойства.
Но кто они?
Когда Vue делаетdataКогда параметр является наблюдаемым, он также создает наблюдатель для каждой функции вычисляемого свойства и всех функций наблюдения (которые не следует путать с классом Наблюдателя) и функции рендеринга для каждого экземпляра компонента. Наблюдатели подобны компаньонам этих функций. В основном они делают две вещи:
- При создании они оценивают функцию. Это вызовет сбор зависимостей.
- Когда они получают уведомление об изменении значения, от которого они зависят, они перезапускают свою функцию.. Это приведет к пересчету вычисляемого свойства или повторному рендерингу всего компонента.
Прежде чем наблюдатель вызовет функцию, за которую он отвечает, происходит важный шаг:Они устанавливают себя в качестве целей для статических переменных в классе Dep.. Это гарантирует, что свойства реактивных данных будут зарегистрированы как подчиненные при доступе к ним.
Отследить, кто звонил в localStorage
Мы не можем этого сделать, потому что не можем использовать внутренности Vue. Однако мы можем использовать идею Vue о том, что наблюдатель может установить цель в статическое свойство перед вызовом функции, за которую он отвечает. Можем ли мы позвонитьlocalStorageУстановить ссылку на экземпляр компонента раньше?
Если предположить, что при установкеdataопция называетсяlocalStorage, вы можете вставить егоbeforeCreateа такжеcreatedсередина. Оба этих хука запускаются до и после инициализации параметра данных, поэтому мы можем установить целевую переменную, затем очистить эту переменную и сослаться на текущий экземпляр компонента (к которому мы можем получить доступ в хуке жизненного цикла). Затем в нашем пользовательском геттере мы можем зарегистрировать эту цель как зависимость.
Последнее, что мы хотим сделать, это сделать эти хуки жизненного цикла частью всех наших компонентов, мы можем сделать это с помощью глобального миксина для всего проекта.
// LocalStorage项目键与依赖它的Vue实例列表之间的映射
const storeItemSubscribers = {};
// 当前正在初始化的Vue实例
let target = undefined;
const getItem = window.localStorage.getItem;
localStorage.getItem = (key) => {
console.info("Getting", key);
// 收集依赖的Vue实例
if (!storeItemSubscribers[key]) storeItemSubscribers[key] = [];
if (target) storeItemSubscribers[key].push(target);
// 调用原始函数
return getItem.call(localStorage, key);
};
const setItem = window.localStorage.setItem;
localStorage.setItem = (key, value) => {
console.info("Setting", key, value);
// 更新相关Vue实例中的值
if (storeItemSubscribers[key]) {
storeItemSubscribers[key].forEach((dep) => {
if (dep.hasOwnProperty(key)) dep[key] = value;
});
}
// 调用原始函数
setItem.call(localStorage, key, value);
};
Vue.mixin({
beforeCreate() {
console.log("beforeCreate", this._uid);
target = this;
},
created() {
console.log("created", this._uid);
target = undefined;
}
});
Теперь, когда мы запускаем первый пример, мы получаем счетчик, который увеличивается на число каждую секунду.
new Vue({
el: "#counter",
data: () => ({
counter: localStorage.getItem("counter")
}),
computed: {
even() {
return this.counter % 2 == 0;
}
},
template: `<div class="component">
<div>Counter: {{ counter }}</div>
<div>Counter is {{ even ? 'even' : 'odd' }}</div>
</div>`
});
setInterval(() => {
const counter = localStorage.getItem("counter");
localStorage.setItem("counter", +counter + 1);
}, 1000);
Конец нашего мысленного эксперимента
Когда мы будем решать исходную проблему, имейте в виду, что это в основном мысленный эксперимент. В нем отсутствуют некоторые функции, такие как обработка удаленных элементов и удаленных экземпляров компонентов. Он также имеет некоторые ограничения, например, имя свойства экземпляра компонента должно храниться в том жеlocalStorageто же имя, что и элемент в . Тем не менее, основная цель — лучше понять, как Vue Reactive работает под капотом, и максимально использовать это, поэтому я надеюсь, что вы извлечете пользу из всех этих вещей.
источник:css-tricks.com, автор: roberto, перевод: паблик аккаунт "front-end full-stack developer"