Рука об руку, чтобы помочь вам понять вычислительный принцип Vue

Vue.js

предисловие

computedсуществуетVueЭто очень часто используемая конфигурация свойств, которая может меняться при изменении свойств зависимостей, что очень удобно. Тогда эта статья даст вам полное представлениеcomputedвнутренние принципы и рабочий процесс.

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

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

Если вы хотите понять принцип, самое главное — знать, как его использовать, что поможет вам понять это позже.

Первое, объявление функции:

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

Во-вторых, объявление объекта:

computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}

Напоминание: Атрибуты данных, используемые в вычислении, в дальнейшем вместе именуются «атрибутами зависимостей».

процесс работы

Сначала понятьcomputedПримерный процесс, чтобы увидеть, что является основной точкой вычислительных свойств.

Входной файл:

// 源码位置:/src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

_init:

// 源码位置:/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // mergeOptions 对 mixin 选项和 new Vue 传入的 options 选项进行合并
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    // 初始化数据
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

initState:

// 源码位置:/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 */)
  }
  // 这里会初始化 Computed
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initComputed:

// 源码位置:/src/core/instance/state.js 
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  // 1
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
    
  for (const key in computed) {
    const userDef = computed[key]
    // 2
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if (!isSSR) {
      // create internal watcher for the computed property.
      // 3
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        { lazy: true }
      )
    }

    // 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)) {
      // 4
      defineComputed(vm, key, userDef)
    }
  }
}
  1. Определяется в экземпляре_computedWatchersОбъект, используемый для хранения «вычисленных свойств»Watcher"
  2. получить вычисляемое свойствоgetter, вам нужно определить, является ли это объявлением функции или объявлением объекта
  3. Создайте «вычисляемое свойство»Watcher",getterПереданный в качестве параметра, он будет вызываться при обновлении свойства зависимости, и вычисляемое свойство будет переоценено. требует вниманияWatcherизlazyКонфигурация, которая является идентификатором реализации кеша
  4. defineComputedПерехват данных вычисляемых свойств

defineComputed:

// 源码位置:/src/core/instance/state.js 
const noop = function() {}
// 1
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 判断是否为服务端渲染
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    // 2
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    // 3
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // 4
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
  1. sharedPropertyDefinitionявляется начальным объектом описания свойства вычисляемого свойства
  2. Когда вычисляемое свойство объявляется с помощью функции, установите объект описания свойства.getа такжеset
  3. Когда вычисляемое свойство объявляется с использованием объекта, установите объект описания свойства.getа такжеset
  4. Перехват данных вычисляемых свойств,sharedPropertyDefinitionПередать в качестве третьего параметра

Рендеринг на стороне клиента используетcreateComputedGetterСоздайтеget, рендеринг на стороне сервера используетcreateGetterInvokerСоздайтеget. Между ними есть большая разница: рендеринг на стороне сервера не кэширует вычисляемые свойства, а оценивает их напрямую:

function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

Но обычно мы больше говорим о рендеринге на стороне клиента, давайте посмотримcreateComputedGetterреализация.

createComputedGetter:

// 源码位置:/src/core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    // 1
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 2
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 3
      if (Dep.target) {
        watcher.depend()
      }
      // 4
      return watcher.value
    }
  }
}

Это ядро ​​реализации вычисляемого свойства,computedGetterТо есть он срабатывает, когда вычисляемое свойство выполняет перехват данных.get.

  1. надinitComputedфункция, "вычисляемое свойствоWatcher" хранится в экземпляре_computedWatchers, здесь вынесите соответствующее "вычисляемое свойство"Watcher"
  2. watcher.dirtyявляется триггерной точкой для реализации кэширования вычисляемых свойств,watcher.evaluateПовторная оценка вычисляемых свойств
  3. Коллекция свойств зависимостей "рендеринг"Watcher"
  4. Вычисляемое свойство оценивает и сохраняет значение вvalueсередина,getВозвращает значение вычисляемого свойства

Кэширование и обновление вычисляемого свойства

тайник

Далее мы будемcreateComputedGetterРазделите, проанализируйте их отдельные рабочие процессы. Это триггер для кэширования:

if (watcher.dirty) {
  watcher.evaluate()
}

см. далееWatcherСоответствующая реализация:

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
    // 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
    // dirty 初始值等同于 lazy
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

Также не забудьте создать "вычисляемое свойство"Watcher", настроеноlazyправда.dirtyНачальное значение эквивалентноlazy. Поэтому, когда рендеринг страницы инициализируется и значение вычисляемого свойства извлекается, оно будет выполнено один раз.watcher.evaluate.

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

После оценки присвоить значениеthis.value,надcreateComputedGetterвнутриwatcher.valueПросто обновите здесь. тогдаdirtyУстановите значение false, если свойство зависимости не изменится, при следующем извлечении значения оно не будет выполнено.watcher.evaluate, а напрямую вернутьсяwatcher.value, который реализует механизм кэширования.

возобновить

Когда свойство зависимости будет обновлено, оно будет вызыватьсяdep.notify:

notify() {
  this.subs.forEach(watcher => watcher.update())
}

затем выполнитьwatcher.update:

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

Как «рассчитать недвижимостьWatcher"изlazyправда, вотdirtyбудет установлено значение true. Дождитесь, пока рендеринг страницы примет значение вычисляемого свойства и будет выполнено условие точки срабатывания, выполнитеwatcher.evaluateВыполните повторную оценку, и вычисленное свойство обновится соответствующим образом.

Свойства зависимостей собирают зависимости

Соберите средство наблюдения за вычисляемыми свойствами

При инициализации рендеринг страницы будет «рендеритьWatcher"Вставить в стек и установить наDep.target

Вычисляемое свойство встречается во время рендеринга страницы, и его значение получено, поэтому выполнитеwatcher.evaluateлогика, затем позвонитеthis.get:

get () {
  // 1
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 2
    value = this.getter.call(vm, vm) // 计算属性求值
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    popTarget()
    this.cleanupDeps()
  }
  return value
}
Dep.target = null
let stack = []  // 存储 watcher 的栈

export function pushTarget(watcher) {
  stack.push(watcher)
  Dep.target = watcher
} 

export function popTarget(){
  stack.pop()
  Dep.target = stack[stack.length - 1]
}

pushTargetЭто "вычисляемое свойство"Watcher"Вставить в стек и установить наDep.target, в это время стек [рендеринг Watcher, вычисление свойства Watcher]

this.getterОценивать вычисляемые свойства, запускать перехват данных свойств зависимостей при их полученииget,воплощать в жизньdep.dependсобирать зависимости ("вычисленное свойствоWatcher»)

Сбор рендеринга Watcher

this.getterПосле завершения оценкиpopTragte, "вычисленные свойстваWatcher«Из стека,Dep.targetустановить на "рендеринг"Watcher", В настоящее времяDep.targetэто "рендерингWatcher"

if (Dep.target) {
  watcher.depend()
}

watcher.dependСоберите зависимости:

depend() {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

depsВнутреннее хранилище является свойством зависимостиdep, этот шаг представляет собой зависимость коллекции свойств зависимостей ("рендерингWatcher»)

После того, как две вышеуказанные зависимости собраны, свойство зависимостиsubsхранить дваWatcher, [наблюдатель вычисляемых свойств, наблюдатель отрисовки]

Почему свойства зависимостей собирают наблюдатель рендеринга

Когда я впервые прочитал исходный код, было странно, что свойства зависимостей собирают «вычисляемые свойства».Watcher«Разве это не хорошо? Почему свойства зависимостей все еще собираются» рендерингWatcher"?

Сценарий 1. В шаблоне используются как свойства зависимостей, так и вычисляемые свойства.

<template>
  <div>{{msg}} {{msg1}}</div>
</template>

export default {
  data(){
    return {
      msg: 'hello'
    }
  },
  computed:{
    msg1(){
      return this.msg + ' world'      
    }
  }
}

Шаблоны используют свойства зависимости. Когда страница отображает значение свойства зависимости, свойство зависимости сохраняет значение «рендеринг».Watcher",такwatcher.dependЭтот шаг представляет собой повторяющийся сбор, ноwatcherВнутреннее будет разгружено.

Вот почему я сомневаюсь,VueДля хорошей структуры должна быть причина. Поэтому я подумал о другом сценарии, который можно разумно объяснить.watcher.dependэффект.

Второй сценарий: в шаблоне используются только вычисляемые свойства

<template>
  <div>{{msg1}}</div>
</template>

export default {
  data(){
    return {
      msg: 'hello'
    }
  },
  computed:{
    msg1(){
      return this.msg + ' world'      
    }
  }
}

Свойства зависимостей не используются в шаблоне. При отображении страницы свойства зависимостей не будут собирать «рендеринг».Watcher". В настоящее время в свойствах зависимостей будут только "вычисляемые свойства"Watcher", когда свойство зависимости изменяется, активируется только "вычисляемое свойство"Watcher"изupdate. Расчет собственностиupdateбудет толькоdirtyУстановите значение true, и вычисляемое свойство не будет обновляться, если оно не оценивается немедленно.

Итак, вам нужно собрать «рендерингWatcher", после выполнения "вычисленного свойства"Watcher", а затем выполните "RenderWatcher". Рендеринг страницы принимает значение вычисляемого свойства и выполняетwatcher.evaluateОценка будет пересчитана, а вычисляемое свойство страницы будет обновлено.

Суммировать

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

Предпосылка обновления вычислительных свойств требует «рендерингаWatcher'', поэтому свойство зависимостиsubsбудет хранить не менее двухWatcher.