предисловие
Когда многие люди упоминают вычисляемые свойства во Vue, первая реакция заключается в том, что вычисляемые свойства будут кэшироваться, так как же именно они кэшируются? Что именно кешируется и когда кеш выйдет из строя, я думаю, многие люди до сих пор не понимают этого.
В этой статье, основанной на версии Vue 2.6.11, мы углубимся в принцип и познакомим вас с тем, как выглядит так называемый кеш.
Уведомление
В этой статье предполагается, что у вас есть базовое понимание реактивных принципов Vue, если выWatcher,Depи что такое渲染watcherЕсли вы не очень хорошо знакомы с концепциями, вы можете поискать статьи или учебные пособия по основным принципам адаптивного дизайна. Для видеоурока я рекомендую г-на Хуан И. Если вы хотите увидеть упрощенную реализацию, вы также можете прочитать статью, которую я написал первой:
Обратите внимание, что я также описал принцип вычисления в этой статье, но вычисление в этой статье основано на версии Vue 2.5, а изменения по сравнению с текущей версией 2.6 все еще очень велики, поэтому вы можете использовать его только в качестве справки.
Пример
Согласно условности моей статьи, я продемонстрирую это на минимальном примере.
<div id="app">
<span @click="change">{{sum}}</span>
</div>
<script src="./vue2.6.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
count: 1,
}
},
methods: {
change() {
this.count = 2
},
},
computed: {
sum() {
return this.count + 1
},
},
})
</script>
Этот пример очень простой, просто начните показывать числа на странице.2, после нажатия на цифру становится3.
Разобрать
Просмотрите процесс наблюдателя
Переходя к теме, Vue инициализирует вычисляемое свойство при первом запуске.Во-первых, давайте рассмотрим концепцию наблюдателя.Его основная концепцияgetоценить иupdateвозобновить.
-
При оценке сначаласамТо есть сам наблюдатель назначается
Dep.targetэта глобальная переменная. -
Затем в процессе оценки будет прочитан отзывчивый атрибут, и dep адаптивного атрибута соберет этот наблюдатель как зависимость.
-
В следующий раз, когда адаптивное свойство будет обновлено, оно найдет наблюдатель, собранный им из dep, и активирует его.
watcher.update()обновить.
Так что самое главное, что этоgetДля чего используется этотupdateКакое обновление будет запущено.
В базовом потоке просмотра реактивного обновления вышеперечисленные концепцииgetОценка относится к функции повторного рендеринга компонентов Vue, иupdateКогда это происходит, он фактически повторно вызывает функцию рендеринга компонента для обновления представления.
Отличительной чертой Vue является то, что этот процесс также применяется к вычисляемым обновлениям.
инициализировать вычисляемый
Сначала спойлер, Vue также будет обертывать каждое вычисляемое свойство в опции с помощью наблюдателя.getфункция, очевидно, предназначена для выполнения определяемой пользователем функции оценки, иupdateЭто более сложный процесс, и далее я объясню его подробно.
Во-первых, когда компонент инициализируется, он войдет в функцию, которая инициализирует вычисленные
if (opts.computed) { initComputed(vm, opts.computed); }
ВойтиinitComputedпосмотри
var watchers = vm._computedWatchers = Object.create(null);
// 依次为每个 computed 属性定义
for (const key in computed) {
const userDef = computed[key]
watchers[key] = new Watcher(
vm, // 实例
getter, // 用户传入的求值函数 sum
noop, // 回调函数 可以先忽视
{ lazy: true } // 声明 lazy 属性 标记 computed watcher
)
// 用户在调用 this.sum 的时候,会发生的事情
defineComputed(vm, key, userDef)
}
Сначала определяется пустой объект, используемый для хранения всех вычисляемых свойств, связанных с Watcher, после чего мы назовем его计算watcher.
Затем цикл генерирует вычисляемое свойство для каждого计算watcher.
Его форма сохраняет ключевые свойства и упрощается следующим образом:
{
deps: [],
dirty: true,
getter: ƒ sum(),
lazy: true,
value: undefined
}
могу это увидетьvalueСначала это было неопределенно,lazyЭто правда, что указывает на то, что его значение вычисляется лениво, и оно не будет вычислено до тех пор, пока оно не будет фактически прочитано в шаблоне.
этоdirtyАтрибуты на самом деле являются ключом к кэшированию, запомните его в первую очередь.
Далее обратите внимание на более важныеdefineComputed, который определяет, когда пользователь читаетthis.sumТо, что происходит после того, как значение этого вычисляемого свойства продолжает упрощаться и исключать некоторую логику, не влияющую на процесс.
Object.defineProperty(vm, 'sum', {
get() {
// 从刚刚说过的组件实例上拿到 computed watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// ✨ 注意!这里只有dirty了才会重新求值
if (watcher.dirty) {
// 这里会求值 调用 get
watcher.evaluate()
}
// ✨ 这里也是个关键 等会细讲
if (Dep.target) {
watcher.depend()
}
// 最后返回计算出来的值
return watcher.value
}
}
})
На эту функцию нужно обратить внимание, она делает несколько вещей, мы объясним это с помощью процесса инициализации:
первыйdirtyЭта концепция представляет грязные данные, указывая на то, что эти данные должны быть отозваны пользователем.sumфункция для оценки. Давайте пока проигнорируем логику обновления и прочитаем ее в шаблоне в первый раз.{{sum}}Это должно быть правдой, поэтому инициализация проходит оценку.
evaluate () {
// 调用 get 函数求值
this.value = this.get()
// 把 dirty 标记为 false
this.dirty = false
}
Эта функция на самом деле очень понятная, она сначала вычисляет, а потом ставитdirtyУстановите значение «ложь».
Оглянитесь на абзац, который мы только что имелиObject.definePropertyлогика,
Нет особых обстоятельств в следующий раз, затем прочитайтеsumкогда это было найденоdirtyЕсли оно ложно, должно ли оно быть возвращено напрямую?watcher.valueЭто значение в порядке, что на самом делеКэш вычисляемых свойствКонцепция чего-либо.
возобновить
Процесс инициализации окончен, я считаю, что все правыdirtyа также缓存Имейте общее представление (если нет, внимательно оглянитесь назад).
Далее я расскажу о процессе обновления, который доработан до примера в этой статье, т.е.countКак именно запускается обновление?sumИзменения на странице.
Прежде всего, вернемся к тому, что я только что упомянул.evaluteфункция, то есть чтениеsumОперация оценки, выполняемая при обнаружении грязных данных.
evaluate () {
// 调用 get 函数求值
this.value = this.get()
// 把 dirty 标记为 false
this.dirty = false
}
Dep.target изменен на визуализацию наблюдателя
Вход здесьthis.get(), во-первых, чтобы быть ясным, прочитайте в шаблоне{{ sum }}переменная, глобальнаяDep.targetдолжно быть渲染watcher, если вы не понимаете этого здесь, вы можете перейти к статье, которую я упомянул в начале, чтобы понять это.
ГлобальныйDep.targetсостояние - это стекtargetStackсохранить для легкой вперед и назадDep.target, а когда он откатится, можно посмотреть в следующей функции.
В настоящее время Dep.target является наблюдателем за рендерингом, а targetStack — [наблюдателем за рендерингом].
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} finally {
popTarget()
}
return value
}
Первый входpushTarget, то есть положить计算watcherустановить себя какDep.target, ожидая сбора зависимостей.
законченныйpushTarget(this)назад,
Dep.target изменен на наблюдатель за расчетами
В настоящее время Dep.target является наблюдателем вычислений, а targetStack [рендеринг наблюдателя, вычисление наблюдателя].
затем выполняется дляvalue = this.getter.call(vm, vm),
фактическиgetterФункция, которая была объяснена в форме наблюдателя в предыдущей главе, передается пользователем.sumфункция.
sum() {
return this.count + 1
}
Здесь при выполнении читайтеthis.count, обратите внимание, что это адаптивное свойство, поэтому они как-то начинают устанавливаться неразрывно связанными...
сработает здесьcountизgetУгони, упрости
// 在闭包中,会保留对于 count 这个 key 所定义的 dep
const dep = new Dep()
// 闭包中也会保留上一次 set 函数所设置的 val
let val
Object.defineProperty(vm, 'count', {
get: function reactiveGetter () {
const value = val
// Dep.target 此时就是计算watcher
if (Dep.target) {
// 收集依赖
dep.depend()
}
return value
},
})
Тогда видно, чтоcountбудет собирать计算watcherКак зависимость, как ее собрать?
// dep.depend()
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
Собственно вот звонокDep.target.addDep(this)собирать, возвращаться к计算watcherизaddDepФункция повышается, что на самом деле в основном связано с некоторыми оптимизациями дедупликации, сделанными внутри Vue.
// watcher 的 addDep函数
addDep (dep: Dep) {
// 这里做了一系列的去重操作 简化掉
// 这里会把 count 的 dep 也存在自身的 deps 上
this.deps.push(dep)
// 又带着 watcher 自身作为参数
// 回到 dep 的 addSub 函数了
dep.addSub(this)
}
назад сноваdepподнялся.
class Dep {
subs = []
addSub (sub: Watcher) {
this.subs.push(sub)
}
}
Это экономит计算watcherтак какcountЗависимости в депе исчезли.
Пройдя такой процесс сбора, некоторые в это время заявляют:
sum 的计算watcher:
{
deps: [ count的dep ],
dirty: false, // 求值完了 所以是false
value: 2, // 1 + 1 = 2
getter: ƒ sum(),
lazy: true
}
count的dep:
{
subs: [ sum的计算watcher ]
}
Можно видеть, что наблюдатель вычисляемого свойства и dep реактивного значения, от которого оно зависит, сохраняют друг друга и зависят друг от друга.
На этом оценка окончена, вернитесь к计算watcherизgetterфункция:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} finally {
// 此时执行到这里了
popTarget()
}
return value
}
реализованоpopTarget,计算watcherвытолкнуть стек.
Dep.target изменен на визуализацию наблюдателя
В настоящее время Dep.target является наблюдателем за рендерингом, а targetStack — [наблюдателем за рендерингом].
Затем функция завершает выполнение и возвращает2это значение, в это время дляsumатрибутgetВизит еще не закончен.
Object.defineProperty(vm, 'sum', {
get() {
// 此时函数执行到了这里
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
})
В настоящее времяDep.targetКонечно, это того стоит渲染watcher, поэтому введитеwatcher.depend()Логика, этот шагвесьма критично.
// watcher.depend
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
вспомни только сейчас计算watcherформа? этоdepsсохранено вcountзам.
То есть он снова позвонитcountВверхdep.depend()
class Dep {
subs = []
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
этот разDep.targetуже渲染watcher, так что этоcountДеп, в свою очередь, поставит渲染watcherхранится в собственномsubsсередина.
count的dep:
{
subs: [ sum的计算watcher,渲染watcher ]
}
Итак, подходя к сути этого вопроса, на этот разcountОбновлено, как запустить обновление представления?
назад сноваcountЛогика реактивного захвата выглядит следующим образом:
// 在闭包中,会保留对于 count 这个 key 所定义的 dep
const dep = new Dep()
// 闭包中也会保留上一次 set 函数所设置的 val
let val
Object.defineProperty(vm, 'count', {
set: function reactiveSetter (newVal) {
val = newVal
// 触发 count 的 dep 的 notify
dep.notify()
}
})
})
Хорошо, вот триггер, который мы только что тщательно подготовилиcountДЕПnotifyФункция, я чувствую себя ближе и ближе к успеху.
class Dep {
subs = []
notify () {
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Логика здесь очень проста, положиsubsНаблюдатели, сохраненные в наблюдателе, по очереди вызывают своих наблюдателей.updateметод, то есть
- передача
计算watcherобновление - передача
渲染watcherобновление
Разбери его.
Рассчитать обновление наблюдателя
update () {
if (this.lazy) {
this.dirty = true
}
}
wtf, вот и все... Да просто поставил计算watcherизdirtyУстановите для свойства значение true и спокойно ждите следующего чтения.
Обновление наблюдателя за рендерингом
Вот собственно и звонитvm._update(vm._render())Эта функция, основанная наrenderсгенерированная функцияvnodeПришло время визуализировать представление.
пока вrender, вы обязательно посетитеsumэто значение, затем вернуться кsumОпределенныйgetначальство:
Object.defineProperty(vm, 'sum', {
get() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// ✨上一步中 dirty 已经置为 true, 所以会重新求值
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
// 最后返回计算出来的值
return watcher.value
}
}
})
Запущено из-за обновления реактивного свойства на предыдущем шаге计算 watcherизdirtyОбновить до истинного. Таким образом, он повторно вызовет пользователя, переданного вsumФункция вычисляет последнее значение, и страница естественным образом отображает последнее значение.
На этом весь процесс обновления вычисляемого свойства завершен.
Когда действует кеш
В соответствии с приведенным выше резюме, только при обновлении ответного значения зависимости вычисляемого свойства будетdirtyСбросьте значение true, чтобы фактическое вычисление не происходило при следующем чтении.
В этом случае предположимsumФункция представляет собой определяемую пользователем операцию, требующую много времени, и оптимизация более очевидна.
<div id="app">
<span @click="change">{{sum}}</span>
<span @click="changeOther">{{other}}</span>
</div>
<script src="./vue2.6.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
count: 1,
other: 'Hello'
}
},
methods: {
change() {
this.count = 2
},
changeOther() {
this.other = 'ssh'
}
},
computed: {
// 非常耗时的计算属性
sum() {
let i = 9999999999999999
while(i > 0) {
i--
}
return this.count + 1
},
},
})
</script>
В этом примереotherЗначение не имеет ничего общего с вычисляемыми свойствами, еслиotherЕсли значение инициирует обновление, представление будет повторно визуализировано, аsum, если вычисляемое свойство не кэшируется, каждый раз будут выполняться ненужные вычисления, требующие высокой производительности.
Поэтому только вcountкогда происходят изменения,sumОн будет пересчитан, что является очень умной оптимизацией.
Суммировать
Путь обновления вычисляемого свойства версии 2.6 выглядит следующим образом:
- отзывчивое значение
countвозобновить - Также уведомить
computed watcherа также渲染 watcherвозобновить -
computed watcherустановить грязное в истинное - Рендеринг просмотра считывает вычисленное значение из-за грязного
computed watcherПереоценить.
Я полагаю, что благодаря этой статье вы сможете полностью понять, что такое концепция кэширования вычислительных свойств и при каких обстоятельствах она вступит в силу?
Для кешированных и некэшированных случаев процессы следующие:
Не кэшировать:
-
countизменить, сообщить первым计算watcherобновить, установитьdirty = true - уведомить снова
渲染watcherобновить, перейти при повторном отображении вида计算watcherпрочитать значение, найтиdirtyистинно, повторно выполняет оценку функции, переданную пользователем.
кеш:
-
otherизменение, прямое уведомление渲染watcherвозобновить. - идти, когда представление повторно отображается
计算watcherпрочитать значение, найтиdirtyЕсли false, использовать кэшированное значение напрямую.watcher.value, не выполняет оценку функции, переданную пользователем.
Перспектива
На самом деле этот пропускdirtyСпособ реализации кэша вычисляемых свойств с использованием бита флага соответствует принципу реализации в Vue3. Это также может указывать на то, что с учетом различных требований и отзывов сообщества You Da в настоящее время считает этот метод относительно оптимальным решением для реализации вычисляемого кэша.
Если вам интересна вычислительная реализация Vue3, вы также можете прочитать мою статью Принципы аналогичны. Просто небольшое изменение в способе сбора.
Углубленный анализ: как Vue3 умело реализует мощные вычисления
❤️Спасибо всем
1. Если эта статья была вам полезна, пожалуйста, поддержите ее лайком, ваш "лайк" - движущая сила моего творчества.
2. Подпишитесь на официальный аккаунт «Front-end from advanced to accept», чтобы добавить меня в друзья, я втяну вас в «Front-end группу расширенного обмена», все смогут общаться и добиваться прогресса вместе.