Мастер от 0 до 1: двусторонняя привязка данных ядра Vue

Vue.js
Мастер от 0 до 1: двусторонняя привязка данных ядра Vue

предисловие

​ На вопрос о принципе двусторонней привязки данных Vue каждый может ляпнуть: Vue внутренне передаетObject.definePropertyСпособ перехвата атрибута метода, положитьdataЧтение и запись каждых данных в объекте преобразуется вgetter/setter, чтобы уведомить представление об обновлении при изменении данных. Хотя общий принцип изложен в одном предложении, его внутреннюю реализацию все же стоит исследовать, и в этой статье он будет проанализирован в простой для понимания форме.VueПроцесс реализации принципа внутренней двусторонней привязки. а потом согласноVueДвусторонняя привязка данных исходного кода реализована для дальнейшего закрепления и углубления понимания двусторонней привязки данных. Ниже приведен рендеринг двусторонней привязки данных, которую мы реализовали:

1.gif

Я много работал, чтобы написать в течение долгого времени. Если это полезно для вас, пожалуйста, помогите вручную поставить лайк и поощрить ~

Адрес гитхаба:GitHub.com/ревматизм123/…, который обобщает все статьи в блоге автора, если вам нравится или вдохновляет, пожалуйста, помогите дать звезду ~, что также является поощрением для автора.

1. Что такое двусторонняя привязка данных MVVM

MVVMДвусторонняя привязка данных в основном относится к следующему: изменения данных обновляют представление, а изменения представления обновляют данные, как показано на следующем рисунке:

2.png

который:

  • Когда содержимое поля ввода изменяется,DataДанные в синхронном изменении. которыйView => DataИзменение.
  • DataКогда данные в текстовом узле изменяются, содержимое текстового узла изменяется синхронно. которыйData => ViewИзменение.

в,Viewизменить обновлениеData, который можно реализовать с помощью мониторинга событий, поэтому в этой статье в основном обсуждается, какDataизменить обновлениеView.

Мы реализуем двустороннюю привязку данных, выполнив следующие 4 шага:

1. Реализуйте прослушивательObserver, используемый для захвата и мониторинга всех свойств и уведомления подписчиков об изменении свойств;

2. Реализуйте подписчикаDep, используется для сбора подписчиков, для слушателейObserverи подписчикиWatcherосуществлять единое управление;

3. Реализовать подписчикаWatcher, вы можете получить уведомление об изменении свойства и выполнить соответствующий метод для обновления представления;

4. Реализовать парсерCompile, который может анализировать соответствующие инструкции каждого узла и инициализировать данные шаблона и подписчиков.

Блок-схема вышеуказанных четырех шагов показана следующим образом:

3.png

Исходный код этого экземпляра выложен на github:GitHub.com/ревматизм123/….

Во-вторых, реализация слушателя Observer

слушательObserverРеализация в основном относится к тому, чтобы сделать объект данных «наблюдаемым», то есть каждый раз, когда данные читаются или записываются, мы можем воспринимать, что данные были прочитаны или данные были перезаписаны. Чтобы сделать данные «наблюдаемыми»,Vue 2.0используется в исходном кодеObject.defineProperty()перехватить каждый атрибут данныхsetter / getter,Object.definePropertyметод, как определено в MDN:

Метод Object.defineProperty() напрямую определяет новое свойство объекта или изменяет существующее свойство объекта и возвращает объект.

2.1, Object.DefineProperty () синтаксис

Object.definePropertyСинтаксис определен в MDN следующим образом:

Object.defineProperty(obj, prop, descriptor)

(1) Параметры

  • obj

    Объект, для которого определяются свойства.

  • prop

    Имя свойства, которое необходимо определить или изменить.

  • descriptor

    Дескриптор свойства, который необходимо определить или изменить.

(2) Возвращаемое значение

Объект передан в функцию.

(3) Дескриптор атрибута

Object.defineProperty()Определите атрибуты для объектов, которые разделены на дескрипторы данных и дескрипторы доступа, и эти две формы нельзя смешивать.

И дескрипторы данных, и дескрипторы доступа имеют следующие необязательные ключи:

  • configurable

тогда и только тогда, когда свойствоconfigurableдляtrueКогда дескриптор атрибута может быть изменен, атрибут также может быть удален из соответствующего объекта.По умолчанию ложный.

  • enumerable

тогда и только тогда, когда свойствоenumerableдляtrue, свойство может появиться в свойстве перечисления объекта.Значение по умолчанию — ложь.

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

  • value

Значение, соответствующее этому свойству. Может быть любым допустимым значением JavaScript (число, объект, функция и т. д.).По умолчанию не определено.

  • writable

тогда и только тогда, когда свойствоwritableдляtrueчас,valueможет быть изменен оператором присваивания.Значение по умолчанию — ложь.

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

  • get

данное свойствоgetterметод, если нетgetterтогдаundefined. При доступе к свойству метод будет выполнен, при выполнении метода никакие параметры не передаются, но он будет передан вthisобъект (из-за наследования, здесьthisне обязательно объект, определяющий свойство). По умолчаниюundefined.

  • set

данное свойствоsetterметод, если нетsetterтогдаundefined. Этот метод срабатывает при изменении значения свойства. Метод примет единственный параметр, новое значение параметра для свойства. По умолчаниюundefined.

2.2, реализация слушателя Observer

(1) Объект буквального определения

Во-первых, давайте посмотрим.Предположим, мы определяем объект следующим образом:

let person = {
    name:'tom',
    age:15
}

мы можем пройтиperson.nameа такжеperson.ageпрочитайте и напишите это прямоpersonсоответствующее значение свойства, однако, когда этоpersonсвойства считываются или изменяются без нашего ведома. Итак, как нам определить объект, который мы можем воспринимать, когда его свойства считываются и записываются?

(2) Object.defineProperty() определяет объект

Предположим, мы проходимObject.defineProperty()для определения объекта:

let val = 'tom'
let person = {}
Object.defineProperty(person,'name',{
    get(){
        console.log('name属性被读取了...');
        return val;
    },
    set(newVal){
        console.log('name属性被修改了...');
        val = newVal;
    }
})

мы проходимobject.defineProperty()способ датьpersonизnameсвойство определяетget()а такжеset()Перехват, который запускается всякий раз, когда свойство читается или записываетсяget()а такжеset(), чтобы мы могли воспринимать, когда свойства объекта считываются и записываются. График результатов теста выглядит следующим образом:

3_2.png

(3) Методы улучшения

Методом шага (2),personОбъект данных уже «наблюдаем» и соответствует нашим потребностям. Однако, если есть много атрибутов объекта данных, мы устанавливаем атрибуты один за другим, и код будет очень избыточным, поэтому мы инкапсулируем следующее, чтобы сделать все атрибуты объекта данных наблюдаемыми:

/**
  * 循环遍历数据对象的每个属性
  */
function observable(obj) {
    if (!obj || typeof obj !== 'object') {
        return;
    }
    let keys = Object.keys(obj);
    keys.forEach((key) => {
        defineReactive(obj, key, obj[key])
    })
    return obj;
}
/**
 * 将对象的属性用 Object.defineProperty() 进行设置
 */
function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`${key}属性被读取了...`);
            return val;
        },
        set(newVal) {
            console.log(`${key}属性被修改了...`);
            val = newVal;
        }
    })
}

С помощью описанной выше инкапсуляции метода мы можем напрямую определитьperson:

let person = observable({
    name: 'tom',
    age: 15
});

определяется такpersonОба свойства являются «наблюдаемыми».

3. Внедрение абонентской базы

3.1, шаблон проектирования "публикация-подписка"

Режим публикации-подписки также называется режимом наблюдателя, который определяет зависимость между объектами «один ко многим».При изменении состояния объекта все зависящие от него объекты будут уведомлены.

(1) Преимущества модели публикации-подписки:

  • Модель публикации-подписки широко используется в асинхронном программировании, это альтернатива передаче callback-функций, например, мы можем подписаться на такие события, как error и succ запросов ajax. Используя шаблон публикации-подписки в асинхронном программировании, нам не нужно уделять слишком много внимания внутреннему состоянию объекта во время асинхронной операции, а нужно только подписаться на интересующие события.
  • Модель публикации-подписки может заменить жестко запрограммированный механизм уведомления между объектами, и одному объекту больше не нужно явно вызывать интерфейс другого объекта. Паттерн публикации-подписки позволяет слабо связать два объекта, хотя подробности друг о друге неизвестны, но это не влияет на их связь друг с другом. При появлении нового подписчика код издателя не нуждается в каких-либо изменениях, аналогично, когда нужно изменить издателя, это не повлияет на предыдущих подписчиков. Вы можете свободно изменять их до тех пор, пока ранее согласованные названия событий не изменились.

(2) Жизненный пример модели публикации-подписки

Наш пример офиса продаж, чтобы проиллюстрировать публикацию - подписаться с шаблона:

Сяо Мин недавно влюбился в дом.После того, как он пришел в офис продаж, ему сказали, что дома в здании уже распроданы. К счастью, менеджер по продажам сообщил Сяо Мину, что скоро будут поздние запуски, и разработчик проходит соответствующие процедуры, и после их завершения вы можете купить его.

Но когда именно, пока никто не знает. Поэтому Сяо Мин записал номер телефона офиса продаж и будет звонить каждый день, чтобы спросить, не пора ли покупать. Помимо Xiao Ming, Xiaohong, Xiaoqiang и Xiaolong также ежедневно консультируются по этому вопросу с отделом продаж. Через неделю менеджер по продажам решил уволиться, потому что устал отвечать на 1000 звонков с одним и тем же содержанием каждый день.

​ Конечно, такой дурацкой компании по продажам в реальности не существует.На самом деле история такова: Перед уходом Сяо Мин оставил свой номер телефона в офисе продаж. Менеджер по продажам пообещал ему, что он отправит сообщение, чтобы уведомить Сяо Мина, как только новая недвижимость будет запущена. То же самое верно для Xiaohong, Xiaoqiang и Xiaolong. Все их номера телефонов записаны в реестре офиса продаж. Когда новый объект запускается, продавец открывает список, проходит по указанным выше номерам телефонов и отправляет текстовое сообщение. в свою очередь, чтобы сообщить им. Это реальный пример шаблона публикации-подписки.

3.2, Реализация абонентской службы

После завершения «наблюдения» за данными, то есть мы знаем, когда данные читаются или записываются, мы можем уведомить те представления, которые зависят от данных, для обновления, когда данные читаются или записываются.Для удобства нам нужно чтобы сначала собрать все зависимости, и как только данные изменятся, мы единообразно уведомим об обновлении. По сути, это упомянутый в предыдущем разделе режим «публикация-подписчик», данные меняются на «издатель», а зависимый объект — «подписчик».

Теперь нам нужно создать контейнер коллекции зависимостей, который является подписчиком сообщений.Dep, чтобы вместить всех "подписчиков". ПодписчикDepОн в основном отвечает за сбор подписчиков, а затем за выполнение функции обновления, соответствующей подписчикам, при изменении данных.

Создать подписчика сообщенийDep:

function Dep () {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
Dep.target = null;

С подписчиками мы потомdefineReactiveФункция трансформируется и в нее вживляется подписчик:

defineReactive: function(data, key, val) {
	var dep = new Dep();
	Object.defineProperty(data, key, {
		enumerable: true,
		configurable: true,
		get: function getter () {
			if (Dep.target) {
				dep.addSub(Dep.target);
			}
			return val;
		},
		set: function setter (newVal) {
			if (newVal === val) {
				return;
			}
			val = newVal;
			dep.notify();
		}
	});
}

С точки зрения кода мы проектируем абонентаDepКласс, который определяет некоторые свойства и методы, важно отметить, что он имеет статическое свойствоDep.target, который является уникальным в миреWatcher, потому что одновременно может быть только один глобальныйWatcherрассчитывается, помимо собственных свойствsubsСлишком Watcherмассив .

4. Реализация наблюдателя за подписчиками

подписчикWatcherВы должны добавить себя на абонент во время инициализацииDep, как его добавить? мы уже знаем слушателяObserverИменно в функции get выполняется операция добавления подписчиков, поэтому нам нужно добавить только подписчиковWatcherАктивировать соответствующийgetФункция для выполнения операции добавления подписчиков, то как вызватьgetФункция не может быть проще, пока получено соответствующее значение атрибута, она может быть запущена Основная причина в том, что мы используемObject.defineProperty( )мониторинг данных. Здесь нужно остановиться еще на одном подробном моменте, нам нужно толькоWatcherПодписчики должны быть добавлены во время инициализации, поэтому требуется операция оценки, чтобы вы могли что-то сделать с подписчиком: вDep.targetКэшируйте подписчиков вверх и вниз, успешно добавляйте их, а затем удаляйте. подписчикWatcherРеализация выглядит следующим образом:

function Watcher(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    this.value = this.get();  // 将自己添加到订阅器的操作
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this; // 全局变量 订阅者 赋值
        var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null; // 全局变量 订阅者 释放
        return value;
    }
};

подписчикWatcherпроанализируйте, как показано ниже:

подписчикWatcherэто класс, который в своем конструкторе определяет некоторые свойства:

  • **vm:** объект экземпляра Vue;
  • **эксп:** даnodeузлаv-modelЗначения атрибутов, такие как директивы или атрибуты в интерполяционной нотации. Такие какv-model="name",expто естьname;
  • **кб:** даWatcherСвязанная функция обновления;

Когда мы переходим к созданию экземпляра рендераwatcherВремя, сначала введитеwatcherлогика конструктора, он выполнит своюthis.get()метод, введитеgetфункция, которая сначала выполнит:

Dep.target = this;  // 将自己赋值为全局的订阅者

на самом деле положитьDep.targetНазначить текущий рендерwatcher, а затем выполните:

let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数

В ходе этого процессаvmДоступ к данным выше фактически запускает объект данныхgetter.

за стоимость объектаgetterоба держат одинdep, после срабатыванияgetterбудет вызван, когдаdep.depend()метод также будет выполнятьсяthis.addSub(Dep.target), то есть текущийwatcherподписаться на этого держателя данныхdepизwatchers, цель состоит в том, чтобы уведомить, какие данные могут быть уведомлены при изменении последующих данных.watchersподготовиться к.

Это фактически завершило процесс сбора зависимостей. Так это здесь? На самом деле нет, после завершения сбора зависимостей нужно поставитьDep.targetВозврат к предыдущему состоянию, то есть:

Dep.target = null;  // 释放自己

а такжеupdate()Функция используется для вызова при изменении данныхWatcherЕго собственная функция обновления выполняет операцию обновления. пройти первымlet value = this.vm.data[this.exp];Получите последние данные, затем сравните их с предыдущимиget()Полученные старые данные сравниваются, и если они не совпадают, вызывается функция обновленияcbобновить.

Пока что простой подписчикWatcherДизайн завершен.

5. Реализация парсерной компиляции

5.1, Анализ кода ключевой логики Parser Compile

через слушателяObserverПодписчикDepи подписчикиWatcherНа самом деле реализован пример двусторонней привязки данных, но весь процесс не разобранdomУзел, но напрямую исправить узел для замены данных, поэтому далее необходимо реализовать парсерCompileсделать разбор и привязку. парсерCompileЭтапы реализации:

  • Разбирать директивы шаблона, заменять данные шаблона и инициализировать представления;
  • Привязать узел, соответствующий инструкции шаблона, к соответствующей функции обновления и инициализировать соответствующего подписчика;

Разберем ключевой код обработки инструкций в виде '{{variable}}' и пощупаем парсерCompileЛогика обработки кода ключа следующая:

compileText: function(node, exp) {
	var self = this;
	var initText = this.vm[exp]; // 获取属性值
	this.updateText(node, initText); // dom 更新节点文本值
    // 将这个指令初始化为一个订阅者,后续 exp 改变时,就会触发这个更新回调,从而更新视图
	new Watcher(this.vm, exp, function (value) { 
		self.updateText(node, value);
	});
}

5.2 Простая реализация экземпляра Vue

слушатель завершенияObserver, ПодписчикDep,подписчикWatcherи парсерCompileреализации, мы можем смоделировать инициализациюVueПример для проверки выполнимости вышеизложенной теории. Мы инициализируем один следующим кодомVueПример, исходный код этого примера выложен на github:GitHub.com/ревматизм123/…, вы можете клонировать git, если вам интересно:

<body>
    <div id="mvvm-app">
        <input v-model="title">
        <h2>{{title}}</h2>
        <button v-on:click="clickBtn">数据初始化</button>
    </div>
</body>
<script src="../dist/bundle.js"></script>
<script type="text/javascript">
    var vm = new MVVM({
        el: '#mvvm-app',
        data: {
            title: 'hello world'
        },

        methods: {
            clickBtn: function (e) {
                this.title = 'hello world';
            }
        },
    });
</script>

Запустите приведенный выше пример, диаграмма эффекта выглядит следующим образом, это то же самое, что и фактический эффект привязки данных Vue!

1.gif

6. Исходный код Vue — двусторонняя привязка данных

Главы со 2 по 6 выше, от слушателяObserver, ПодписчикDep,подписчикWatcherи парсерCompileреализация, завершает простойVueРеализация экземпляра привязки данных. В этой главе мы начинаем сVueСлушатель анализа исходного кодаObserver, ПодписчикDep,подписчикWatcherреализация, чтобы помочь вам понятьVueКак реализовать двустороннюю привязку данных в исходном коде.

6.1, реализация слушателя-наблюдателя

В этом разделе мы в основном познакомимся с реализацией слушателя Observer, ядром которого является использованиеObject.definePropertyдобавлено к даннымgetterА сеттеры предназначены для автоматического выполнения некоторой логики при доступе к данным и записи данных.

(1) состояние инициализации

существуетVueФаза инициализации,_initКогда метод выполнится, он выполнитсяinitState(vm)метод, который определен вsrc/core/instance/state.js середина.

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initStateМетод заключается в основном вprops,methods,data,computed а также wathcerи другие свойства были инициализированы. Здесь мы сосредоточимся на анализеdata, мы представим инициализацию других свойств в будущих статьях.

(2) данные инициализации

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

dataОсновной процесс инициализации также делает две вещи, одна из которых заключается в определенииdataФункция возвращает обход объекта черезproxyпоставить каждое значениеvm._data.xxxпредставленыvm.xxxна; другой должен позвонитьobserveспособ наблюдать за всемdataменять, ставитьdataОн также стал отзывчивым, и далее мы в основном представим наблюдение.

(3) наблюдать

observeФункция состоит в том, чтобы отслеживать изменения в данных, которые определены вsrc/core/observer/index.js середина:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

observeФункция метода заключается в добавлении данных типа объекта, отличного от VNode.Observer, если он был добавлен, вернуться напрямую, в противном случае создать экземплярObserverЭкземпляр объекта. Далее давайте посмотримObserverэффект.

(4) Наблюдатель

Observerэто класс, роль которого заключается в добавлении геттеров и сеттеров к свойствам объекта для зависимостей для сбора и отправки обновлений:

xport class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

ObserverЛогика конструктора очень проста, сначала создайте экземплярDepобъект,DepОбъекты, которые мы представим в разделе 2. Далее будетvalueДля вынесения суждений он будет вызываться для массивовobserveArrayметод, в противном случае вызовите простой объектwalkметод. можно увидетьobserveArrayэто пересечь массив и снова вызватьobserveметод иwalkМетод состоит в том, чтобы пройти вызов ключа объектаdefineReactiveметод, то давайте посмотрим, что делает этот метод.

(5) определитьреактивный

defineReactiveФункция состоит в том, чтобы определить отзывчивый объект и динамически добавить его к объекту.getterа такжеsetter, который определен вsrc/core/observer/index.js середина:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

defineReactiveФункция инициализируется в началеDepэкземпляр объекта, затем получитьobjДескриптор свойства , затем рекурсивный вызов дочерних объектовobserveметод, который гарантирует, что независимо отobjНасколько сложна структура, все ее подсвойства также могут стать реагирующими объектами, чтобы мы могли получить доступ или изменитьobjГлубоко вложенное свойство в , также может вызывать геттеры и сеттеры.

6.2. Внедрение абонентского отдела

ПодписчикDepэто всеgetterЯдро коллекции зависимостей, которое определено вsrc/core/observer/dep.js середина:

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

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

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null

DepЯвляетсяClass, который определяет некоторые свойства и методы, здесь следует отметить, что он имеет статическое свойствоtarget, который является уникальным в миреWatcher, что является очень умным решением, так как одновременно может быть только один глобальныйWatcherрассчитывается, помимо собственных свойствsubsСлишкомWatcherмассив .Depна самом деле правильноWatcherразновидность управления,DepвырватьсяWatcherСуществовать в одиночестве бессмысленно.

6.3. Реализация наблюдателя за подписчиками

подписчикWatcherНекоторые соответствующие реализации , которые определены вsrc/core/observer/watcher.js середина

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
   。。。。。。
}

Watcher ЯвляетсяClassВ его конструкторе и определяет некоторыеDepсвязанные свойства , где,this.deps а также this.newDeps Выражать Watcherпроведенный экземплярDepмассив экземпляров; в то время какthis.depIds а также this.newDepIdsПредставляяthis.deps а также this.newDeps из idЗадавать .

(1) Анализ процесса

Когда мы переходим к созданию экземпляра рендерераwatcher, сначала введитеwatcherлогика конструктора, которая затем выполнит своюthis.get()метод, введитеgetфункция, которая сначала выполняет:

pushTarget(this)

на самом деле положитьDep.targetНазначить текущий рендерwatcherИ протолкнуть стек (для восстановления). Затем выполняется снова:

value = this.getter.call(vm, vm)

В это время срабатывает объект данныхgetter.

стоимость каждого объектаgetterоба держат одинdep, после срабатыванияgetterбудет вызван, когдаdep.depend()метод также будет выполнятьсяDep.target.addDep(this).

Только что мы упомянули это времяDep.targetбыл назначен для рендерингаwatcher, затем выполните дляaddDepметод:

addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

В это время будут сделаны некоторые логические суждения (чтобы гарантировать, что одни и те же данные не будут добавляться несколько раз), а затем выполненыdep.addSub(this), то он будет выполнятьсяthis.subs.push(sub), то есть текущийwatcherподписаться на этого держателя данныхdepизsubs, цель состоит в том, чтобы уведомить, какие данные могут быть уведомлены при изменении последующих данных.subsподготовиться к. так вvm._render()В процессе все данные будут активированыgetter, чтобы процесс сбора зависимостей был фактически завершен.

Когда мы изменим данные ответа в компоненте, он вызоветsetterЛогика последнего звонкаwatcherсерединаupdateметод:

  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

здесь будетWatcherРазличные состояния , будут выполнять различную логику обновления.

6.4, Схема двусторонней привязки данных Vue

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

4.png

7. Резюме

Эта статья передает слушателюObserver, ПодписчикDep,подписчикWatcherи реализация парсера, который имитирует инициализациюVueПримеры, которые помогут вам понять основные принципы двусторонней привязки данных. Далее, изVueПредставлен исходный уровеньVueПроцесс реализации двусторонней привязки данных, понятьVueИсходный код реализован логично, что углубляет понимание двусторонней привязки данных. Я надеюсь, что эта статья будет полезна для вас.

Я много работал, чтобы написать в течение долгого времени. Если это полезно для вас, пожалуйста, помогите вручную поставить лайк и поощрить ~

Адрес github: github.com/fengshi123/…, где собраны все статьи в блоге автора.Если вам нравится или есть вдохновение, пожалуйста, помогите поставить звездочку ~, что также является поощрением для автора.

использованная литература

1. Принцип двусторонней привязки Vue и его реализация:Блог Woo Woo.cn на.com/Go to Buddha/Afraid/68…

2. Технология Vue показала:США ТБ Huang Yi.GitHub.IO/v UE-Anarias…