Как Vue.js вычисляет и работает?

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

Статья впервые опубликована на:GitHub.com/US TB-Вуд, умри, о ты…

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

Эта статья будет состоять из следующих шести модулей:

  • вычисленные и просмотренные определения
  • Сходства и различия между компьютерным и часовым использованием
  • Расширенное использование часов
  • Суть вычисляемых - вычисляемые часы
  • Как работают базовые часы?
  • Суммировать

вычисленные и просмотренные определения

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

<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  }
})

результат:

Original message: "Hello"
Computed reversed message: "olleH"

Вычисляемые свойства кэшируются на основе их зависимостей. Они переоцениваются только при изменении соответствующих зависимостей. Стоит отметить, что в реквизитах и ​​данных компонента нельзя определить «reversedMessage», иначе будет сообщено об ошибке.

2.watch — это действие прослушивания, которое наблюдает и реагирует на изменения данных в экземпляре Vue. Пример с официального сайта:

<div id="watch-example">
  <p>
    Ask a yes/no question:
    <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</div>
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
  el: '#watch-example',
  data: {
    question: '',
    answer: 'I cannot give you an answer until you ask a question!'
  },
  watch: {
    // 如果 `question` 发生改变,这个函数就会运行
    question: function (newQuestion, oldQuestion) {
      this.answer = 'Waiting for you to stop typing...'
      this.debouncedGetAnswer()
    }
  },
  created: function () {
    // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
    // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
    // AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
    // `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
    // 请参考:https://lodash.com/docs#debounce
    this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
  },
  methods: {
    getAnswer: function () {
      if (this.question.indexOf('?') === -1) {
        this.answer = 'Questions usually contain a question mark. ;-)'
        return
      }
      this.answer = 'Thinking...'
      var vm = this
      axios.get('https://yesno.wtf/api')
        .then(function (response) {
          vm.answer = _.capitalize(response.data.answer)
        })
        .catch(function (error) {
          vm.answer = 'Error! Could not reach the API. ' + error
        })
    }
  }
})
</script>

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

Сходства и различия между компьютерным и часовым использованием

Ниже приведены сходства и различия между двумя вариантами использования:

такой же: И вычисляемые данные, и часы играют роль мониторинга/полагания на данные и их обработки.

Сходства и различия: на самом деле это реализация прослушивателя Vue, ноComputed в основном используется для обработки синхронных данных, а watch в основном используется для наблюдения за изменением определенного значения для выполнения сложной бизнес-логики с высокими затратами.. Когда вы можете использовать вычисление, сначала используйте вычисление, избегая неловкой ситуации многократного вызова watch, когда несколько данных влияют на один из них.

Расширенное использование часов

1.метод handler и непосредственное свойство

<div id="demo">{{ fullName }}</div>
var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      console.log('第一次没有执行~')
      this.fullName = val + ' ' + this.lastName
    }
  }
})

Как видите, часы не будут выполняться во время инициализации. Глядя на приведенный выше пример, вычисление прослушивателя выполняется только при изменении значения firstName. Но что, если вы хотите выполнить его в первый раз, когда он связан? Теперь давайте изменим наш пример:

  watch: {
    firstName: {
      handler(val) {
        console.log('第一次执行了~')
        this.fullName = val + ' ' + this.lastName
      },
      // 代表在watch里声明了firstName这个方法之后立即先去执行handler方法
      immediate: true
    }
  }

Когда вы открываете консоль, вы видите, что напечатано «выполняется впервые~». Вы обратили внимание на обработчик? Мы привязали метод-обработчик к firstName. Метод watch, который мы написали ранее, на самом деле является этим обработчиком по умолчанию. Vue.js будет обрабатывать эту логику, и окончательная компиляция на самом деле будет этим обработчиком.

Immediate:true означает, что если firstName объявлено в wacth, то метод-обработчик внутри будет выполнен немедленно, если false, то он будет таким же, как наш предыдущий эффект и не будет выполняться при привязке. Зачем добавлять метод-обработчик и немедленное: true для выполнения в первый раз во время привязки? Это будет понятно позже при анализе исходного кода.

2.глубокий атрибут

В часах также есть атрибут deep, который указывает, следует ли включать глубокий мониторинг. Значение по умолчанию — false. Давайте рассмотрим пример:

<div id="app">
  <div>obj.a: {{obj.a}}</div>
  <input type="text" v-model="obj.a">
</div>
var vm = new Vue({
  el: '#app',
  data: {
    obj: {
    	a: 1
    }
  },
  watch: {
    obj: {
      handler(val) {
       console.log('obj.a changed')
      },
      immediate: true
    }
  }
})

Когда мы вводим данные в поле ввода, чтобы изменить значение obj.a, мы обнаруживаем, что «obj.a изменен» не выводится на консоль. Из-за ограничений современного JavaScript (и устаревания Object.observe) Vue не может обнаружить добавление или удаление свойств объекта. Поскольку Vue выполняет процесс преобразования геттера/сеттера для свойств при инициализации экземпляра, свойство должно существовать в объекте данных, чтобы Vue мог преобразовать его, чтобы он был реактивным.

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

mounted() {
  this.obj = {
    a: '123'
  }
}

Этот обработчик выполнит и напечатает «obj.a изменен».

Но что, если нам нужно следить за значением свойства в obj? Вот где атрибут deep пригодится. Нам нужно только добавить deep: true, чтобы тщательно отслеживать значение свойства в obj.

  watch: {
    obj: {
      handler(val) {
       console.log('obj.a changed')
      },
      immediate: true,
      deep: true
    }
  }

Глубокий атрибут означает глубокий обход, который будет проходить объект слой за слоем, добавляя прослушиватель к каждому слою. Вариант реализации в исходном коде определен в src/core/observer/traverse.js:

/* @flow */

import { _Set as Set, isObject } from '../util/index'
import type { SimpleSet } from '../util/index'
import VNode from '../vdom/vnode'

const seenObjects = new Set()

/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

Если this.deep == true, то есть есть дип, запускаем зависимость каждого дип объекта и отслеживаем его изменения. Метод traverse выполняет рекурсию каждого объекта или массива, запуская их геттеры, так что каждый член объекта или массива собирается по зависимостям, образуя «глубокую» зависимость. Также есть небольшая оптимизация в реализации этой функции, в процессе обхода субреактивные объекты записываются в visibleObjects через их dep.id, чтобы избежать повторного обращения в будущем.

Но использование атрибута deep добавит слушателей на каждый уровень, и производительность может быть очень высокой. Таким образом, мы можем оптимизировать в строковой форме:

  watch: {
    'obj.a': {
      handler(val) {
       console.log('obj.a changed')
      },
      immediate: true
      // deep: true
    }
  }

Пока не встретится свойство obj.a, для этого свойства будет установлена ​​функция прослушивания для повышения производительности.

Суть вычисляемых - вычисляемые часы

Мы знаем, что метод _init вызывается при использовании new Vue(), который инициализирует жизненный цикл, инициализирует события, инициализирует рендеринг, инициализирует данные, вычисления, методы, действия и т. д. Если вы не понимаете эту часть, вы можете обратиться к другой статье, которую я написал:Перспектива исходного кода Vue.js: процесс разбора шаблона и рендеринга данных в окончательный DOM. Сегодня я в основном рассматриваю реализацию следующих часов инициализации (initWatch), для облегчения понимания я добавил комментарий, который определен в src/core/instance/state.js:

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

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  // 声明一个watchers且同时挂载到vm实例上
  const watchers = vm._computedWatchers = Object.create(null)
  // 在SSR模式下computed属性只能触发getter方法
  const isSSR = isServerRendering()

  // 遍历传入的computed方法
  for (const key in computed) {
    // 取出computed对象中的每个方法并赋值给userDef
    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
      )
    }

    // 如果不是SSR服务端渲染,则创建一个watcher实例
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      // 如果computed中的key没有设置到vm中,通过defineComputed函数挂载上去 
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // 如果data和props有和computed中的key重名的,会产生warning
      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)
      }
    }
  }
}

В исходном коде мы можем обнаружить, что он сначала объявляет пустой объект с именем watchers, а также монтирует этот пустой объект на vm. Затем просмотрите вычисленные свойства и назначьте метод каждого свойства в userDef. Если userDef — это функция, назначьте ее получателю, а затем определите, выполняется ли рендеринг на стороне сервера. Если нет, создайте экземпляр Watcher. Однако следует отметить, что во вновь созданный здесь экземпляр мы передаем четвертый параметр, который является ComputedWatcherOptions. const ComputedWatcherOptions = {computed: true}, этот объект является ключом для реализации вычисляемого наблюдателя. На данный момент логика в Watcher изменилась:

    // 源码定义在src/core/observer/watcher.js中
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    // 其他的代码......
    this.dirty = this.computed // for computed watchers

Параметры, переданные здесь, — это вычисленныеWatcherOptions, определенные выше.Когда вы переходите к методу initData, параметры не существуют, но когда вы переходите к initComputed, вычисляемый в вычисленныхWatcherOptions является истинным, обратите внимание на приведенную выше строку кода this. dirty = this.computed , присвоить this.computed этому.dirty. Затем посмотрите на код ниже:

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

This.get() можно оценить, только если this.dirty имеет значение true, а затем установить для this.dirty значение false. В процессе оценки выполняется value = this.getter.call(vm, vm), что на самом деле является функцией-получателем, определяемой вычисляемым свойством, в противном случае значение возвращается напрямую.

Когда данные, от которых зависит вычисляемое свойство, изменяются, запускается процесс установки, чтобы уведомить всех наблюдателей, которые подписываются на его изменения, для обновления, и выполняется метод watcher.update():

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

Таким образом, для вычисляемого наблюдателя, такого как вычисляемое свойство, на самом деле есть 2 режима: ленивый и активный. Если установлено this.dep.subs.length === 0, это означает, что никто не подписывается на изменения этого вычисляемого наблюдателя, поэтому установите this.dirty = true, и оно будет переоценено только тогда, когда вычисляемое свойство будет доступ в следующий раз. В противном случае будет выполнен метод getAndInvoke:

  getAndInvoke (cb: Function) {
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      this.dirty = false
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }

Функция getAndInvoke произведет пересчет, а затем сравнит старое и новое значения в трех случаях (1. когда старое и новое значения не хотят ждать, 2. когда значение является объектом или массивом, 3. когда установлено свойство deep), будет выполнена функция обратного вызова, тогда здесь эта функция обратного вызова — это this.dep.notify(), которая в нашем случае запускает наблюдатель рендеринга для повторного рендеринга. Это объясняет то, что написано на официальном сайте.Вычисляемые свойства кэшируются на основе их зависимостей.

Как работают базовые часы?

Как упоминалось выше, метод _init вызывается для завершения инициализации при вызове new Vue(). Среди них вызывается метод initWatch, определенный в src/core/instance/state.js:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

Пройдите объект наблюдения и назначьте каждый watch[key] обработчику. Если это массив, пройдите метод createWatcher фильма, в противном случае вызовите метод createWatcher напрямую. Далее взгляните на определение метода createWatcher:

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

По коду видно, что функция метода createWatcher vm.?watch(keyOrFn, handler, options) вызывает метод Vue.prototype.$watch, определенный в src/core/instance/state.js:

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}

Через код мы можем обнаружить, что часы в конечном итоге вызовут Vue.prototype.watchметод, этот метод сначала оцениваетcbЕсли это объект, вызовитеcreateWatcherметод, потому чтоwatch, этот метод сначала определяет, является ли cb объектом, а затем вызывает метод createWatcher, потому чтоМетод watch может быть вызван непосредственно пользователем, и ему может быть передан объект или функция. Затем выполните const watcher = new Watcher(vm, expOrFn, cb, options), чтобы создать экземпляр наблюдателя. Здесь следует отметить, что это наблюдатель пользователя, потому что options.user = true. Создав экземпляр наблюдателя, как только отслеживаемые данные отправят изменения, он в конечном итоге выполнит метод запуска наблюдателя, выполнит функцию обратного вызова cb, и если мы установим для параметра немедленное значение true, функция обратного вызова cb будет выполняться напрямую. То есть, когда для непосредственного свойства установлено значение true, оно может быть выполнено при первой привязке часов. Наконец, возвращается метод unwatchFn, который вызовет метод разрыва для удаления наблюдателя.

Так как же работает наблюдатель? По сути, он также основан на Watcher, это наблюдатель за пользователями. Как упоминалось ранее, вычисляемое свойство вычисляется по сути вычисляемым наблюдателем.

Суммировать

Благодаря приведенному выше анализу у нас есть глубокое понимание того, как вычисляется вычисляемое свойство и как работает свойство прослушивания. Вычисляемое свойство — это, по сути, вычисляемое наблюдение, а прослушивающее свойство — это, по сути, наблюдение пользователя. И они на самом деле являются реализацией прослушивателя Vue, ноВычисления в основном используются для обработки синхронных данных, а часы в основном используются для наблюдения за изменением определенного значения для выполнения сложной бизнес-логики с большими затратами.. Когда вы можете использовать вычисление, сначала используйте вычисление, избегая неловкой ситуации многократного вызова watch, когда несколько данных влияют на один из них.

Можете обратить внимание на мой паблик-аккаунт «Muchen Classmate», фермера на гусиной фабрике, который обычно записывает какие-то банальные мелочи, технологии, жизнь, инсайты и срастается.