186

Vuex2.0源码解析 - 知乎专栏

 6 years ago
source link: https://zhuanlan.zhihu.com/p/29982815?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Vuex2.0源码解析

前端工程师/网球爱好者/爱音乐
本文通过一些简单流程图和文字说明介绍,用一种背离源码、更简单的方式去了解Vuex的原理。让我们在使用Vuex的时候明白背后的运行机制,方便我们更好的使用和调试解决问题。

1、什么是Vuex?

Vuex是一个专门为Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。通俗一点理解即:针对组件繁多交互复杂的单页面应用,Vuex提供了一种便利、准确和可预测的状态管理方式,方便组件之间的数据共享和修改。

2、Vuex核心概念

在理解Vuex源码之前,有一些Vuex的核心概念需要简单介绍一下,进而帮助更好的理解源码。

  • State:即状态,也就是Vuex核心管理的对象;
  • Getters:派生状态,对state的二次包装(例如:默认后端时间戳转化为日期格式),Getters里的方法所有组件都可以使用;
  • Mutations:所有状态的修改都是通过提交mutation,mutation类似事件,定义事件类型和回调函数,而回调函数就是进行状态修改的地方,状态修改一定是同步进行,从而确保状态修改可以被追踪到;
  • Actions:同样是进行状态修改,跟Mutations唯一不同的是进行异步的状态修改,本质是在回调提交Mutation;
  • Modules:为了解决状态树庞大进而store臃肿的问题,提出module概念,分化store到每个module,每个module都是一个小store。

3、Vuex2.0源码结构

本文介绍Vuex2.0源码部分,首先看下整个源码构成,如下图:

Vuex源码部分总共包括五个部分:

  • install:安装部分源码;
  • store:源码核心部分,本文重点介绍内容;
  • api:源码提供一些内部和外部api;
  • 辅助函数:语法糖,让我们在使用Vuex的时候书写更为简便;
  • plugin:提供一些默认插件,我们也可以自定义扩展插件的书写。

4、核心源码解析

4.1、install

安装部分源码的核心目的:给Vue注入一个store属性。

总体流程如下:

核心注入源码部分:

function vuexInit () {
    const options = this.$options
    // store 注入
    if (options.store) {
      this.$store = options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
}

4.2、store

store是整个Vuex的核心内容,其他几个部分的源码都是为了支撑store而存在的,在store中完成所有组件共享数据(状态)的注册、调用和修改方法。首先通过一个流程图看下store构造函数的构成:

store像是一个生态环境,整个应用里的每个组件都要在这个环境里注册,完成状态的可预测管理。注册之前需要进行环境检测,这像是进入这个生态环境注册的门票,或者说是通行证;达到注册标准之后,进行一些内部属性和方法的初始化工作,这像是为注册工作搭建一个“舞台”,接下来的注册工作都在这个“舞台”上完成。

4.2.1、installModule

安装模块部分源码主要完成模块的state、mutations、actions和getters的注册工作,先总体看下源码构成:

模块安装初期针对一些内部api的注册工作,接下来是state的更新工作,更新逻辑如下:

热更新:在注销一个module的时候,其逻辑是安装一个空module达到更新的状态,而这个更新即为热更新。热更新的状态修改单独处理。

在获取父节点的状态之后,进行一次基于父节点状态的commit提交修改即可完成模块的state更新到state tree。源码如下:

if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
}

接下来是一次store本地化的操作,即完成dispatch、commit、getters和state的本地化工作。本地化的目的是为了更好的进行数据方法操作。

mutations注册逻辑和actions注册逻辑几近相似,逻辑如下:

在store环境里有两个属性:_mutations和_actions分别用来存储模块定义的mutations和actions,依据当前模块的类型进行查找对应的内部属性对象,并且将模块对应的回调函数进行包装插入到对应属性对象里,到此即完成注册工作。二者差别在于包装回调函数处理上的不同,mutation直接将回调函数包装起来即可,action对于回调函数的结果进行Promise对象的处理,然后包装。

getters的注册逻辑如下:

getters是为了获取派生状态,因此命名的定义不允许重复,首先根据模块类型进行重名判定,判定的依据来自内部属性:_wrappedGetters,这个属性存储着整个应用的注册getter。接下来就是将模块的getters按照类型存入即完成注册。最后是一个子模块的递归调用的方法。

installModule只是完成了模块的注册工作,离我们可以使用这些状态还有一些需要处理的代码。

4.2.2、resetStoreVM

这个方法是对state和getters进行最后的使用处理,从而用户可以调用这些状态。源码逻辑如下:

核心内容是store._vm这样一个内部变量,本质上将注册后的state和getters作为新的数据源实例化一个Vue对象传递给store._vm,并且删除旧的store._vm。与此同时,定义store.getters.xxx=store._vm[xxx],从而完成使用getters的正确姿势。state的使用是由store内部提供了一个api,调用这个api返回store._vm.data.$$http://state.xxx,在更新store._vm之后,就可以访问这个模块的state。

mutations和actions使用通过store内部提供的两个重要api来实现,接下来介绍api部分。

5、api

5.1 commit 和 dispatch

这两个api分别是用来完成mutations和actions的使用工作。源码逻辑如下:

commit的逻辑是:从上面注册过的内部属性对象里依据参数拿到对应的mutations,然后通过_withCommit提交包装的回调函数即可,同时使用内部api subscribe进行状态修改追踪订阅。而dispatch则是通过参数拿到对应注册的actions,然后promise.all执行回调,回调里则是进行commit提交。

5.2 _withCommit

_withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
 }

这个内部api是每次提交状态修改的核心源码,其逻辑很简单,在每次执行状态修改的时候,保证内部属性_committing为true,而这个属性的默认初始值为false。这样在追踪状态变化的时候,如果_committing不为true,那么认为这次的修改是不正确的。 源码中还有一些内部api类似registerModule、unregisterModule、hotUpdate、watch以及subscribe等,在这里就不详细赘述。

6、辅助函数

Vuex除了上述提供的api以外,还提供了一些辅助函数,即操作 store 各种属性的一系列语法糖,目的是为了帮助我们使用Vuex的时候更方便。具体分为四个辅助函数:mapState、mapMutations、mapActions和mapGetters。为了更清晰的解释语法糖的包装形式,先看一下使用方法:

computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })

对state进行举例说明,以上代码是使用方法,从代码量上来看确实精简了不少,这样一个包装过程中重要的是将这个语法糖翻译成计算机可识别的js代码,因此在mapState的源码封装过程中实际做了很重要的一件事:将state注入到Vue实例的computed属性里,同时对于不同格式的语法糖进行函数形式的解构包装即可,从而还原成完整的执行函数。

同理可以对于mapMutations、mapActions和mapGetters进行解释:mapMutations和mapActions分别将commit 和 dispatch注入vue实例的methods里,而mapActions是将getters注入到vue实例的computed里,剩下的都是一些将语法糖书写改为函数形式即可,对于这几种的语法糖封装方式在源码上都大同小异。

7、plugin

Vuex2.0里提供了两个plugin:devtool和logger。分别是接入开发者工具和输出state变化的log插件;从源码角度去看插件逻辑没什么需要特别地说明,只是我们在开发插件的过程中可能需要对于内部提供的一些api和属性有更多的了解和掌握,例如subscribe 的内部API,例如travel-to-state、mutation钩子函数等,这样我们才可以根据自身的需求去开发相应的插件。

本文介绍了Vuex2.0的源码核心,整体的源码量不大,通过一些简易流程和介绍说明源码运行机制,让大家基本在脱离源码的基础上简略理解Vuex的原理。

本文介绍了源码里比较核心的部分,源码里还有一些辅助函数和内部api值得推敲和品味,还是希望大家有机会去阅读一下完整的源码,从而更完整的理解Vuex,也更容易帮助你进行debug;其次通过通读源码,整体地理解它的设计理念以及编码风格,帮助你日后在迈向技术高工路上进行实践学习。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK