Суть вычислений в Vue — ленивые часы

внешний интерфейс Vue.js

Два месяца назад я перевел краткое введение в Vue в Nuggets.computedЭто статья о том, как это работает, перевод очень общий, поэтому я не буду публиковать адрес. Старший, которым я очень восхищаюсь, прокомментировал статью, а содержание - это название этой статьи"Чувствую, что исходный текст не проясняет суть вычисляемой реализации - ленивый наблюдатель". В прошлые выходные я как раз изучал исходный код Vue, причем специально его читал.computed, поделитесь результатами, которые вы видите, с другими.

Советы: если вы раньше не видели исходный код Vue или мало знаете о принципе привязки данных Vue, я рекомендую вам прочитать мою предыдущую статью.Простая и понятная интерпретация исходного кода привязки данных Vue., или другие статьи, связанные с форумными блогами (таких статей много в Интернете). Потому что, чтобы понять эту статью, вам нужен этот пункт знаний.

один. initComputed 

Сначала предположим, что такой наборcomputed:

//先假设有两个data: data_one 和 data_two
computed:{
    isComputed:function(){
        return this.data_one + 1;
    },
    isMethods:function(){
        return this.data_two + this.data_one;
    }
}

мы знаем, что вnew Vue()Когда он выполняет серию операций инициализации, в Vuedata,props,methods,computedвсе инициализированы здесь:

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

В статье о привязке данных я подробно рассказал об этом.initData()Эта функция и в этой статье я сосредоточусь на подробномinitComputed()эта функция.

const computedWatcherOptions = { lazy: true } //用于传入Watcher实例的一个对象

function initComputed (vm: Component, computed: Object) {
  //声明一个watchers,同时挂载到Vue实例上
  const watchers = vm._computedWatchers = Object.create(null)
  //是否是服务器渲染
  const isSSR = isServerRendering()

  //遍历传入的computed
  for (const key in computed) {
    //userDef是computed对象中的每一个方法
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }
    
    //如果不是服务端渲染的,就创建一个Watcher实例
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    if (!(key in vm)) {
      //如果computed中的key没有在vm中,通过defineComputed挂载上去
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      //后面都是警告computed中的key重名的
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

существуетinitComputedРанее мы видели, что объявленоcomputedWatcherOptionsОбъект этого объекта является ключом к реализации «ленивого наблюдателя».

см. далееinitComputed, который сначала объявляет пустой объект с именем watchers и монтирует этот пустой объект на виртуальной машине. Затем выполните итерацию по вычисленным свойствам и назначьте метод каждого свойства дляuserDef,еслиuserDefЕсли это функция, назначьте ееgetter, а затем рассудите, является ли это рендерингом на стороне сервера, если нет, создайте егоWatcherпример.WatcherЯ также проанализировал экземпляр в предыдущей статье, поэтому я не буду анализировать его построчно, но следует отметить, что во вновь созданный здесь экземпляр мы передаем четвертый параметр, которыйcomputedWatcherOptions, В настоящее время,WatcherЛогика изменилась:

//这段代码在Watcher类中,文件路径为vue/src/core/observer/watcher.js
if (options) {
    this.deep = !!options.deep
    this.user = !!options.user
    this.lazy = !!options.lazy
    this.sync = !!options.sync
 } else {
    this.deep = this.user = this.lazy = this.sync = false
 }

Варианты здесь относятся кcomputedWatcherOptions, когда мы идемinitDataкогда логикаoptionsне существует, поэтомуthis.lazy = false, но когда у нас естьcomputedWatcherOptionsназад,this.lazy = true. В то же время за этим стоит такой код:this.dirty = this.lazy,dirtyЗначение такжеtrue.

this.value = this.lazy
      ? undefined
      : this.get()

Из этого кода мы можем узнать, что когдаlazyдляfalse, он возвращаетсяundefinedвместоthis.get()метод. Другими словами, не выполняетcomputedДва метода в: (см. вычисляемый пример, который я написал в начале)

function(){
  return this.data_one + 1;
}
function(){
  return this.data_two + this.data_one;
}

Это также означает,computedЗначение еще не обновлено. И этой логике пока придет конец.

2. определить свойство

давай вернемсяinitComputedИз функции:

if (!(key in vm)) {
   //如果computed中的key没有在vm中,通过defineComputed挂载上去
   defineComputed(vm, key, userDef)
} 

Видно, что когда значение ключа не смонтировано на вм, выполнитьdefineComputedфункция:

//一个用来组装defineProperty的对象
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  //是否是服务端渲染,注意这个变量名 => shouldCache
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    //如果userDef是function,给sharedPropertyDefinition.get也就是当前key的getter
    //赋上createComputedGetter(key)
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    //否则就使用userDef.get和userDef.set赋值
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  //最后,我们把这个key挂载到vm上
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

defineComputed, сначала определите, является ли это рендерингом на стороне сервера, если нет, значит, вычисляемое свойство нужно кэшировать, т. е.shouldCacheэто дляtrue. Далее, судьяuserDefТо ли это функция, то ли это, значит, это наша рутинаcomputedиспользование, будетgetterустановить какcreateComputedGetter(key)Возвращаемое значение. Если это не функция, значит это вычисляемое свойство настроено нами и его нужно использоватьuserDef.getа такжеuserDef.setпридти дляgetterа такжеsetterНазначение, я не буду подробно останавливаться на этой другой части, она не будет настроенаcomputedдрузья могут просматривать документсеттеры для вычисляемых свойств. Наконец, будетcomputedКлюч монтируется на виртуальной машине, и геттер вызывается при доступе к вычисляемому свойству.

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

Наконец мы видимcreateComputedGetterЭта функция, он возвращает функциюcomputedGetter, то еслиwatcherприсутствие, судьяwatcher.dirtyСуществует ли, согласно предыдущему анализу, первая новаяWatcherПримеры времениthis.dirtyэто дляtrue, звоните в это времяwatcher.evaluate():

function evaluate () {
    this.value = this.get()
    this.dirty = false
}

this.get()На самом деле это метод, который выполняет вычисляемое свойство. позжеthis.dirtyустановить какfalse. Кроме того, когда мы выполняемthis.get()Это будетDep.targetНазначено, поэтому оно также будет выполненоwatcher.depend(), который будет вычислять свойствоwatcherдобавить в зависимости. последнее возвращениеwatcher.value, наконец, получаем значение вычисляемого свойства, donecomputedинициализация.

3. Кэш вычисляемых свойств — ленивый Watcher

Однако на данный момент мы не рассмотрели пункт этой статьи, а именно «ленивый наблюдатель». Помните, что официальная документация Vue описывает это так:computedиз:

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

Оглядываясь назад на предыдущий код, мы обнаружили, что до тех пор, пока значение свойства данных в вычисляемом свойстве не обновляется, после первого получения значения watch.lazy всегда имеет значение false и никогда не будет выполняться.watcher.evaluate(), поэтому это вычисляемое свойство никогда не будет переоцениваться, всегда используя последнее полученное значение (также известное как кэшированное).

Как только значение атрибута данных изменится, как мы знаем, это вызоветupdate()Вызвать повторную визуализацию страницы (эта часть контента немного нервная, друзья, которые не понимают, должны сначала понять принцип привязки данных к данным), повторноinitComputed,Такthis.dirty = this.lazy = true, вычисляемое свойство будет переоценено.

Хорошо, я закончил говорить о принципе вычислений, но эта статья все еще оставляет дыру вcreateComputedGetterВ функции есть такая строка кода:

const watcher = this._computedWatchers && this._computedWatchers[key]

Из контекста мы можем сделать выводthis._computedWatchersдолжны быть сохранены вinitComputedКогда создается экземпляр наблюдателя, но когда поместить этот экземпляр вthis._computedWatchersв? Я еще не нашел его.Если кто-то знает, пожалуйста, оставьте сообщение, чтобы поделиться, и мы можем обсудить вместе, большое спасибо!