Подробное объяснение принципа работы vuex

Vue.js Vuex внешний фреймворк

предисловие

Vuex, как структура управления состоянием, официально созданная Vue, а также простой дизайн API и удобная поддержка инструментов разработки, хорошо используются в средних и крупных проектах Vue. в видеfluxВосходящая звезда архитектуры поглощает своих предшественниковreduxРазличные преимущества идеального сочетанияvueизОтзывчивыйДанные, лично считаю, что опыт разработки превысилReact + ReduxЭта пара друзей.

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

понимать вычисляемый

Использование данных в хранилище в vuex практически неотделимо от общего атрибута, вычисляемого в vue. Самый простой официальный пример выглядит следующим образом

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

Я не знаю, думали ли вы о том, как обновляются вычисления Vue, и почему vm.reversedMessage автоматически изменяется при изменении vm.message?

Давайте посмотрим на исходный код, связанный с атрибутом данных и вычисленный в vue.

// src/core/instance/state.js
// 初始化组件的state
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)
  // 当组件存在data属性
  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)
  }
}

Метод initState автоматически запускается при создании экземпляра компонента. Этот метод в основном завершает инициализацию данных, методов, свойств, вычисляет и просматривает эти часто используемые свойства. Давайте посмотрим, на что нам нужно обратить внимание.initDataиinitComputed(менее релевантный код удален для экономии времени)

Первый взглядinitData этой строки

// src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // .....省略无关代码
  
  // 将vue的data传入observe方法
  observe(data, true /* asRootData */)
}

// src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value)) {
    return
  }
  let ob: Observer | void
  // ...省略无关代码
  ob = new Observer(value)
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}


При инициализации метод наблюдения по существу создает экземпляр объекта Observer, класс этого объекта выглядит следующим образом:

// src/core/observer/index.js
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    // 关键代码 new Dep对象
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // ...省略无关代码
    this.walk(value)
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 给data的所有属性调用defineReactive
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

В конструкторе объекта последний вызовwalkметод, который просматривает все атрибуты в данных и вызываетdefineReactiveметод,defineReactiveпутьvueОсновой для реализации MDV (Model-Driven-View) по существу являются методы set и get, которые представляют данные Когда данные изменяются или получаются, их можно воспринять (конечно,vueТакже рассмотрим различные ситуации, такие как массивы, вложенные объекты в объекты, которые не анализируются в этой статье). Давайте взглянемdefineReactiveисходный код

// src/core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 重点,在给具体属性调用该方法时,都会为该属性生成唯一的dep对象
  const dep = new Dep()

  // 获取该属性的描述对象
  // 该方法会返回对象中某个属性的具体描述
  // api地址https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果该描述不能被更改,直接返回,因为不能更改,那么就无法代理set和get方法,无法做到响应式
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  // 重新定义data当中的属性,对get和set进行代理。
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 收集依赖, reversedMessage为什么会跟着message变化的原因
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 通知依赖进行更新
      dep.notify()
    }
  })
}

Мы можем видеть это в所代理的属性изgetметод, он будет вызываться, когда существует dep.Targetdep.depend()метод, этот метод очень прост, но прежде чем мы поговорим об этом методе, нам нужно знать новый классDep

Dep — это объект, реализованный Vue для обработки зависимостей. Он в основном действует как ссылка, которая должна соединять реактивные данные и наблюдатель. Код очень простой.

// src/core/observer/dep.js
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      // 更新 watcher 的值,与 watcher.evaluate() 类似,
      // 但 update 是给依赖变化时使用的,包含对 watch 的处理
      subs[i].update()
    }
  }
}

// 当首次计算 computed 属性的值时,Dep 将会在计算期间对依赖进行收集
Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
  // 在一次依赖收集期间,如果有其他依赖收集任务开始(比如:当前 computed 计算属性嵌套其他 computed 计算属性),
  // 那么将会把当前 target 暂存到 targetStack,先进行其他 target 的依赖收集,
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  // 当嵌套的依赖收集任务完成后,将 target 恢复为上一层的 Watcher,并继续做依赖收集
  Dep.target = targetStack.pop()
}

Код очень прост, вернемся к вызовуdep.depend()метод, когдаDep.Targetсуществует, он будет вызываться, иdepend方法это добавить отл.watcherизnewDeps, в то же время будет所访问当前属性изdepв объектеsubsВставьте наблюдателя текущего Dep.target.Это кажется немного запутанным, но это не имеет значения, позже мы объясним это на примере.

После разговора о get и методе прокси, давайте поговорим о методе set прокси.Наконец вызывается метод set.dep.notify(), При установке определенного значения атрибута в данных будет вызываться следующий атрибутdep.notify()метод, черезclass DepПодразумевается, что метод notify будет добавлен ко всем наблюдателям депа, то есть когда вы модифицируетеdataКогда значение свойства установлено, оно будет вызываться одновременноdep.notify()обновить все зависимости, которые зависят от этого значенияwatcher.

Введение оконченоinitDataЭту линию мы продолжаем знакомитьinitComputedЭта строка, эта строка в основном решает, когда устанавливатьDep.targetпроблема (если значение не задано, то не будет вызыватьсяdep.depend(), то есть зависимость не может быть получена).

// src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  // 初始化watchers列表
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (!isSSR) {
      // 关注点1,给所有属性生成自己的watcher, 可以在this._computedWatchers下看到
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    if (!(key in vm)) {
      // 关注点2
      defineComputed(vm, key, userDef)
    }
  }
}

При инициализации вычислений есть 2 места, на которые следует обратить внимание.

  1. Для каждого свойства создается собственный экземпляр наблюдателя, а{ lazy: true }Передать как варианты
  2. Метод defineComputed вызывается для каждого атрибута (по сути, то же самое, что и data, проксируя свои собственные методы set и get, мы ориентируемся на прокси-данные).getметод)

ПосмотримWatcherконструктор

// src/core/observer/watcher.js
constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    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
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // 如果初始化lazy=true时(暗示是computed属性),那么dirty也是true,需要等待更新
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.getter = expOrFn // 在computed实例化时,将具体的属性值放入this.getter中
    // 省略不相关的代码
    this.value = this.lazy
      ? undefined
      : this.get()
  }

В дополнение к ежедневной инициализации есть 2 важные строки кода

this.dirty = this.lazy this.getter = expOrFn

существуетcomputedСгенерированоwatcher, установит ленивый наблюдателя в значение true, чтобы уменьшить объем вычислений. Следовательно, при создании экземпляра this.dirty также имеет значение true, что указывает на необходимость обновления данных. давайте вспомним сейчасВ вычислении для грязного и ленивого наблюдателя, сгенерированного каждым атрибутом, установлено значение true.. В то же время значение свойства, переданное вычисляемым(обычно работает), положить вwatcherизgetterХранится в.

Давайте посмотрим на вторую проблемуdefineComputedЧто такое метод get проксируемого свойства

// src/core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    // 如果找到了该属性的watcher
    if (watcher) {
      // 和上文对应,初始化时,该dirty为true,也就是说,当第一次访问computed中的属性的时候,会调用 watcher.evaluate()方法;
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

когда第一次При доступе к значению в вычислении это будет связано с инициализациейwatcher.dirty = watcher.lazyПричина вызова метода evalute(), метода evalute() очень проста, то есть он вызывает экземпляр наблюдателя вgetметоды и настройкиdirty = false, мы объединяем эти два метода

// src/core/instance/state.js
evaluate () {
  this.value = this.get()
  this.dirty = false
}
  
get () {  
// 重点1,将当前watcher放入Dep.target对象
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 重点2,当调用用户传入的方法时,会触发什么?
    value = this.getter.call(vm, vm)
  } catch (e) {
  } finally {
    popTarget()
    // 去除不相关代码
  }
  return value
}

В методе get первая строка вызываетсяpushTargetметод, эффект которого заключается вDep.targetУстановите для входящего наблюдателя, то есть посещенногоcomputedсреднийwatcher,
а потом позвонилvalue = this.getter.call(vm, vm)метод, подумайте об этом, что происходит, когда вы вызываете этот метод?

this.getterКак упоминалось в функции построения Watcher, суть заключается в методе, переданном пользователем, т. е.this.getter.call(vm, vm)Будет вызван метод, объявленный пользователем, тогда, если метод используетсяthis.dataИспользуется значение in или otherdefineReactiveОбернутый объект затем получает доступ к this.data.defineReactiveБудет ли обернутое свойство обращаться к методу get проксируемого свойства. мы оглядываемся назад
getКак выглядит метод.

Примечание: я говорил о другом использовании defineReactive, это связано с vuex позже, мы упомянем об этом позже.

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 这个时候,有值了
      if (Dep.target) {
        // computed的watcher依赖了this.data的dep
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    }

Комментарии к коду были написаны, поэтому я не буду их объяснять.На данный момент мы завершили процесс сбора зависимостей и знаем, как вычисляется, знает, от кого зависит. Наконец, согласно прокси this.datasetметод называетсяnotify, вы можете изменить значение this.data, чтобы обновить значение всех вычисляемых свойств, зависящих от значения this.data.

Затем по следующему коду просто разберем процесс получения зависимостей и обновления

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join()
    }
  }
})
vm.reversedMessage // =>  olleH
vm.message = 'World' // 
vm.reversedMessage // =>  dlroW
  1. Инициализируйте данные и вычисляйте, проксируйте их методы set и get соответственно и создавайте уникальный экземпляр dep для всех атрибутов в данных.
  2. Создайте уникальный наблюдатель для reversedMessage в вычислении и сохраните его в vm._computedWatchers
  3. доступreversedMessage, установите Dep.target так, чтобы он указывал на наблюдатель reversedMessage, и вызовите конкретный метод этого свойства.reversedMessage.
  4. Если вы получите доступ к this.message в методе, будет вызван метод get прокси-сервера this.message, иdepПрисоединяйтесь ко входу reversedMessagewatcher, и одновременно в отд.subsДобавь этоwatcher
  5. настраиватьvm.message = 'World', вызовите метод set агента сообщений, чтобы вызватьдеп уведомитьметод'
  6. Поскольку это вычисляемое свойство, просто поместитеwatcherсерединаdirtyустановить на истину
  7. последний шагvm.reversedMessage, когда вы обращаетесь к его методу get, вы знаетеreversedMessageизwatcher.dirtyправда, звонитеwatcher.evaluate()способ получения нового значения.

Таким образом, это также может объяснить, почему иногда, когда к вычислению не обращаются (или не полагаются шаблоны), когда значение this.data изменяется, оно находится через vue-toolscomputedПричина, по которой значение in не меняется, потому что оно не срабатываетgetметод.

плагин vuex

С учетом вышеизложенного в качестве предзнаменования мы можем легко объяснить принцип работы vuex.

Мы знаем, что vuex существует только как подключаемый модуль vue, в отличие от Redux, MobX и других библиотек, которые можно применять ко всем фреймворкам, vuex можно использовать только на vue, в основном потому, что он сильно зависит от вычисляемой системы обнаружения зависимостей vue и его система плагинов,

пройти черезофициальная документацияМы знаем, что у каждого плагина vue должен быть общедоступный метод установки, и vuex не является исключением. Код относительно прост, и вызывается метод applyMixin, который в основном используется во всех компонентах.beforeCreateЖизненный цикл внедряет настройкиthis.$storeТакой объект, поскольку он относительно прост, не будет здесь подробно вводить код, и каждому будет легко его прочитать самостоятельно.

// src/store.js
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
// src/mixins.js
// 对应applyMixin方法
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

Когда мы используем vuex в нашем бизнесе, нам нужно написать что-то вроде следующего

const store = new Vuex.Store({
    state,
    mutations,
    actions,
    modules
});

ТакVuex.StoreЧто это за вещь? Давайте сначала посмотрим на его конструктор

// src/store.js
constructor (options = {}) {
  const {
    plugins = [],
    strict = false
  } = options

  // store internal state
  this._committing = false
  this._actions = Object.create(null)
  this._actionSubscribers = []
  this._mutations = Object.create(null)
  this._wrappedGetters = Object.create(null)
  this._modules = new ModuleCollection(options)
  this._modulesNamespaceMap = Object.create(null)
  this._subscribers = []
  this._watcherVM = new Vue()

  const store = this
  const { dispatch, commit } = this
  this.dispatch = function boundDispatch (type, payload) {
    return dispatch.call(store, type, payload)
}
  this.commit = function boundCommit (type, payload, options) {
    return commit.call(store, type, payload, options)
}

  // strict mode
  this.strict = strict

  const state = this._modules.root.state

  // init root module.
  // this also recursively registers all sub-modules
  // and collects all module getters inside this._wrappedGetters
  installModule(this, state, [], this._modules.root)

  // 重点方法 ,重置VM
  resetStoreVM(this, state)

  // apply plugins
  plugins.forEach(plugin => plugin(this))

}

Помимо кучи инициализации мы заметили такую ​​строчку кода
resetStoreVM(this, state)Он ключ ко всему vuex

// src/store.js
function resetStoreVM (store, state, hot) {
  // 省略无关代码
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      ?state: state
    },
    computed
  })
}

Удалив некоторый нерелевантный код, мы обнаружили, что суть заключается в том, чтобы использовать состояние, которое мы передали, как данные скрытого компонента vue, то есть наша операция фиксации фактически состоит в изменении значения данных этого компонента. вычислено выше, измененоdefineReactiveПосле проксирования значения объекта он соберет зависимостиwatcherсерединаdirtyУстановите значение true и подождите до следующего раза, чтобы получить доступ к значению в наблюдателе, чтобы снова получить последнее значение.

Это объясняет, почему свойства объекта состояния в vuex должны быть определены заранее, еслиstateувеличить наполовинуатрибут, посколькуАтрибутыне былdefineReactive, поэтому его зависимая система не определяется и, естественно, не может быть обновлена.

Из вышеизложенного мы можем знать, что store._vm.$data.?state === store.state, мы можем сделать это в любом проекте, содержащем фреймворк vuex.


Суммировать

Общая идея vuex родилась вflux, но его метод реализации полностью использует собственный адаптивный дизайн Vue.Мониторинг зависимостей и сбор зависимостей относятся к прокси-перехвату Vue метода получения набора свойств объекта. Последнее предложение заканчивается тем, как работает vuex,vuex中的store本质就是没有template的隐藏着的vue组件;

(Если эта статья помогла всем, ставьте лайк, спасибо)~

Справочная статья

Глубокое понимание вычисляемых свойств Vue Computed