Понять принцип Vuex

Vue.js

vuexтак какVueНеотъемлемая часть всего семейного ведра, изучение и понимание его исходного кода может не только изучить превосходные идеи автора и навыки написания кода, но и помочь нам писать более качественный и стандартизированный код в процессе разработки.

Исходная версия 3.1.2, старайтесь не использовать исходный код напрямую во время отладки.console.log, поскольку иногда его вывод не соответствует ожидаемым данным, рекомендуется использоватьdebuggerОтладка и чтение исходного кода В следующей статье некоторая обработка совместимости и надежности в исходном коде будет соответствующим образом проигнорирована, и будет виден только основной процесс.

use

существуетvueКогда плагин используется вVue.use(Vuex)Плагин будет обработан, этот процесс пройдетmixinСпасательные крючки в различных компонентахbeforeCreateувеличение для каждого экземпляра в$storeАтрибуты

install

существуетvueпроект, использоватьvuexДля управления данными первое, что нужно сделать, этоvuexимпорт иVue.use(Vuex)

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

в исполненииVue.use(Vuex), это вызоветvuexметоды, представленные вinstallинициализация и будетVueПередано как параметр, всеvueПлагины предоставляютinstallметод, используемый для инициализации некоторых операций, метод находится в/src/store.jsподвергается в

let Vue // bind on install

export function install (_Vue) {
  // 容错判断
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue // 只初始化赋值一次--单例模式
  applyMixin(Vue)
}

первый вstoreопределить переменную вVue, принятьVueпример

installВ функции он сначала определит, была ли она вызванаVue.use(Vuex), а затем позвонитеapplyMixinметод для инициализации некоторых операций

Суммировать:installМетод только сделали отказоустойчивым, а потом вызовapplyMixin,Vueназначать

applyMixin

applyMixinметод в/src/mixinвыставлены в$storeобъект

export default function (Vue) {
  // 获取当前的vue版本号
  const version = Number(Vue.version.split('.')[0])

  // 若是2以上的vue版本,直接通过mixin进行挂载$store
  if (version >= 2) {
    // 在每个实例beforeCreate的时候进行挂载$store
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // vue 1.x版本处理 省略...
  }

  function vuexInit () {
    // 1. 获取每个组件实例的选项
    const options = this.$options

  // 2. 检测options.store是否存在
    if (options.store) {
      // 下面详细说明
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 检测当前组件实例的父组件是够存在,并且其父组件存在$store
      // 存在,则为当前组件实例挂载$store属性
      this.$store = options.parent.$store
    }
  }
}

весьmixinТрудность заключается в понимании файлаthis.$store = typeof options.store === 'function' ? options.store() : options.storeчто ты сделал

В использованииvuex, будуstoreМонтируется на корневой компонент

import Vue from 'vue'
import Counter from './Counter.vue'
import store from './store'

new Vue({
  el: '#app',
  store,
  render: h => h(Counter)
})

в первый звонокvuexInitфункция,options.storeЭто корень вариантовstore, поэтому он определит, является ли его типfunction, если да, выполните функцию и присвойте результат корневому экземпляру$store, в противном случае назначьте напрямую.

Резюме: весьmixinЧто делает файл, так это использоватьmixinЖизненные крючки в отдельных экземплярахbeforeCreateдобавить к нему атрибуты$storeи присвойте ему значение, гарантируя, что его можно будет напрямую передать в каждом экземпляреthis.$storeПолучите данные и поведение.

Module

moduleОсновная функция модуля: определить нашиstoreПо определенным правилам он преобразуется в древовидную структуру и инстанцируетсяStoreПри выполнении он назначит полученную древовидную структуруthis._modules, и последующие операции будут выполняться на основе этого дерева.

древовидная структура

Сначала мыvuexОпределите некоторые состояния и модули и посмотрите, что представляет собой трансформированная древовидная структура.

const state = {
  count: 0
}

const getters = {
}

const mutations = {
}

const actions = {
}

const modules = {
  moduleA:{
    state: {
      a: 'module a'
    },
    modules: {
      moduleB:{
        state: {
          b: 'module b'
        }
      }
    }
  },
  moduleC: {
    state: {
      c: 'module c'
    }
  }
}

export default new Vuex.Store({
  modules,
  state,
  getters,
  actions,
  mutations
})

vuexПосле получения определенного состояния и модуля он будет отформатирован в древовидную структуру, и многие последующие операции основаны на этой древовидной структуре для операций и обработки, которую можно распечатать в любом используемом компоненте.this.$store._modules.rootнаблюдать за его структурой

Форматированная древовидная структура, каждый уровень будет содержатьstate,_rawModule,_childrenтри основных свойства

структура узла дерева

{
  state:{},
  _rawModule:{},
  _children: {}
}

state

Корневой модуль будет обертывать себя и все содержащиеся в нем подмодули.stateДанные размещаются в древовидной структуре в соответствии с уровнем модуля, а корневой модульstateБудет содержать себя и все данные подмодуля,stateбудет содержать данные для себя и своих подмодулей

{
  state: {
  count: 0,
    moduleA: {
      a: 'module a',
        moduleB: {
          b: 'module b'
        }
    },
    moduleC: {
      c: 'module c'
    }
  }
}

_rawModule

Каждый уровень древовидной структуры будет содержать_rawModuleузел, который вызываетstoreпередается при инициализацииoptions, в корне_rawModuleЭто все опции при инициализации, а подмодули используются при их инициализации.options

{
  modules:{},
  state:{},
  getters:{},
  actions:{},
  mutations:{}
}

_children

_childrenТекущий модуль и его подмодули будут отформатированы в соответствии с согласованной древовидной структурой и помещены в родительский компонент или вместе с ним._children, имя ключа - это имя его модуля

{
  moduleA:{
    state: {},
    _rawModule:{},
    _children:{
      moduleB:{
        state: {},
        _rawModule:{},
        _children:{}
      },
    }
  },
  moduleC:{
    state: {},
    _rawModule:{},
    _children:{}
  }
}

Резюме: По призывуstoreПараметры, переданные во время инициализации, преобразуются внутри в древовидную структуру, которую можно передать черезthis.$store._modules.rootПроверить

конвертировать

Зная древовидную структуру после конвертации, давайте посмотримvuexобрабатывается в коде, вsrc/moduleпапка, существуетmodule-collection.jsа такжеmodule.jsДва документа, в основномModuleCollectionа такжеModuleДва класса для коллекции модулей

Module

ModuleОсновная функция состоит в том, чтобы сгенерировать соответствующую структуру узла в соответствии с разработанной структурой узла дерева.После создания экземпляра будет сгенерирована базовая структура данных, и для ее прототипа будут установлены некоторые методы работы для вызова экземпляра.

import { forEachValue } from '../util'

export default class Module {
  constructor (rawModule, runtime) {
    // 是否为运行时 默认为true
    this.runtime = runtime
    // _children 初始化是一个空对象
    this._children = Object.create(null)
    // 将初始化vuex的时候 传递的参数放入_rawModule
    this._rawModule = rawModule
    // 将初始化vuex的时候 传递的参数的state属性放入state
    const rawState = rawModule.state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  // _children 初始化是一个空对象,为其增加子模块
  addChild (key, module) {
    this._children[key] = module
  }
  
  // 根据 key,获取对应的模块
  getChild (key) {
    return this._children[key]
  }
}

ModuleCollection

комбинироватьModuleиспользуется для создания древовидных структур

import Module from './module'
import { forEachValue } from '../util'

export default class ModuleCollection {
  constructor (rawRootModule) {
    // 根据options 注册模块
    this.register([], rawRootModule, false)
  }
 
  // 利用 reduce,根据 path 找到此时子模块对应的父模块
  get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }

  register (path, rawModule, runtime = true) {
    // 初始化一个节点
    const newModule = new Module(rawModule, runtime)
    
    if (path.length === 0) { // 根节点, 此时 path 为 []
      this.root = newModule
    } else { // 子节点处理
      // 1. 找到当前子节点对应的父
      // path ==> [moduleA, moduleC]
      // path.slice(0, -1) ==> [moduleA]
      // get ==> 获取到moduleA
      const parent = this.get(path.slice(0, -1))
      // 2. 调用 Module 的 addChild 方法,为其 _children 增加节点
      parent.addChild(path[path.length - 1], newModule)
    }

    // 若是存在子模块,则会遍历递归调用 register
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}
  1. инициализацияModuleCollectionФактический параметр передан, когдаnew Vuex.Store({....options})серединаoptions, В настоящее времяrawRootModuleто естьoptions, следующие операции основаны наrawRootModuleзамок

optionsаббревиатура структуры данных

{
  modules: {
    moduleA:{
      modules: {
        moduleB:{
        }
      }
    },
    moduleC: {
    }
  }
}
  1. воплощать в жизньthis.register([], rawRootModule, false)

    []соответствующий параметрpath, который сохраняет иерархический путь текущего модуля, напримерmoduleBсоответствующий путь["moduleA", "moduleB"]

    rawRootModuleсоответствующий параметрrawModule, представляющий параметр инициализацииoptionsСоответствующие данные в , напримерmoduleAсоответствующийrawModuleдля:

    moduleA:{
      state: {
        a: 'module a'
      },
      mutations:{
        incrementA: ({ commit }) => commit('increment'),
        decrementA: ({ commit }) => commit('decrement'),
      },
      modules: {
        moduleB:{
          state: {
            b: 'module b'
          }
        }
      }
    }
    
  2. каждое исполнениеregisterсозданныйModule, который генерирует дерево узловnewModule, то, судя поpathдлина, чтобы решитьnewModuleгде разместить первое исполнениеregisterВремяpathдля[], то сразуnewModuleназначить наthis.root, в остальных случаях черезpathНайдите родительский узел, соответствующий текущему узлу, и поместите его в_childrenсередина

  3. судитьrawModule.modulesСуществует ли он, если есть подмодуль, он будет пройденrawModule.modulesсделать рекурсивный вызовregisterВыполните рекурсивную обработку, которая в конечном итоге сгенерирует желаемую древовидную структуру.

Store

Пройдя через предзнаменование, наконец, прибылvuexосновной классstore,существуетstoreбудет определено вstate,mutations,actions,gettersждать обработки

Первый взглядStoreобщая структура

class Store {
  constructor (options = {}) {}

  get state () {}

  set state (v) {}

  commit (_type, _payload, _options) {}

  dispatch (_type, _payload) {}

  subscribe (fn) {}

  subscribeAction (fn) {}

  watch (getter, cb, options) {}

  replaceState (state) {}

  registerModule (path, rawModule, options = {}) {}

  unregisterModule (path) {}

  hotUpdate (newOptions) {}

  _withCommit (fn) {}
}

В использованииvuex, вы увидите, что часто используемые методы и свойства определены вstoreНа занятиях постепенно реализуются основные функции путем совершенствования содержания на занятии

State

определено в модулеstateпройти черезvuxПосле обработки вы можетеvueпрошедший$store.state.xxxИспользуется и будет управлять обновлениями представлений при изменении данных.

первый вstoreинициализировать в

class Store {
  constructor(options) {
    // 定义一些内部的状态  ....
    this._modules = new ModuleCollection(options)
    const state = this._modules.root.state
    // 初始化根模块,会递归注册所有的子模块
    installModule(this, state, [], this._modules.root)
    // 初始化 store、vm
    resetStoreVM(this, state)
  }

  // 利用类的取值函数定义state,其实取的值是内部的_vm伤的数据,代理模式
  get state() {
    return this._vm._data.$state
  }

  _withCommit (fn) {
    fn()
  }
}

будет выполняться первымinstallModule, рекурсивный вызов зарегистрирует данные всех подмодулей, функция будет рекурсивно вызывать себя, чтобы упростить атрибуты подмодулей, и, наконец, имена модулей всех подмодулей будут использоваться в качестве ключей.stateВ качестве соответствующего значения вложен уровень вложенности модуля, и, наконец, создается желаемая структура вложенности данных.

{
  count: 0,
  moduleA: {
    a: "module a",
    moduleB: {
      b: "module b"
    }
  },
  moduleC: {
    c: "module c"
  }
}

installModuleОб обработкеstateОсновной код выглядит следующим образом

/**
 * @param {*} store 整个store
 * @param {*} rootState 当前的根状态
 * @param {*} path 为了递归使用的,路径的一个数组
 * @param {*} module 从根模块开始安装
 * @param {*} hot 
 */
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length // 是不是根节点

  // 设置 state
  // 非根节点的时候 进入,
  if (!isRoot && !hot) {
    // 1. 获取到当前模块的父模块
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 2. 获取当前模块的模块名
    const moduleName = path[path.length - 1]
    // 3. 调用 _withCommit ,执行回调
    store._withCommit(() => {
      // 4. 利用Vue的特性,使用 Vue.set使刚设置的键值也具备响应式,否则Vue监控不到变化
      Vue.set(parentState, moduleName, module.state)
    })
  }

  // 递归处理子模块的state
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

БудуstateПосле обработки в нужную структуру он будет объединятьсяresetStoreVMправильноstateДля обработки, если состояние переменной определено непосредственно в Store, ее можно получить извне, но при ее изменении привязка данных vue не может использоваться для управления обновлением представления, поэтому использование характеристик vue , экземпляр vue помещается в _vm, а затем используется функция значения класса для получения

когда используешь$store.state.countКогда функция значения класса является первойget stateВозьмите значение, значение, возвращаемое функцией значения, равноresetStoreVMназначенный_vm, в сочетании сvueОтзывчивая обработка

function resetStoreVM(store, state) {
  store._vm = new Vue({
    data: {
      $state: state
    }
  })
}

Mutations

существуетvuex, рекомендуется использовать при синхронном изменении состояния данныхmutationsИзмените его, не рекомендуется использовать его напрямуюthis.$store.state.xxx = xxxВнесите изменения, чтобы включить строгий режимstrict: trueобрабатывать

vuexдляmutationsОбработка разделена на две части, первый шаг заключается вinstallModuleкогда все модулиmutationsЧтобы собрать подписки, второй шагStoreнезащищенныйcommitМетод публикует и выполняет соответствующий метод

подписка

первый вStoreДобавить один в обработке_mutationsАтрибуты

constructor(options){
 // 创建一个_mutations 空对象,用于收集各个模块中的 mutations
 this._mutations = Object.create(null)
}

существуетinstallModuleОбработать все рекурсивные вызовы вmutations

const local = module.context = makeLocalContext(store, '', path)
// 处理 mutations
module.forEachMutation((mutation, key) => {
  registerMutation(store, key, mutation, local)
})

существуетregisterMutationсоответствующий в функцииnutationsсобирать

// 注册 mutations 的处理 -- 订阅
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

Все модули на данный моментmutationsбудет подписан на_mutations, вам нужно только найти соответствующийmutationsВыполнение обхода, где массив используется для сбора подписок, потому что вvuex, определенные в разных модулях с тем же именемmutationsбудет выполняться последовательно, поэтому вам нужно использовать подписку на массив и пройти вызов, поэтому также рекомендуется использоватьvuexКогда проект имеет определенную сложность и объем, рекомендуется использовать пространство именnamespaced: true, что может уменьшить ненужное дублированиеmutationsВсе выполняются, что приводит к неконтролируемым проблемам

makeLocalContextфункция будетvuexВозможность обработки, опустить код, чтобы открыть пространство имен, в основном дляgettersа такжеstateугон

function makeLocalContext (store, namespace, path) {
  const local = {
    dispatch: store.dispatch,
    commit: store.commit
  }

  // getters 和 state 必须是懒获取,因为他们的修改会通过vue实例的更新而变化
  Object.defineProperties(local, {
    getters: {
      get: () => store.getters
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

выпускать

После того, как подписка на коллекцию будет завершена, вам необходимоStoreПредоставьте метод для запуска выпуска, выполняя соответствующую функцию для изменения состояния данных.

первый вStoreопределить классcommitметод

{
  // 触发 mutations
  commit (_type, _payload, _options) {
    // 1. 区分不同的调用方式 进行统一处理
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    // 2. 获取到对应type的mutation方法,便利调用
    const entry = this._mutations[type]
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
  }
}

Однако в исходном коде при вызове внешнего мира вызывающий класс напрямую не вызывается.commitметод, но построениеconstructorпереписан вcommit

constructor(options){
 // 1. 获取 commit 方法
  const { commit } = this
  // 2. 使用箭头函数和call 保证this的指向
  this.commit = (type, payload, options) => {
   return commit.call(this, type, payload, options)
  }
} 

unifyObjectStyleМетод выполняет процесс форматирования параметров, и вызов мутаций может использоватьthis.$store.commit('increment', payload)а такжеthis.$store.commit({type: 'increment', payload})Функция unifyObjectStyle предназначена для форматирования различных параметров в одну ситуацию двумя способами:actionsпо аналогии

function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

  return { type, payload, options }
}

Actions

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

подписка

первый вStoreДобавить один в обработке_actionsАтрибуты

constructor(options){
 // 创建一个 _actions 空对象,用于收集各个模块中的 actions
 this._actions = Object.create(null)
}

существуетinstallModuleОбработать все рекурсивные вызовы вactions

const local = module.context = makeLocalContext(store, '', path)
// 处理actions
module.forEachAction((action, key) => {
 const type = action.root ? key : '' + key
 const handler = action.handler || action
 registerAction(store, type, handler, local)
})

существуетregisterActionсоответствующий в функцииactionsсобирать

// 注册 actions 的处理
function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  // actions 和 mutations 在执行时,第一个参数接受到的不一样 
  entry.push(function wrappedActionHandler (payload) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    
    // 判断是否时Promise
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    
    return res
  })
}

выпускать

actionsвыполнение релиза иmutationsЛечение такое же, разница в том, чтоdispatchметод требует дополнительной обработки

// 触发 actipns
dispatch (_type, _payload) {
  // check object-style dispatch
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)

  const action = { type, payload }
  const entry = this._actions[type]

 // 若是多个,则使用Promise.all(),否则执行一次
  const result = entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)
 
  // 拿到执行结果 进行判断处理
  return result.then(res => {
    try {
      this._actionSubscribers
        .filter(sub => sub.after)
        .forEach(sub => sub.after(action, this.state))
    } catch (e) {}
    return res
  })
}