Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vuex原理及应用场景分析 #12

Open
luoway opened this issue Aug 19, 2019 · 0 comments
Open

Vuex原理及应用场景分析 #12

luoway opened this issue Aug 19, 2019 · 0 comments

Comments

@luoway
Copy link
Owner

luoway commented Aug 19, 2019

Vuex原理

Vuex是什么?

把组件的共享状态抽取出来,以一个全局单例模式管理

通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。

这就是 Vuex 背后的基本思想,借鉴了 FluxReduxThe Elm Architecture。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库。

以上是Vuex官方的说明。

从使用者角度来讲,Vuex是管理组件共享数据的一种可选方案,该方案定义了一套state、getter、mutation、action概念及规则,并实现了模块化的数据管理。

Vuex的基本原理有三部分:全局共享状态、状态管理、模块管理。

全局共享状态

当我们需要用到Vuex管理的状态时,会这样用

//在vue实例中
this.$store.state.foo

this.$store就是被全局共享的对象,该对象是通过以下步骤

new Vue({
  store: new Vuex.Store({/*...*/})
})

创建并设置的。至此,Vuex.Store实例可以在Vue实例上访问到。

但是,在Vue实例内,还只能通过this.$root.store获取。实现Vue实例内全局共享的关键是Vue提供的安装插件方法:

Vue.use(Vuex)

这一步实际上会执行Vuex.install(Vue),该方法执行了以下内容:

Vue.mixin({
  beforeCreate(){
    const options = this.$options
    if($options.store){
      //初始赋值store
      this.$store = options.store
    }else if(options.parent && options.parent.$store){
      //使用父级Vue实例的$store属性(实现了Vue实例内全局共享)
      this.$store = options.parent.$store
    }
  }
})

状态管理

Vuex的状态管理分为4个概念:

  • state,集合状态树上的状态
  • getters,集合基于状态派生的只读计算函数
  • mutations,集合更改状态的方法
  • actions,集合触发mutation的具体函数

其中,state、getters是Vue组件内常用的读取状态的属性集合,mutations、actions是Vue组件内常用的更新状态的方法集合。

当我们使用Vuex来进行状态管理,会这样用:

const options = {
  state:{
    foo: 'foo'
  },
  getter:{
    foo2(state){
      return state.foo + '2'
    }
  },
  mutations:{
    setFoo(state, val){
      state.foo = val
    }
  },
  actions:{
    updateFoo(context){
      context.commit('setFoo')
    }
  }
}
new Vuex.Store(options)

实例化过程中,options的内容会被Vuex读取并进行以下操作:

创建命名空间,在后一节“模块管理”中具体说明;

遍历getters,包装为接收局部state局部getters全局state全局getters为参数的新函数并存储;

state与存储的getters一起,通过创建新的Vue实例

new Vue({
  data: {
    $$state: state
  },
  computed//由getters组成
})

进行存储。对外则通过设置取值器提供访问;

//设置getters的取值器
Object.defineProperty(store.getters, key, {
  get: () => store._vm[key]
})
//设置state的取值器
class Store {
  get state(){
    return this._vm._data.$$state
  }
}

遍历mutations,包装为接收局部state提交载荷的新函数并存储;

遍历actions,包装为返回Promise的新函数并存储。

如果存在子模块,则对子模块遍历并逐个递归这一过程。

模块管理

命名空间

Vuex使用“模块”来对store进行分割,模块内默认操作当前模块的状态,也可以访问全局状态,还可以通过命名空间访问其他模块的局部状态。

this.$store.dispatch('some/nested/module/foo')

这行代码即派发了三级模块moduleactions.foo()方法。

对于dispatch而言,它只是派发了名为'some/nested/module/foo'的已存储的方法,而方法命名的的过程是在Vuex实例化过程中安装模块时完成的。

installModule(store, rootState, path.concat(key), child, hot)

在对子模块进行遍历递归安装过程中,只有模块路径path和配置对象child会发生变化,path是命名的依据。

局部状态

Vuex的状态管理始终是统一的、全局的,其创建的状态树是唯一的。“局部状态”是访问状态树的一块局部,即分别对state、getters、commit、dispatch进行修改:

  • 通过Object.defineProperty设置local.statelocal.getters的取值器
  • 创建新的local.commit()local.dispatch()方法,闭包存储当前命名空间,自动地为传入的参数加上该命名空间

得到的local对象会在上一节mutationsactions包装函数时作为入参使用。

更多具体实现可以查看笔者上一篇《Vuex源码解析》

应用场景

当你需要在Vue组件中管理共享状态时,就可以使用Vuex。

共享状态的目的就是为了进行通信。Vue组件通信方式默认是父子组件间通信,非父子组件通信要么转换成多级的父子组件间通信,要么使用全局变量进行状态管理,Vuex提供了第三种方案:创建一个在Vue实例间共享的单例进行状态管理。

下面用伪代码的方式描述下三者的区别:

//多级的父子组件通信
<A> $on('receiverA')
  <B> $on('receiverB', ()=>$emit('receiverA'))
    <C> $emit('receiverB')

//全局变量
window.Bus = new Vue()
Bus.$on('receiverA')
<C> Bus.$emit('receiverA')

//Vuex
new Vuex.Store({
  actions:{
    receiverA
  }
})
<C> this.$store.dispatch('receiverA')

多级的父子组件通信、全局变量、Vuex,三种方式都能达到Vue组件通信的目的。

多级的父子组件通信显然不是一个合适的解决方案,因为修改通信过程要改多处功能不相关的组件代码。全局变量、Vuex都能够脱离组件层级地实现通信,但是全局变量由于简单、缺少约束,随着项目的持续迭代,容易出现重复命名导致的问题,Vuex则没有这样的问题,因此是三种方式中功能比较完善的方式。

然而,完善的功能是建立在使用体验良好的API封装上的,未必是性能优先的。尽管Vuex小心地只维护唯一状态树,但API封装的过程中的遍历、递归、Object.defineProperty、函数包装等等行为,不可避免地产生了性能消耗,这种消耗在特定场景下会表现出性能问题。

坑:模块动态注册

模块动态注册的实现逻辑很简单:

class Store {
  registerModule (path, rawModule, options = {}) {
    if (typeof path === 'string') path = [path]

    this._modules.register(path, rawModule)
    installModule(this, this.state, path, this._modules.get(path), options.preserveState)
    // reset store to update getters...
    resetStoreVM(this, this.state)
  }
}

坑的关键在于resetStoreVM()方法,该方法内容是创建新的Vue实例来进行状态管理。

function resetStoreVM (store, state, hot) {
  //...

  // 使用一个Vue实例来存储状态树
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  
  //...
}

每次执行resetStoreVM()都将执行vue实例化过程以创建一棵新的状态树,替代原有状态树,每个动态注册模块的过程都会重复这一步。
在Vue实例化过程中,state对象会被递归地使用Object.defineProperty()转换为响应式,尽管Vuex通过直接将对象赋值给data以复用原有状态树进行优化,这仍然是高耗性能的,意味着你最好避免多次调用resetStoreVM()方法,而应当考虑合并多个需要动态注册的模块以减少调用次数。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant