深入了解VUEX原理

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方法仅仅做了一个容错处理,然后调用applyMixinVue赋值

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.jsmodule.js两个文件,主要通过ModuleCollectionModule两个类进行模块收集

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放置的位置,第一次执行registerpath[],则直接将newModule赋值给this.root,其余情况,便是通过path找到当前节点对应的父节点并将其放置在_children

  3. 判断rawModule.modules是否存在,若是存在子模块,便遍历rawModule.modules进行递归调用register进行递归处理,最终会生成一个期望的树形结构

Store

经历了前面的铺垫,终于到了vuex的核心类store,在store中会对定义的statemutationsactionsgetters等进行处理

首先看看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处理成期望的结构之后,会结合resetStoreVMstate进行处理,若是直接在Store中定义变量state,外面可以获取到,但是当修改了之后并不能利用的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的选项进行处理,省略开启命名空间的代码,主要是将gettersstate进行劫持处理

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方法做了一个参数格式化的处理,调用 mutations 可以使用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
  })
}

求内推

此文章还没写作完成,但是想借这个平台获取一些内推的前端职位,所以提前发了出来,后续会继续编写,😄
上海前端有合适岗位的小伙伴可以联系我,杭州的机会也在看,失业中....
微信号: Nnordon_Wang

posted @ 2020-03-04 22:57  NordonWang  阅读(217)  评论(0编辑  收藏  举报