Студенты, вы хотите учитьсяVueКак насчет принципа реагирования на данные и отсутствия возможности начать? Вас когда-нибудь обескураживали сложные туториалы по исходному коду? Если вы, как и я, хотите углубиться в принципы после работы над проектом, поздравляю, вы попали по адресу. Эта серия статей начнется с чистогоVueИсходя из принципа отзывчивости, без вмешательства других факторов, это приводит каждого к реализации собственной адаптивной системы.
Дружеское напоминание: поскольку наш код будет изменяться в нескольких версиях, я надеюсь, что когда вы прочитаете статью, вы сможете ввести соответствующий код, который поможет вам понять.
адрес проекта:gitee
Адрес сериала:
0. Предисловие
Модели данных — это обычные объекты JavaScript. И когда вы их изменяете, представление обновляется.
использоватьVue, нам нужно только изменить данные (state), представление может быть соответствующим образом обновлено, что является адаптивной системой. Чтобы реализовать собственную реактивную систему, нам сначала нужно понять, что делать:
- Перехват данных: когда данные меняются, мы можем делать определенные вещи
- Коллекция зависимостей: нам нужно знать содержимое этих слоев представления (
DOM) зависит от того, какие данные (state) - Распространение обновлений: как уведомить иждивенцев об изменениях данных при изменении данных
DOM
Далее будем поэтапно реализовыватьИгрушкаОтзывчивая система
1. Перехват данных
Почти все статьи и руководства объясняются вVueОтзывчивые системы сначала расскажут о:VueиспользоватьObject.definePropertyдля захвата данных. Итак, давайте также начнем с захвата данных.劫持Это понятие немного сбивает с толку, не беда, прочитав следующий контент, вы обязательно все поймете.
Object.definePropertyИспользованиеMDNПроверьте это. Ниже мы будемobjопределитьaАтрибуты
const obj = {}
let val = 1
Object.defineProperty(obj, a, {
get() { // 下文中该方法统称为getter
console.log('get property a')
return val
},
set(newVal) { // 下文中该方法统称为setter
if (val === newVal) return
console.log(`set property a -> ${newVal}`)
val = newVal
}
})
Таким образом, когда мы посещаемobj.a, Распечататьget property aи возвращает 1,obj.a = 2При установке нового значения выведитеset property a -> 2. Это эквивалентно нашему обычномуobj.aПоведение извлечения и присвоения значений с использованием пользовательскихgetterа такжеsetterпереписать исходное поведение, которое数据劫持имея в виду.
Но есть проблема с приведенным выше кодом: нам нужна глобальная переменная для хранения значения этого свойства, поэтому мы можем написать следующее
// value使用了参数默认值
function defineReactive(data, key, value = data[key]) {
Object.defineProperty(data, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
}
})
}
defineReactive(obj, a, 1)
еслиobjКак насчет нескольких свойств? Мы можем создать новый классObserverперебирать объект
class Observer {
constructor(value) {
this.value = value
this.walk()
}
walk() {
Object.keys(this.value).forEach((key) => defineReactive(this.value, key))
}
}
const obj = { a: 1, b: 2 }
new Observer(obj)
еслиobjКак насчет вложенных свойств? Мы можем использовать рекурсию для захвата данных вложенных свойств.
// 入口函数
function observe(data) {
if (typeof data !== 'object') return
// 调用Observer
new Observer(data)
}
class Observer {
constructor(value) {
this.value = value
this.walk()
}
walk() {
// 遍历该对象,并进行数据劫持
Object.keys(this.value).forEach((key) => defineReactive(this.value, key))
}
}
function defineReactive(data, key, value = data[key]) {
// 如果value是对象,递归调用observe来监测该对象
// 如果value不是对象,observe函数会直接返回
observe(value)
Object.defineProperty(data, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue) // 设置的新值也要被监听
}
})
}
const obj = {
a: 1,
b: {
c: 2
}
}
observe(obj)
В этой части у вас может немного закружиться голова, так что давайте разберемся:
执行observe(obj)
├── new Observer(obj),并执行this.walk()遍历obj的属性,执行defineReactive()
├── defineReactive(obj, a)
├── 执行observe(obj.a) 发现obj.a不是对象,直接返回
├── 执行defineReactive(obj, a) 的剩余代码
├── defineReactive(obj, b)
├── 执行observe(obj.b) 发现obj.b是对象
├── 执行 new Observer(obj.b),遍历obj.b的属性,执行defineReactive()
├── 执行defineReactive(obj.b, c)
├── 执行observe(obj.b.c) 发现obj.b.c不是对象,直接返回
├── 执行defineReactive(obj.b, c)的剩余代码
├── 执行defineReactive(obj, b)的剩余代码
代码执行结束
Можно видеть, что отношение вызова трех вышеперечисленных функций выглядит следующим образом:
Три функции вызывают друг друга для формирования рекурсии, которая отличается от обычной рекурсии.
Некоторые студенты могут подумать, что покаsetterВызовите функцию рендеринга для повторного рендеринга страницы, вы не можете завершить обновление страницы при изменении данных? Это действительно возможно, но стоимость этого такова: любое изменение данных вызовет повторную визуализацию страницы, что слишком дорого. Эффект, который мы хотим сделать, таков: когда данные изменяются, обновляются только данные, связанные с этими данными.DOMструктуре, она включает в себя следующее:依赖
2. Собирайте зависимости и распространяйте обновления
полагаться
Прежде чем формально объяснить сбор зависимостей, давайте посмотрим, что такое зависимость. Возьмем пример из жизни: покупки на Таобао. Теперь в магазине на Таобао есть видеокарта (Воздух) находится на стадии предпродажи, если мы хотим его купить, то можем нажать预售提醒, Когда видеокарта начинает продаваться, Taobao отправляет нам сообщение.После того, как мы увидим сообщение, мы можем начать покупать.
Абстрагированием этого примера является модель публикации-подписки: покупатель нажимает на напоминание перед продажей, что эквивалентно регистрации его собственной информации (подписки) на Taobao, и Taobao сохраняет информацию о покупателе в структуре данных (например, в массиве). . Когда видеокарта будет официально открыта для покупки, Taobao уведомит всех покупателей: видеокарта поступила в продажу (выпущена), и покупатель предпримет какие-то действия на основании этой новости (Например, выкупить его для майнинга).
существуетVueВ адаптивной системе данным соответствует видеокарта, так чему же соответствует покупатель в примере? является абстрактным классом:Watcher. Вам не нужно ломать голову над значением имени, просто знайте, что оно делает: каждыйWatcherЭкземпляр подписывается на один или несколько данных, также известных какwactherЗависимость (товар — зависимость покупателя), когда зависимость меняется,WatcherЭкземпляр получит сообщение о том, что данные изменились, а затем выполнит функцию обратного вызова для реализации определенных функций, таких как обновление страницы (покупатель выполняет какие-то действия).
следовательноWatcherКласс может быть реализован следующим образом
class Watcher {
constructor(data, expression, cb) {
// data: 数据对象,如obj
// expression:表达式,如b.c,根据data和expression就可以获取watcher依赖的数据
// cb:依赖变化时触发的回调
this.data = data
this.expression = expression
this.cb = cb
// 初始化watcher实例时订阅数据
this.value = this.get()
}
get() {
const value = parsePath(this.data, this.expression)
return value
}
// 当收到数据变化的消息时执行该方法,从而调用cb
update() {
this.value = parsePath(this.data, this.expression) // 对存储的数据进行更新
cb()
}
}
function parsePath(obj, expression) {
const segments = expression.split('.')
for (let key of segments) {
if (!obj) return
obj = obj[key]
}
return obj
}
если ты прав
WatcherЕсли есть какие-либо сомнения по поводу того, когда этот класс будет создан, не беда, мы скоро об этом поговорим.
На самом деле в предыдущем примере есть еще один момент, о котором мы еще не упомянули: в примере с видеокартой Taobao сохранит информацию о покупателе в массиве, поэтому в нашей адаптивной системе также должен быть массив для сохранения информации о покупателе. информация, а также то, чтоwatcher.
Подводя итог функциям, которые нам необходимо реализовать:
- иметь массив для хранения
watcher -
watcherЭкземпляры должны подписывать (зависимости) данные, то есть получать зависимости или собирать зависимости -
watcherСрабатывает, когда зависимостиwatcherФункция обратного вызова, то есть обновление отправки.
Каждые данные должны поддерживать собственный массив, который используется для хранения данных, зависящих от самих себя.watcher, мы можемdefineReactiveопределить массив вdep, так что через замыкание каждое свойство может иметь своеdep
function defineReactive(data, key, value = data[key]) {
const dep = [] // 增加
observe(value)
Object.defineProperty(data, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue)
dep.notify()
}
})
}
На данный момент мы реализовали первую функцию, а затем реализовали процесс сбора зависимостей.
Коллекция зависимостей
Теперь давайте сосредоточимся на начальном процессе рендеринга страницы (игнорируем функцию рендеринга и виртуальнуюDOMи т. д.): движок рендеринга будет анализировать шаблон, например, движок встречает интерполяционное выражение, если мы создаем экземплярwatcher, что случится? отWatcherКак вы можете видеть в коде, он будет выполнен при создании экземпляра.getметод,getЭффект от метода такой获取Данные, от которых мы зависим, и мы переписываем поведение доступа к данным, определяем для каждых данныхgetter,следовательноgetterфункция будет выполняться, если мы находимся вgetterцентрwatcherдобавить вdepВ массиве (информация о малозарегистрированном покупателе Taobao) сбор зависимостей не может быть завершен! !
Примечание: выполнить до
getterчас,new Watcher()изgetМетод еще не выполнен.
new Watcher()выполнить, когдаconstructor, который вызывает экземплярgetметод, экземплярgetМетод считывает значение данных, которое запускает данныеgetter,getterПосле выполнения экземплярgetМетод выполняется и возвращает значение,constructorПосле завершения выполнения создание экземпляра завершается.
У некоторых студентов могут возникнуть сомнения: очевидно, да
watcherДля сбора зависимостей необходимоwatcherСбор данных, как они стали даннымиdepсобиратьwatcherкакие? Студенты, у которых есть этот вопрос, могут посмотреть предыдущий пример Taobao (Taobao записывает информацию о пользователях) или узнать больше о модели публикации-подписки.
С помощью приведенного выше анализа нам нужно толькоgetterВнесите некоторые изменения:
get: function reactiveGetter() {
dep.push(watcher) // 新增
return value
}
Проблема приходит снова,watcherОткуда эта переменная? Мы создаем экземпляр в функции компиляции шаблонаwatcherиз,getterНе могу получить этот экземпляр. Решение тоже очень простое,watcherНе будет ли достаточно поместить экземпляр глобально, например, вwindow.targetначальство. следовательно,WatcherизgetМетод модифицируется следующим образом
get() {
window.target = this // 新增
const value = parsePath(this.data, this.expression)
return value
}
Таким образом, будетgetв методеdep.push(watcher)превратиться вdep.push(window.target)Вот и все.
Обратите внимание, что вы не можете писать
window.target = new Watcher(). потому что исполнениеgetterпри создании экземпляраwatcherеще не закончено, так чтоwindow.targetещеundefined
Процесс сбора зависимостей: выражения интерполяции встречаются при рендеринге страниц,
v-bindКогда вам нужны данные и т. д., вы создадите экземплярwatcher, создать экземплярwatcherбудет оценивать зависимые данные, запускаяgetter,данныеgetterФункция сама добавит зависимостиwatcher, тем самым завершая сбор зависимостей. Мы можем понять это какwatcherСбор зависимостей и способ реализации кода заключается в хранении зависимостей в данных.watcher
Внимательные читатели могут обнаружить, что при использовании этого метода новое выражение интерполяции создается каждый раз, когда встречается выражение интерполяции.
watcher, так что каждый узел соответствуетwatcher. На самом деле этоvue1.xМетод заключается в обновлении в единицах узлов с более тонкой детализацией. а такжеvue2.xПодход заключается в том, что каждый компонент соответствуетwatcher, экземплярwatcherбольше не являетсяexpression, но функция рендеринга, которая преобразуется из шаблона компонента, такого компонентаwatcherВы можете собрать все свои зависимости и обновить их в единицах компонентов, что является средним способом. достигатьvue2.xОтзывчивая система включает в себя множество других вещей, таких как компонентизация, виртуальнаяDOMи т. д., и эта серия статей фокусируется только на принципе реагирования на данные, поэтому его нельзя реализоватьvue2.x, но принцип одинаков для обоих с точки зрения отзывчивости.
Распространять обновления
После реализации сбора зависимостей последней функцией, которую мы хотим реализовать, является отправка обновлений, то есть запуск при изменении зависимостей.watcherПерезвоните. Из части сбора зависимостей мы знаем, какие данные получены, то есть какие данные запускаются.getter, это значитwatcherНа какие данные полагаться и как уведомлять об изменении данныхwatcherШерстяная ткань? Думаю, многие студенты уже догадались:setterРаспространяйте обновления.
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue)
dep.forEach(d => d.update()) // 新增 update方法见Watcher类
}
3. Оптимизируйте код
1. Депозитный класс
мы можем поставитьdepМассив абстрагируется в класс:
class Dep {
constructor() {
this.subs = []
}
depend() {
this.addSub(Dep.target)
}
notify() {
const subs = [...this.subs]
subs.forEach((s) => s.update())
}
addSub(sub) {
this.subs.push(sub)
}
}
defineReactiveФункцию нужно только соответствующим образом изменить
function defineReactive(data, key, value = data[key]) {
const dep = new Dep() // 修改
observe(value)
Object.defineProperty(data, key, {
get: function reactiveGetter() {
dep.depend() // 修改
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue)
dep.notify() // 修改
}
})
}
2. window.target
существуетwatcherизgetметод
get() {
window.target = this // 设置了window.target
const value = parsePath(this.data, this.expression)
return value
}
Как вы могли заметить, мы не сбрасывалиwindow.target. Некоторые учащиеся могут подумать, что это нормально, но рассмотрим следующий сценарий: есть объектobj: { a: 1, b: 2 }Сначала мы создаем экземплярwatcher1,watcher1полагатьсяobj.a,Такwindow.targetто естьwatcher1. После этого мы посетилиobj.b, что просходит? доступobj.bвызоветobj.bизgetter,getterпозвонюdep.depend(),Такobj.bизdepбудет собиратьwindow.target, то есть,watcher1, что приводит кwatcher1зависел отobj.b, это не правда. Для решения этой проблемы делаем следующие модификации:
// Watcher的get方法
get() {
window.target = this
const value = parsePath(this.data, this.expression)
window.target = null // 新增,求值完毕后重置window.target
return value
}
// Dep的depend方法
depend() {
if (Dep.target) { // 新增
this.addSub(Dep.target)
}
}
Из вышеприведенного анализа видно, что,window.targetСмысл в том, что в текущем контексте выполненияwatcherпример. из-заjsОднопоточная функция, только по одной за разwatcherкод выполняется, поэтомуwindow.targetв настоящее время находится в процессе создания экземпляраwatcher
3. способ обновления
что мы достигли раньшеupdateМетоды, как показано ниже:
update() {
this.value = parsePath(this.data, this.expression)
this.cb()
}
Давайте рассмотримvm.$watchметод, к которому мы можем получить доступ в определенном обратном вызовеthis, а обратный вызов может получать новые и старые значения данных прослушивания, поэтому внесите следующие изменения
update() {
const oldValue = this.value
this.value = parsePath(this.data, this.expression)
this.cb.call(this.data, this.value, oldValue)
}
4. Узнайте об исходном коде Vue
существуетИсходный код Vue — строка 56, мы увидим такую переменную:targetStack, который выглядит как нашwindow.targetНемного связано, да, это так. Представьте себе такой сценарий: у нас есть два вложенных родительского и дочернего компонента, и при рендеринге родительского компонента будет создан новый родительский компонент.watcher, обнаруживается, что в процессе рендеринга есть подкомпоненты, он начнет рендерить подкомпоненты, и будет создан новый подкомпонент.watcher. В нашей реализации создайте новый родительский компонентwatcherчас,window.targetбудет указывать на родительский компонентwatcher, а затем создайте новый подкомпонентwatcher,window.targetбудет стегать компонентыwatcherПереопределить, дочерний компонент визуализируется, вернуться к родительскому компонентуwatcherчас,window.targetсталnull, это вызовет проблемы, поэтому мы используем структуру стека для сохраненияwatcher.
const targetStack = []
function pushTarget(_target) {
targetStack.push(window.target)
window.target = _target
}
function popTarget() {
window.target = targetStack.pop()
}
WatcherизgetМетод модифицируется следующим образом
get() {
pushTarget(this) // 修改
const value = parsePath(this.data, this.expression)
popTarget() // 修改
return value
}
также,Vueиспользуется вDep.targetвместоwindow.targetчтобы сохранить текущийwatcher, это не имеет большого эффекта, пока существует глобально уникальная переменная для сохранения текущегоwatcherТолько что
5. Обобщите код
Код теперь резюмируется следующим образом:
// 调用该方法来检测数据
function observe(data) {
if (typeof data !== 'object') return
new Observer(data)
}
class Observer {
constructor(value) {
this.value = value
this.walk()
}
walk() {
Object.keys(this.value).forEach((key) => defineReactive(this.value, key))
}
}
// 数据拦截
function defineReactive(data, key, value = data[key]) {
const dep = new Dep()
observe(value)
Object.defineProperty(data, key, {
get: function reactiveGetter() {
dep.depend()
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue)
dep.notify()
}
})
}
// 依赖
class Dep {
constructor() {
this.subs = []
}
depend() {
if (Dep.target) {
this.addSub(Dep.target)
}
}
notify() {
const subs = [...this.subs]
subs.forEach((s) => s.update())
}
addSub(sub) {
this.subs.push(sub)
}
}
Dep.target = null
const TargetStack = []
function pushTarget(_target) {
TargetStack.push(Dep.target)
Dep.target = _target
}
function popTarget() {
Dep.target = TargetStack.pop()
}
// watcher
class Watcher {
constructor(data, expression, cb) {
this.data = data
this.expression = expression
this.cb = cb
this.value = this.get()
}
get() {
pushTarget(this)
const value = parsePath(this.data, this.expression)
popTarget()
return value
}
update() {
const oldValue = this.value
this.value = parsePath(this.data, this.expression)
this.cb.call(this.data, this.value, oldValue)
}
}
// 工具函数
function parsePath(obj, expression) {
const segments = expression.split('.')
for (let key of segments) {
if (!obj) return
obj = obj[key]
}
return obj
}
// for test
let obj = {
a: 1,
b: {
m: {
n: 4
}
}
}
observe(obj)
let w1 = new Watcher(obj, 'a', (val, oldVal) => {
console.log(`obj.a 从 ${oldVal}(oldVal) 变成了 ${val}(newVal)`)
})
4. Меры предосторожности
1. Закрытие
VueДостижение такой мощной функции неотделимо от кредита закрытия: вdefineReactiveЗамыкание формируется в , так что каждое свойство каждого объекта может содержать свое значениеvalueи зависимые объектыdep.
2. Будут ли зависимости собираться, пока срабатывает геттер?
ответ отрицательный. существуетDepизdependметод, мы видим, что толькоDep.targetЗависимости будут добавлены только в том случае, если они верны. Например, он будет запущен при отправке обновления.watcherизupdateметод, который также вызываетparsePathпринять значение, но в это времяDep.targetдляnull, никакие зависимости добавляться не будут. При ближайшем рассмотрении видно, что толькоwatcherизgetметод будет вызыватьсяpushTarget(this)иди прямоDep.targetзадание, в другое времяDep.targetобеnull,а такжеgetметод будет только инстанцированwatcherвызывается когда , поэтому в нашей реализацииwatcherЗависимости значения уже определены при его создании, и любые последующие операции, считывающие значение, не будут увеличивать зависимость.
3. Использование свойств вложенных объектов
Мы комбинируем приведенный выше код, чтобы подумать над следующим вопросом:
let w2 = new Watcher(obj, 'b.m.n', (val, oldVal) => {
console.log(`obj.b.m.n 从 ${oldVal}(oldVal) 变成了 ${val}(newVal)`)
})
мы знаем,w2будет зависеть отobj.b.m.n, ноw2будет зависеть отobj.b, obj.b.m? Или,obj.b,和obj.b.m, они хранятся в замыканииdepбудутw2? Ответ положительный. Давайте не будем сначала анализировать это с точки зрения кода, представьте, что если мы позволимobj.b = null, то очевидноw2Функция обратного вызова должна быть запущена, что означаетw2Будет опираться на свойства объекта промежуточного уровня.
Далее, давайте проанализируем это с уровня кода:new Watcher(), он позвонитwatcher的getметод, будетDep.targetУстановить какw2,getметод вызоветparsePathЧтобы получить значение, давайте посмотрим на конкретный процесс получения значения:
function parsePath(obj, expression) {
const segments = expression.split('.') // 先将表达式分割,segments:['b', 'm', 'n']
// 循环取值
for (let key of segments) {
if (!obj) return
obj = obj[key]
}
return obj
}
Приведенный выше поток кода выглядит следующим образом:
- локальная переменная
objдля объектаobj, читатьobj.bзначение, триггерыgetter,вызыватьdep.depend()(Долженdepдаobj.bв закрытииdep),Dep.targetсуществует, добавьте зависимости - локальная переменная
objдляobj.b, читатьobj.b.mзначение, триггерыgetter,вызыватьdep.depend()(Долженdepдаobj.b.mв закрытииdep),Dep.targetсуществует, добавьте зависимости - локальная переменная
objдля объектаobj.b.m, читатьobj.b.m.nзначение, триггерыgetter,вызыватьdep.depend()(Долженdepдаobj.b.m.nв закрытииdep),Dep.targetсуществует, добавьте зависимости
Как видно из приведенного выше кода,w2будет зависеть от каждого элемента, относящегося к целевому свойству, что также логично.
5. Резюме
В заключение:
- передача
observe(obj),Будуobjустановить реактивный объект,observe函数,Observe, defineReactive函数Трое звонят друг другу, таким образом, рекурсивноobjУстановить как реактивный объект - создается при отображении страницы
watcher, этот процесс будет считывать значение зависимых данных, завершая таким образом在getter中获取依赖 - Запускается при изменении зависимостей
setter, тем самым отправляя обновление, выполняя обратный вызов и завершая在setter中派发更新
взять дырку
Строго говоря, готовая адаптивная система не может использоваться для рендеринга страниц, потому что рендеринг реальной страницыwatcherНет необходимости задавать функцию обратного вызова, мы называем это рендерингом.watcher. Кроме того, рендерингwatcherВы можете получить функцию рендеринга вместо выражения в качестве параметра и автоматически выполнять повторный рендеринг при изменении зависимости, что вызовет проблему повторяющихся зависимостей. Кроме того, еще один важный элемент, который мы не рассмотрели, — это обработка массивов.
Я не могу понять проблемы, упомянутые выше, это не имеет значения, статьи после этой серии будут решать эти проблемы шаг за шагом, я надеюсь, что вы можете продолжать уделять внимание.