Углубленный анализ принципа сбора зависимостей Vue

Vue.js внешний фреймворк

Во-первых, давайте поговорим о режиме наблюдателя.

Паттерн Observer — это реализация一对多Шаблон поведенческого проектирования для реляционной развязки. В основном это включает в себя две роли: цель наблюдения и наблюдатель. Как показано на рисунке:

Его особенности:观察者Чтобы подписаться напрямую观察目标,观察目标После уведомления,观察者для обработки (это также观察者模式отличный от发布/订阅模式самая большая разница)

объяснять:В некоторых местах говорится, что шаблон наблюдателя и发布/订阅模式одинаковые, но не совсем одинаковые.发布/订阅режим, его способность развязки на один шаг ближе,发布者Просто сделайте хорошую работу по выпуску новостей, и не волнуйтесь, есть новости или нет.订阅者подписка. Режим наблюдателя требует, чтобы обе стороны существовали одновременно.

Шаблон наблюдателя, реализованный следующим образом:

// 观察者集合
class ObserverList {
    constructor() {
        this.list = [];
    }
    add(obj) {
        this.list.push(obj);
    }
    removeAt(index) {
        this.list.splice(index, 1);
    }
    count() {
        return this.list.length;
    }
    get(index) {
        if (index < 0 || index >= this.count()) {
            return;
        }
        return this.list[index];
    }
    indexOf(obj, start = 0) {
        let pos = start;
        while (pos < this.count()) {
            if (this.list[pos] === obj) {
                return pos;
            }
            pos++;
        }
        return -1;
    }
}
// 观察者类
class Observer {
    constructor(fn) {
        this.update = fn;
    }
}
// 观察目标类
class Subject {
    constructor() {
        this.observers = new ObserverList(); 
    }
    addObserver(observer) {
        this.observers.add(observer);
    }
    removeObserver(observer) {
        this.observers.removeAt(
            this.observers.indexOf(observer)
        );
    }
    notify(context) {
        const count = this.observers.count();
        for (let i = 0; i < count; ++i) {
            this.observers.get(i).update(context);
        }
    }
}

Теперь предположим, что нам нужно напечатать последнее значение A при изменении данных A, приведенный выше код реализован следующим образом:

const observer = new Observer((newval) => {
    console.log(`A的最新值是${newval}`);
})
const subject = new Subject();
subject.addObserver(observer);
// 现在,做出A最新值改变的通知
> subject.notify('Hello, world');
// 控制台输出:
< 'Hello, world'


2. Коллекция зависимостей Vue и Vue

~~Vue — это фреймворк для реализации data-driven представлений~~ (ерунда, все это знают, давайте поговорим о ключевом моменте) Все мы знаем, что Vue может обновлять представление при изменении данных, да и другие места, использующие эти данные тоже Изменения будут синхронизированы, более того, данные должны зависеть от вида и других мест, где используются данные, изменятся. так,Vue должен знать, используются ли данные, техника реализации этого механизма называется依赖收集Согласно введению официальной документации Vue, его принцип показан на следующем рисунке:

- Каждый экземпляр компонента имеет соответствующийwatcherЭкземпляры — процесс рендеринга компонентов, записи свойств в виде зависимостей — когда мы манипулируем частью данных, зависимостьsetterпозвонят, чтобы сообщитьwatcherпересчитать, в результате чего связанные с ним компоненты будут обновлены

Итак, теперь возникает вопрос: ~~ Какая технология экскаватора сильнее,... ~~ Если мы сейчас используем 3 данных A, B, C в шаблоне, то как мы можем обновить вид при изменении A, B и C? Шерстяная ткань?Для этого необходимо рассмотреть следующие два вопроса:1. Как узнать, какие данные используются в шаблоне? 2. Данные изменились, как мы можем сказатьrender()функция?

Естественно задуматься о том, есть ли шанс выполнить такой процесс, а именно: 1. Поскольку для рендеринга шаблона нужно использовать определенные данные, к этим данным нужно обращаться, так что пока геттер перехвачен, есть шанс сделать Обработку 2. При изменении значения тоже есть сеттеры для перехвата, тогда перехватывающий сеттер может и сделать следующее действие.

Итак, в геттере мы собираем зависимости (так называемые зависимости — это данные, от которых должен зависеть этот компонент), и когда зависимые данные установлены, сеттер может получить это уведомление, тем самым сообщаяrender()функция пересчета.


3. Зависимость коллекции и узоры наблюдателей

Мы обнаружим, что приведенный выше сценарий сбора зависимостей vue — это ровно один一对多образом (одни данные изменены, несколько мест, которые используют эти данные, должны иметь возможность их обрабатывать) и,Если зависимые данные изменяются, это должно быть обработано,так观察者模式Естественно подходит для решения проблемы сбора зависимостей. Итак, в коллекции зависимостей Vue: кто такие наблюдатели? Кто является объектом наблюдения? Судя по всему: - зависимые данные есть观察目标- Представления, вычисляемые свойства, слушатели это观察者

Соответствует коду реализации режима наблюдателя в начале статьи, сделатьnotifyдействие может бытьsetterсделать, сделатьaddObserver()действия, вы можетеgetterосуществляется в.


В-четвертых, проанализируйте коллекцию зависимостей Vue из исходного кода.

Давайте начнем наше путешествие по анализу исходного кода. Основное чтение здесь — это ранняя версия коммита Vue 2. Исходный код относительно прост и подходит для понимания сути.

1. Роль

Коллекция зависимостей реализована в исходном коде Vue, и реализованы три класса:Dep: играть观察目标роль, у каждого данных будетDepЭкземпляр класса, он имеет внутри очередь сабов, саб означает подписчиков, и сохраняет данные, которые зависят от этих данных.观察者, когда эти данные изменятся, вызовитеdep.notify()Уведомить наблюдателей -Watcher: играть观察者роль, поведение观察者函数обработка упаковки. Такие какrender()функция, будет заключена вWatcherПример -Observer:Вспомогательный可观测类, массив/объект можно преобразовать в可观测数据

2. Все данныеDepэкземпляр класса

Dep类Экземпляры прикрепляются к каждым данным и используются для управления зависимыми от данныхWatcherэкземпляр класса

let uid = 0; 
class Dep {
    static target = null;  // 巧妙的设计!
    constructor() {
        this.id = uid++;
        this.subs = [];
    }
    addSub(sub) {
        this.subs.push(sub);
    }
    removeSub(sub) {
        this.subs.$remove(sub);
    }
    depend() {
        Dep.target.addDep(this);
    }
    notify() {
        const subs = this.subs.slice();
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }
}

Поскольку JavaScript является однопоточной моделью, хотя существует несколько观察者函数, но одновременно будет только один观察者函数выполняется, то тот, который выполняется в данный момент观察者函数, соответствующийWatcherЭкземпляры будут назначены переменным, таким как Dep.target, чтобы вы могли узнать, кто является текущим наблюдателем, посетив Dep.target. в последующем依赖收集на работе,getterпозвонюdep.depend(),а такжеsetterпозвонюdep.notify()

3. Настройте наблюдение за данными

Выше мы говорим, что каждые данные будут иметь одинDep类пример, что это значит? Прежде чем объяснять наблюдение за данными, давайте на конкретном примере покажем изменения до и после обработки объекта, показанного ниже (т.options.data):

{
    a: 1,
    b: [2, 3, 4],
    c: {
        d: 5
    }
}

После настройки наблюдения за данными это будет выглядеть так:

{
    __ob__, // Observer类的实例,里面保存着Dep实例__ob__.dep => dep(uid:0)
    a: 1,   // 在闭包里存在dep(uid:1)
    b: [2, 3, 4], // 在闭包里存在着dep(uid:2),还有b.__ob__.dep => dep(uid:4)
    c: {
        __ob__, // Observer类的实例,里面保存着Dep实例__ob__.dep => dep(uid:5)
        d: 5 // 在闭包里存在着dep(uid:6)
    }
}

Мы найдем, что новые символыObserver类Выходите на сцену, чтобы сказать этот класс Observer, он должен быть из生产每个组件изComponent类конструктора, т.Component类В конструкторе будет выполняться ряд действий перед созданием экземпляра компонента, что связано с依赖收集Соответствующий исходный код выглядит следующим образом:

this._ob = observe(options.data)
    this._watchers = []
    this._watcher = new Watcher(this, render, this._update)
    this._update(this._watcher.value)

Ты это видел?observe(options.data),какие? Нет, не так хорошоObserver? как это в нижнем регистреobserve? ~~ Боюсь, это не тот предмет, который я купил в конце ночи? ~ Не волнуйся, давайте сначала посмотримobserveЧто делает функция:

function observe (value, vm) {
    if (!value || typeof value !== 'object') {
        return
    }
    var ob
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
    } else if (shouldConvert && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) {
        ob = new Observer(value)
    }
    if (ob && vm) {
        ob.addVm(vm)
    }
    return ob
}

В итоге:только для объектов/массивовэкземпляр одинObserverЭкземпляр класса, и он будет создан только один раз и только тогда, когда данные можно настроить.Observerэкземпляр класса. Так,ObserverЧто делает класс? И посмотрите на следующий исходный код:

class Observer {
    constructor(value) {
        this.value = value
        this.dep = new Dep()
        def(value, '__ob__', this)
        if (isArray(value)) {
            var augment = hasProto
              ? protoAugment
              : copyAugment
            augment(value, arrayMethods, arrayKeys)
            this.observeArray(value)
        } else {
            this.walk(value)
        }
    }
    walk(obj) {
        var keys = Object.keys(obj)
        for (var i = 0, l = keys.length; i < l; i++) {
            this.convert(keys[i], obj[keys[i]])
        }
    }
    observeArray(items) {
        // 对数组每个元素进行处理
        // 主要是处理数组元素中还有数组的情况
        for (var i = 0, l = items.length; i < l; i++) {
            observe(items[i])
        }
    }
    convert(key, val) {
        defineReactive(this.value, key, val)
    }
    addVm(vm) {
        (this.vms || (this.vms = [])).push(vm)
    }
    removeVm(vm) {
        this.vms.$remove(vm)
    }
}

Подводя итог, это: - будетObserver类Экземпляры монтируются на__ob__На свойстве он обеспечивает использование последующих данных наблюдения и позволяет избежать повторного создания экземпляров. Затем создайте экземплярDepэкземпляр класса и будет对象/数组Сохранено как атрибут значения — если значение является объектом, выполнитьwalk()Обработка, обход объекта и превращение каждого элемента данных в наблюдаемые данные (вызовdefineReactiveобработка метода) - если значение является массивом, выполнитьobserveArray()процедура, рекурсивно вызываемая для элементов массиваobserve(), чтобы иметь возможность обрабатывать случай элементов или массивов

4. Как наблюдать за массивом?

Доступ к свойствам объекта, его значениям и операциям присваивания можно получить с помощьюObject.defineProperty()успешно перехвачен, ноObject.defineProperty()При работе с массивами есть некоторые проблемы, давайте рассмотрим пример, чтобы понять:

const data = {
    arr: [1, 2, 3]
}

function defineReactive(obj, key, val) {
    const property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
        return;
    }
    const getter = property && property.get;
    const setter = property && property.set;
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            console.log('取值过程被拦截了');
            const value = getter ? getter.call(obj) : val;
            return value;
        },
        set(newval) {
            console.log(`新的值是${newval}`)
            if (setter) {
                setter.call(obj, newval);
            } else {
                val = newval;
            }
        }
    })
}

defineReactive(data, 'arr', data.arr);

Затем мы запускаем набор тестов со следующими результатами:

data.arr; // 取值过程被拦截了
data.arr[0] = 1;  // 取值过程被拦截了
data.arr.push(4); // 取值过程被拦截了
data.arr.pop(); // 取值过程被拦截了
data.arr.shift(); // 取值过程被拦截了
data.arr.unshift(5); // 取值过程被拦截了
data.arr.splice(0, 1); // 取值过程被拦截了
data.arr.sort((a, b) => a - b); // 取值过程被拦截了
data.arr.reverse(); // 取值过程被拦截了
data.arr = [4, 5, 6] // 新的值是4,5,6

Видно, что, кроме переназначения массива в обр, других операций не будет.setterобнаружено. Таким образом, чтобы обнаружить операцию изменения массива, когда входящий элемент данных является массивом, Vue выполнит следующую обработку:

var augment = hasProto
  ? protoAugment
  : copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)

То есть сначала над массивом выполняется операция расширения.Эта операция улучшения фактически состоит в том, чтобы определить ряд методов операции в цепочке прототипов массива, чтобы реализовать обнаружение изменений массива, то есть определить набор методов-прототипов вarr.__proto__на указанный объект-прототип, если браузер его не поддерживает__proto__, затем напрямую смонтируйте его на самом объекте массива) и, наконец, выполните операцию наблюдения за элементом массива.Итак, как операция улучшения обнаруживает изменения в массиве?, то нам нужно использовать идею АОП, то есть на основе сохранения исходной операции имплантировать код нашей конкретной операции. Пример выглядит следующим образом:

const arrayMethods = Object.create(Array.prototype); 
// 形成:arrayMethods.__proto__ -> Array.prototype
const originalPush = arrayMethods.push;
Object.defineProperty(arrayMethods, 'push', {
    configurable: true,
    enumerable: false,
    writable: true,
    value(...args) {
        const result = originalPush.apply(this, args);
        console.log('对数组进行了push操作,加入了值:', args);
        return result;
    }
})
data.arr.__proto__ = arrayMethods
data.arr.push([5, 6], 7) // 对数组进行了push操作,加入了值:[5, 6], 7

Следовательно, пока каждый метод операции с массивом обрабатывается таким образом, у нас есть способ уведомить наблюдателя об изменении массива. Конкретная реализация Vue выглядит следующим образом:

;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  var original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    var i = arguments.length
    var args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    var result = original.apply(this, args)
    var ob = this.__ob__
    var inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    ob.dep.notify()
    return result
  })
})

Идея все та же: - сохранить исходную операцию массива -push,unshift,spliceЭти методы принесут новые элементы данных, и у нас есть способ узнать новые элементы данных (то есть входящие параметры) — тогда новые элементы также необходимо настроить как наблюдаемые данные. Таким образом, последующие изменения данных могут быть обработанный. Итак, нам нужно вызвать вновь добавленный элементobserverна экземпляреobserveArrayМетод выполняет процесс наблюдения — поскольку массив изменился, его нужно уведомить观察者, так чтоob.dep.notify()для массивов观察者наблюдатели для уведомления

5. Наблюдатель

Watcherроль观察者, он заботится о данных, может получать уведомления об изменении данных и обрабатывать их. Компонент может иметь несколькоWatcher类пример,Watcher类Упаковка观察者函数,а также观察者函数Используйте данные.观察者函数проходить черезWatcherОн упакован следующим образом: - Рендеринг шаблона:this._watcher = new Watcher(this, render, this._update)- Расчетные свойства:

computed: {
    name() {
        return `${this.firstName} ${this.lastName}`;
    }
}
/*
会形成
new Watcher(this, function name() {
    return `${this.firstName} ${this.lastName}`
}, callback);
*/

существуетWatcherВещи, сделанные в классе, можно резюмировать следующим образом: 1. Входящие组件实例,观察者函数,回调函数,选项, а затем мы сначала объясним 4 переменные:deps,depIds,newDeps,newDepIds, они функционируют следующим образом:deps: кэшировать предыдущий раунд выполнения观察者函数Примеры использования dep -depIds: Хэш-таблица для быстрого поиска -newDeps: сохранить текущий раунд выполнения观察者函数Примеры использования dep -newDepIds: Хэш-таблица для быстрого поиска

2. Выполните первоначальную оценку.Во время первоначальной оценки он вызоветwatcher.get()Способ 3.watcher.get()Будет выполнена следующая обработка: первичная подготовка, вызов观察者函数4. При начальных приготовлениях токWatcherназначение экземпляраDep.target, очистить массивnewDeps,newDepIds5. Выполнить观察者函数, Расчет. Поскольку фаза наблюдения за данными выполняетсяdefineReactive(), поэтому данные, используемые в процессе расчета, будут доступны, тем самым инициируя данныеgetter, тем самым выполняяwatcher.addDep()метод, спец.数据Записать как зависимость 6. Выполнить для каждого данныхwatcher.addDep(dep)После этого данные, соответствующиеdepесли вnewDepsне существует, он будет добавлен кnewDeps, это связано с тем, что данные процесса вычислений могут использоваться несколько раз, но одни и те же зависимости могут быть собраны только один раз. и если вdepsНе существует, что указывает на то, что в предыдущем раунде вычислений текущий наблюдатель не полагался на определенные данные, соответствующие данныеdep.subsВ нем нет текущего наблюдателя, поэтому добавляем текущего наблюдателя в данныеdep.subs7. Провести уборку после мероприятия, первый релизDep.target, затем взятьnewDepsа такжеdepsПроизводится сравнение с последующей обработкой:newDepsне существует вdepsДанные, существующие в кэше, указывают на то, что это кэшированные данные с истекшим сроком действия. Соответственно, соответствующие из данныхdep.subsудалить текущего наблюдателя - будетnewDepsназначатьdeps, указывающее, что результаты вычислений этого раунда кэшируются, поэтому, если следующий раунд вычислений опирается на те же данные, их не нужно собирать снова.

8. Когда某个数据При обновлении из-за перехвата сеттера данные будутdep.subsНаблюдатели в этой очереди наблюдателей получают уведомление о выполненииwatcher.update()метод, в то время какupdate()метод повторяет процесс оценки (т. е. шаги 3-7), так что观察者函数пересчитать иrender()Результат этого пересчета функции наблюдателя заставляет представление синхронизировать последние данные

6, Определить Реальный

Мы все знаем, что Vue реализует перехват данных, используяObject.defineProperty(), при использованииObject.defineProperty()Операции по перехвату данных инкапсулированы вdefineReactiveвнутри. Далее проанализируемdefineReactive()Исходный код:

function defineReactive (obj, key, val) {
    var dep = new Dep()
    var property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
        return
    }
    var getter = property && property.get
    var setter = property && property.set

    var childOb = observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            var value = getter ? getter.call(obj) : val
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                }
                if (isArray(value)) {
                    for (var e, i = 0, l = value.length; i < l; i++) {
                        e = value[i]
                        e && e.__ob__ && e.__ob__.dep.depend()
                    }
                }
            }
            return value
        },
        set: function reactiveSetter (newVal) {
            var value = getter ? getter.call(obj) : val
            if (newVal === value) {
                return
            }
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            childOb = observe(newVal)
            dep.notify()
        }
    })
}

1. Магия замыканий: в приведенном выше кодеObject.defineProperty()внутреннийget/setметод относительноvar dep = new Dep()Образуется замыкание, которое ловко сохраняетdepПример 2.getterВот коллекция зависимостей. Если функция-наблюдатель обращается к определенным данным, мы можем подумать, что функция-наблюдатель зависит от этих данных, поэтому приведите конкретный пример:data.a, используется в:

<template>
    <div>{{a}}</div>
</template>
computed: {
    newValue() {
        return this.a + 1;
    }
}

Затем, после компиляции шаблона, он сформирует AST и выполнитrender()Во время выполнения функции будет запущен data.agetter, и процесс惰性收集из (например,newValueНесмотря на то, что a используется, если он не вызывается для выполнения, он не сработает.getter, он не будет добавлен вdata.aизdep.subs) Теперь предположим, что шаблон становится таким:

<template>
    <div>I am {{a}},plus 1 is {{newValue}}</div>
</template>

Тогда видно, что есть два соответствующих观察者函数: вычисляемое свойствоnewValueа такжеrender()функции, они будут обернуты как два наблюдателя. в исполненииrender()Во время рендеринга функции доступdata.a, так чтоdata.aизdep.subsприсоединилсяrender@watcherК вычисляемому свойству newValue снова обращаются, и к вычисляемому свойству обращаютсяdata.a, так чтоdata.aизdep.subsприсоединилсяnewValue@watcher. такdata.aизdep.subsтам[render@watcher, newValue@watcher] Почему доступ к определенным данным делает данные доступнымиdeps.subsКак насчет добавления наблюдателя к нему? Это связано с тем, что при посещенииgetterРаньше он входил в контекст наблюдателя, поэтому гарантировано одно:Наблюдатель экземпляра класса Watcher готов и был вызванwatcher.get(),Dep.targetцененИтак, мы видимgetterКоллекция зависимостей написана наdep.depend(), и никакие параметры не передаются, это потому, что нам нужно только поставитьDep.targetПрисоединяйтесь к токуdep.subsПросто хорошо. Но мы также обнаружили, чтоDep.prototype.depend()Реализация:

depend() {
    Dep.target.addDep(this);
}

Зачемdepend()когда не напрямуюDep.targetПрисоединяйсяdep.subs, вместо вызоваDep.target.addDepШерстяная ткань? Это потому, что мы не можем бездумно подключить текущего наблюдателя кdep.subs, мы должны гарантироватьdep.subsкаждый вwatcherвсе уникальны.Dep.targetдаWatcher类экземпляр, звонитеdep.depend()эквивалентно вызовуwatcher.addDepМетод, поэтому давайте сделаем еще одну вещь, чтобы сделать это:

Watcher.prototype.addDep = function (dep) {
    var id = dep.id
    if (!this.newDepIds[id]) {
        this.newDepIds[id] = true
        this.newDeps.push(dep)
        if (!this.depIds[id]) {
            dep.addSub(this)
        }
    }
}

Подводя итог, это: судить, была ли собрана эта зависимость в этом раунде расчета, если она была собрана, она не будет собрана, а если не была собрана, она будет добавлена.newDeps. В то же время оценивается, есть ли какая-либо зависимость от кеша, и она больше не будет добавляться в кеш после того, как она была кеширована.dep.subsв.

3.setterПри изменении значения уведомлениеwatcherПересчитать. из-заsetterдоступ к закрытиюdep, так что вы можете получитьdep.subs, чтобы узнать, какие наблюдатели зависят от текущих данных, если их значение изменится, вызвавdep.notify(), пройтиdep.subsнаблюдатель в , выполнить каждыйwatcherизupdate()метод, чтобы каждый наблюдатель пересчитывал.

7. Анализ непонятных моментов

Возвращаясь к примеру в начале, давайте поговорим о примереoption.dataПосле наблюдения становится:

{
    __ob__, // dep(uid:0)
    a: 1, // dep(uid:1)
    b: [2, 3, 4], // dep(uid:2), b.__ob__.dep(uid:3)
    c: {
        __ob__, // dep(uid:4), c.__ob__.dep(uid:5)
        d: 5 // dep(uid:6)
    }
}

Мы не можем не задаться вопросом, почему数组а также对象, после настройки наблюдения за зависимостями будут созданы два экземпляраDep类А экземпляры? Это потому что:数组а также对象, являются данными ссылочного типа, для данных ссылочного типа есть две операции:改变引用а также改变内容, то есть:

data.b = [4, 5, 6]; // 改变引用
data.b.push(7); // 改变内容

Фактически,改变引用В этом случае мы упоминали ранееObject.defineProperty()Как уже упоминалось, предел может быть обнаружен, поэтому闭包внутреннийdepТакие зависимости можно собирать. а также改变内容, но не смог пройтиObject.defineProperty()Обнаружено, значит, операция мутации массива инкапсулирована, поэтому нужно зависнуть на массиве__ob__свойства, в__ob__садиться наdepэкземпляр для обработки改变内容условия, чтобы можно было сформировать ссылку для отслеживания.


3. Резюме

В заключение, коллекция Vue зависит от观察者Применение узора. Принцип резюмируется следующим образом:


1. Наблюдение за зависимостью конфигурации


2. Соберите зависимости


3. Изменение значения данных