Учебник по реализации реактивной системы Vue.js.
Привет! Как и было обещано, в этой статье мы собираемся реализовать реактивную систему Vue.js, которую мы узнали из моей предыдущей статьи. Давай начнем!
Эффект
В этом разделе мы собираемся реализовать класс Effect
; Пожалуйста, смотрите код ниже:
let activeEffect; class Effect { constructor(fn) { this.fn = fn } run() { activeEffect = this const result = this.fn() activeEffect = undefined return result } } function effect(fn) { const _effect = new Effect(fn) _effect.run() return _effect }
Как видите, если выполняется метод run любого Effect
, activeEffect
будет ссылаться на наш текущий эффект, чей метод run
выполняется в данный момент. Выполняя прогон, мы добавляем activeEffect
ко всем зависимостям цели в функции fn
.
Отслеживание и запуск
Как я упоминал в своей предыдущей статье, нам нужно запускать нашу цель всякий раз, когда для нее есть обновления, чтобы обновить все значения или получить от нее новые значения. Это необходимо, потому что нам нужны все новые значения или обновления для этой цели. Если мы читаем значение из нашей цели, нам нужно отслеживать его, чтобы определить, от каких целей зависит эта текущая цель. Я также должен упомянуть, что целью может быть вычисленное значение или значение ссылки и т. д.
function track(target) { // if we have any activeEffect if (activeEffect) { target.dep.add(activeEffect) } } function trigger(target) { const effects = [...target.dep] for (const effect of effects) { effect.run() } }
Ссылка
Пришло время реализовать нашу первую цель: Ref!
Это очень просто. Нам просто нужно запускать после каждого набора, отслеживать получение значения, и все готово!
class Ref { constructor(value) { this.dep = new Set() this.value = value } set value(value) { this._inner_value = value trigger(this) } get value() { track(this) return this._inner_value } } function ref(value) { return new Ref(value) }
Тестовый эффект с реф.
Теперь давайте проверим это с помощью этого кода:
const firstname = ref('Ehsan') const lastname = ref('Movaffagh') let fullname = '' effect(() => { fullname = `${firstname.value} ${lastname.value}` }) console.log(fullname) // Ehsan Movaffagh firstname.value = 'banana' console.log(fullname) // banana Movaffagh
Ух ты! Отлично. Значение fullname
изменилось после того, как мы изменили файл firstname
. Это потому, что после каждого изменения будет выполняться метод эффекта.
Вычислено
Теперь пришло время для более сложного: вычисления значений! У вычисляемых значений есть функции получения, и внутри этих функций у нас есть одна или несколько целей. Поэтому нам нужно создать эффект для этой функции-получателя, чтобы понять, когда эти целевые значения обновляются.
class Computed { constructor(name, getter) { this.dep = new Set() this._cached_value = undefined this.effect = effect(() => { console.log(`run computed effect ${name}`) this._cached_value = getter() trigger(this) }) } get value() { track(this) return this._cached_value } } function computed(name, fn) { return new Computed(name, fn) }
Я добавил это console.log
, чтобы показать вам, когда будет запущен эффект вычисляемого значения!
Тестовый расчет
Теперь давайте проверим это с помощью этого кода:
const number = ref(1) const number2 = ref(2) const sum = computed('sum', () => { return number.value + number2.value }) // run computed effect sum console.log(sum.value) // 3 number.value = 2 // run computed effect sum console.log(sum.value) // 4
Давайте создадим еще одно вычисляемое значение с sum
:
const sumDescription = computed('sumDescription', () => { return `sum(${number.value}, ${number2.value}) = ${sum.value}` }) // run computed effect sumDescription console.log(sumDescription.value) // sum(2, 2) = 4 number.value = 1 // run computed effect sum // run computed effect sumDescription // run computed effect sumDescription console.log(sumDescription.value) // sum(1, 2) = 3
Подожди секунду! Почему есть два журнала sumDescription
? Ответ: один из них для number
, а другой для sum
. Ну, это плохо для производительности! Что мы можем сделать, чтобы предотвратить эту проблему?
Решить проблему с производительностью
Чтобы решить эту проблему, нам нужно определить подходящее время для обновления значений зависимостей цели. Мы уже знаем, когда это происходит: когда нам нужно прочитать значение, верно?!
Для этого нам нужно добавить к нашим эффектам метод scheduler
. Этот планировщик должен указать, когда мы должны выполнить метод запуска нашего эффекта. В trigger
мы будем выполнять scheduler
вместо метода run
любого эффекта.
Эффект
Инициируйте эффект с помощью fn
и scheduler
.
class Effect { constructor(fn, scheduler) { this.fn = fn this.scheduler = scheduler } run() { activeEffect = this const result = this.fn() activeEffect = undefined return result } } function effect(fn, scheduler) { const _effect = new Effect(fn, scheduler) _effect.run() return _effect }
Курок
Вместо run
следует выполнить scheduler
.
function trigger(target) { const effects = [...target.dep] for (const effect of effects) { if (effect.scheduler) { effect.scheduler() } else { effect.run() } } }
Вычислено
Наши изменения в вычислении просты. Мы добавили новый атрибут _dirty
, чтобы отслеживать, является ли вычисленное значение грязным, указывая, что его необходимо обновить, когда мы читаем вычисленное значение.
После того, как мы прочитаем новое значение и обновим _cached_value
, нам не нужно снова выполнять эффект; мы можем просто прочитать из _cached_value
.
class Computed { constructor(name, getter) { this.dep = new Set() this._cached_value = undefined this._dirty = true this._name = name this.effect = new Effect(getter, () => { if (!this._dirty) { this._dirty = true trigger(this) } }) } get value() { track(this) if (this._dirty) { this._dirty = false console.log(`run computed effect ${this._name}`) this._cached_value = this.effect.run() } return this._cached_value } }
Я должен упомянуть, что мы используем конструктор Effect
вместо функции effect
, потому что функция эффекта выполнит getter
, а это не то, что нам нужно. Мы хотим запускать функцию getter
только тогда, когда нам нужно прочитать вычисленное значение.
Тест
А теперь нам нужно протестировать наши новые реализации.
const sumDescription = computed('sumDescription', () => { return `sum(${number.value}, ${number2.value}) = ${sum.value}` }) console.log(sumDescription.value) // run computed effect sumDescription // sum(2, 2) = 4 number.value = 1 console.log(sumDescription.value) // run computed effect sum // run computed effect sumDescription // sum(1, 2) = 3
Как видите, есть только один журнал sumDescription
, потому что обработчик эффектов будет выполняться только тогда, когда нам это нужно!
Заключение
И вот мы здесь! Мы сделали! Мы внедрили Эффект, триггер, отслеживание, ссылку и вычисление. Функциональные возможности реактивный и вычисленный с помощью установщика остаются. Я оставлю это вам, чтобы реализовать их самостоятельно. Я дам вам несколько советов для реактивной части: вы можете использовать Proxy и Reflect.
💡 Совет. Если вы обнаружите, что используете эти функции в нескольких проектах, используйте Bit, чтобы поделиться ими и использовать их повторно. Таким образом, у вас будет независимое управление версиями, тесты и документация для них, что упростит понимание и использование вашего кода другими людьми. Больше не нужно копировать/вставлять повторяющийся код из репозиториев.
Узнать больше:
Надеюсь, тебе понравится!
Создавайте компонуемые приложения Vue с повторно используемыми компонентами, как Lego.
Bit – это цепочка инструментов с открытым исходным кодом для разработки компонуемого программного обеспечения.
С помощью Bit вы можете разрабатывать любую часть программного обеспечения — современное веб-приложение, компонент пользовательского интерфейса, серверную службу или сценарий CLI — как независимую, повторно используемую и компонуемую единицу программного обеспечения. Совместно используйте любой компонент в своих приложениях, чтобы упростить совместную работу и ускорить сборку.
Присоединяйтесь к более чем 100 000 разработчиков, которые вместе создают компонуемое программное обеспечение.
Начните с этих руководств: