vuex 2.*源码解析—— 花半小时就可以读懂vuex实现原理

本文章主要是拆解vuex v2.x的源码,快速看懂vuex1.0与vuex2.0的实现原理。

阅读准备

vuex的源码是基于es6语法,所以在阅读文章之前,你需要先掌握这些基础:

1.es6语法

2.vuex官方文档,像我一样英语阅读能力差的,还有中文文档

3.前端基础js、css、html不用说啦,vue基础用法掌握

下载源码

打开vuex gitHub 因为到我写这篇文章为止,vuex已经发展到了3.0, 3.*是使用ts语法编写,所以相对来说阅读成本又要进阶一层,我自己也还没完全阅读完,后期将会出vuex3.x的源码解析,还有typescript的相关文章。

github上选择 tag 我们选择一个最高版本的v2.5.0,clone到本地

完成后打开文件,可以看到:

其中src目录下就是vuex的源码实现,层级最多就2层,

源码解读

初始化

开始的地方: index.js

阅读的起点,当前要从index.js开始。 先看index.js主要输出了哪些东西:

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}
复制代码

让我们来大致认识下它们: store vuex 对外提供的状态存储类,用在项目中,它就是一个状态管理器,集中管理状态
install vuex提供的在项目中安装自身的方法
version vue版本号
mapState 为组件创建计算属性以返回 Vuex store 中的状态
mapMutations 创建组件方法提交 mutation
mapGetters 为组件创建计算属性以返回 getter 的返回值
mapActions 创建组件方法分发 action
createNamespacedHelpers 创建基于命名空间的组件绑定辅助函数
接下来,我会以vue项目中使用vuex的顺序来解析下vuex是如何发挥作用的,后面再展开vuex的其他属性与方法

vue使用vuex第一步: vue.use(Vuex)

install 为什么不是从store开始说,因为这个才是vuex插入到vue项目中的第一步哈,install与store都来自于store.js 文件

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)
}
复制代码

代码看起来很简单吧,install 里面主要做了什么: 1.判断是否有全局的Vue(即window.Vue), 如果没有,就赋值,确保我们的vuex只被install一次 Vue.use(vuex)就是执行了vuex的install方法;然后把vue挂载到全局对象上,这样子就可以在其他地方访问vue拉 最后一行的applyMixin 来看下具体代码:

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  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
    }
  }
}
复制代码

判断vue版本号,在vue的生命周期初始化钩子(vue1.0的版本是init, vue2.*的是beforeCreated)前执行一断代码,具体执行代码在vuexInit中。给vue实例添加一个store,判断判断vue实例的初始化选项this.options里面有没有store,有就直接赋值到实例属性store,这样子我们就可以在vue实例里面通过this.store,而不用辛苦地this.options.store或者this.options.parent.$store去访问了。

vue使用vuex第二步 new Vuex.Store

实例化store,我们来看Store的构造函数里面做了哪些事情:

export class Store {
    constructor (options = {}) {
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    if (process.env.NODE_ENV !== 'production') {
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `store must be called with the new operator.`)
    }
    ...
}
复制代码

确保install, 接下来是一些断言函数,不过js里面是没有这个断言函数,就是模拟断言函数。 接下来是一些属性赋值,以及把类方法dispatch, commit的this指向当前的store实例,代码就不贴了

再继续看:

installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
plugins.forEach(plugin => plugin(this))
复制代码

这三行代码做了很重要的事情。installModule安装options(创建store实例的时候,传入的模块) resetStoreVM 方法是初始化 store._vm,观测 state 和 getters 的变化; plugins 使用传入的插件
一个一个方法来具体查看

#### installModule

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  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)
    })
  }

  const local = module.context = makeLocalContext(store, namespace, path)

  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
复制代码

最多接收5个传入参数
store 表示当前 Store 实例
rootState 表示根 state
path 表示当前嵌套模块的路径数组
module 表示当前安装的模块
hot 当动态改变 modules 或者热更新的时候为 true

接下来就是对初始化传入的options进行注册安装。具体的什么实现,将会在后面的文章具体说明这边我们先了解它是什么动作,其中对module的注册值得关注下:

if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }
复制代码

因为store是单一的状态树,如果当需要管理的数据越来越多,这棵树就变得难以维护,所以引入module使得其结构更规范易维护。

resetStoreVM

执行完installModule,就执行resetStoreVM

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })
  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}
复制代码

给store设置一个私有变量_vm,它是vue实例,这个 _vm 对象会保留我们的 state 树,以及用计算属性的方式存储了 store 的 getters。

vuex初始化总结

Vuex 的初始化主要核心就是 installModule 和 resetStoreVM 函数。通过对 mutations 、actions 和 getters 的注册,state 的是按模块划分的,按模块的嵌套形成一颗状态树。而 actions、mutations 和 getters 的全局的。 对vuex的初始化有一个大致的了解之后,让我们来对我们在项目中用到的api来具体说明下是怎么实现的,也会将vuex初始化的时候一些未说明清楚的东西说清楚。

vue API详解

commit

先看下commit的函数定义:

  commit (_type, _payload, _options) {
    // check object-style commit
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    const entry = this._mutations[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
            console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    this._subscribers.forEach(sub => sub(mutation, this.state))

    if (
      process.env.NODE_ENV !== 'production' &&
      options && options.silent
    ) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
}
复制代码

dispatch

先看下dispatch的函数定义:

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

    const action = { type, payload }
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    this._actionSubscribers.forEach(sub => sub(action, this.state))

    return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
}

传入的参数: 
_type action 类型  
_payload 我们要更新的值
复制代码

subscribe

先来看下subscribe的函数定义

 subscribe (fn) {
    return genericSubscribe(fn, this._subscribers)
  }
复制代码

watch

 watch (getter, cb, options) {
    if (process.env.NODE_ENV !== 'production') {
      assert(typeof getter === 'function', `store.watch only accepts a function.`)
    }
    return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
  }
复制代码

getter 监听属性名
cb 对应的回调
options 可选,如{deep: true}之类的配置
响应式的接收一个getter返回值,当值改变时调用回调函数。getter接收state状态为唯一参数,并且options作为_vm.watch的参数。

转载于:https://juejin.im/post/5c21a28a5188255a8750c50f

你可能感兴趣的:(前端,javascript)