Учебник по реализации реактивной системы 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 разработчиков, которые вместе создают компонуемое программное обеспечение.

Начните с этих руководств:

→ Микро-Фронтенды: Видео // Гид

→ Кодшеринг: Видео // Гид

→ Модернизация: Видео // Путеводитель

→ Монорепозиторий: Видео // Путеводитель

→ Микросервисы: Видео // Путеводитель

→ Дизайн-система: Видео // Путеводитель

Узнать больше: