10 | 【阅读Vue2源码】Vuex的实现原理

前言

本篇文章分析Vue2的生态库Vuex,本次选择Vuex的版本为3.x的最新版本3.6.2。

为什么选择3.6.2的版本呢?是因为我接触的大多数项目都是三次年前创建的项目,甚至更久远,大多数Vuex的版本都是3.x的,而4.x的Vuex增加了对vue3的支持,实现方式上有一些变化,所以选择3.x版本最新的3.6.2版本作为分析对象。

我们都知道Vuex是Vue的状态管理库,并且更改store中的state就可以更新视图,那么Vuex是怎么做的state更新就更新视图呢?本篇文章就来研究这个主题。

相关链接:

搭建阅读环境

  1. 在github上folk一个vuex的仓库
  2. 拉取自己folk的仓库到电脑本地
  3. 基于tag v3.6.2新建一个分支,例如我的分支为alanlee/read-source/v3.6.2
  4. 打开项目,安装依赖,推荐使用yarn安装依赖
  5. 调整rollup打包配置文件rollup.config.js,在output对象中增加sourcemap: true
  1. 执行打包命令npm run build:main,打包好后dist文件夹就多了对应的xxx.map的源码映射文件,方便调试
  2. 在examples文件中选择一个示例项目作为分析对象,这里我选择todomvc
  1. 修改示例代码中store.js引入的Vuex为dist文件夹下的打包出来的vuex
  1. 修改webpack.config.js的配置,增加devtool: 'eval-source-map',打包出源码映射文件
  2. 运行示例代码,执行npm run dev
  3. 最后在示例代码中打断点,然后进行调试分析源码

源码分析

在进行源码分析之前,先准备一个简单的demo作为分析对象,这里选择的是源码中自带的examples的todomvc,自己改造了一下

Demo示例代码

app.js

javascript 复制代码
import Vue from 'vue'
import App from './components/SimpleApp.vue'
import Vuex from '../../../dist/vuex'

const storeConfig = {
  state: {
    hello: 'AlanLee'
  },
  mutations: {
    changeHello(state, newVal) {
      state.hello = newVal
    }
  },
  actions: {
    updateHello(ctx, payload) {
      ctx.commit('changeHello', payload)
    }
  }
}

const store = new Vuex.Store(storeConfig)
Vue.use(store)

new Vue({
  store, // inject store to all children
  el: '#app',
  render: h => h(App)
})

SimpleApp.vue

javascript 复制代码
<template>
  <div>
    SimpleApp:<span @click="changeText">{{ hello }}</span>
  </div>
</template>

<script>
export default {
  name: 'SimpleApp',
  computed: {
    hello () {
      return this.$store.state.hello
    }
  },
  methods: {
    changeText () {
      // this.$store.commit('changeHello', Math.random())
      this.$store.dispatch('updateHello', Math.random())
    }
  }
}
</script>

这段代码主要展示的是vuex的简单使用方式

  1. 定义Vuex的statemutationsactions
  2. 在vue组件的方法中调用$storecommit/dispatch派发actions更改state,触发视图更新

Vuex的核心概念

Vuex的核心概念有:

  • state
  • mutations
  • actions
  • getters
  • module

其中我们主要关注statemutationsactions即可

调试源码分析

install

因为Vuex本质上是一个vue的插件,所以需要提供install方法

javascript 复制代码
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (__DEV__) {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

主要实现在applyMixin,来看看其实现

applyMixin

javascript 复制代码
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // 兼容vue1的老代码,不是我们分析的重点,忽略代码
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  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
    }
  }
}

逻辑也很简单:

  1. applyMixin调用Vue.mixin,把vuexInit赋值给beforeCreate,相当于Vue组件初始化时会先执行vuexInit
  2. vuexInit就是从options中取用户配置的store,赋值给组件实例的$store,所以我们在Vue组件实例中可以通过this.$store.state.xxx来访问store中的数据

Store

Store的初始化是在new Vuex.Store()时开始的,所以我们找到入口为Store的定义

/src/store.js

javascript 复制代码
export class Store {
  constructor (options = {}) {
    // ...

    const {
      plugins = [],
      strict = false
    } = options

    // 做一些初始化
    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()
    this._makeLocalGettersCache = Object.create(null)

    // bind commit and dispatch to self
    const store = this

    // 包装一下dispatch, commit方法
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // strict mode
    this.strict = strict

    const state = this._modules.root.state

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], this._modules.root)

    // 核心逻辑 实现state的响应式
    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)

    // ...
  }

  get state () {
    return this._vm._data.$$state
  }

  set state (v) {
    if (__DEV__) {
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }

  commit (_type, _payload, _options) {
    // ...
  }

  dispatch (_type, _payload) {
    // ...
  }

  subscribe (fn, options) {
    // ...
  }

  subscribeAction (fn, options) {
    // ...
  }

  watch (getter, cb, options) {
    // ...
  }

  replaceState (state) {
    // ...
  }

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

  unregisterModule (path) {
    // ...
  }

  hasModule (path) {
    // ...
  }

  hotUpdate (newOptions) {
    // ...
  }

  _withCommit (fn) {
    // ...
  }
}

可以看到store中定义了十几个属性和方法,其中我们主要分析:

  • constructor
  • state
  • commit
  • dispatch
  • resetStoreVM,实现state的响应式

State响应式的实现

实现细节在resetStoreVM函数中

javascript 复制代码
function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldVm.
    // using partial to return function with only arguments preserved in closure environment.
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  const silent = Vue.config.silent
  Vue.config.silent = true
  // 核心逻辑,new了一个Vue,只提供了data和computed,将state作为data,以实现state的响应式
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

实现逻辑也很简单,核心逻辑就是:新new了一个 Vue,只提供了datacomputed state作为 data,以实现state的响应式。

那么问题来了,我们的主应用本身是new了一个Vue,那么用了Vuex后,store里又new了一个Vue,就算state的响应式是通过store中的新的Vue实例提供的,理论上来讲,state发生变化,应该是触发当前(store中)Vue实例中的视图更新才对呀,而且这个Vue实例并没有提供模板,也没有$mount挂载元素。

那么Vuex又是怎么做到:store中的Vue实例的data更新,去触发我们主应用的视图更新呢?

实现视图更新

其实实现视图的更新也很简单,Store也不需要做什么特殊的处理,因为Vue已经实现了这个功能。

  1. 其实就是在模板中访问store时<span>{{ $store.state.hello }}</span>,会触发defineReactive中设置的Object.defineProperty的get
  1. 在get中触发dep.depend()收集依赖,收集依赖时会把当前的这个Vue实例的渲染函数作为Watcher的回调函数
  1. 当更新state的值时,触发set,执行depend收集的依赖(Watcher的回调函数),也就是主应用Vue实例的渲染函数,所以主应用Vue对应的视图也会更新
  1. 简单来讲,就是Store里的Vue收集的依赖是主应用Vue的更新函数

八卦一下:

Vuex为什么叫Vuex?难道就是因为store里面new了一个Vue,所以叫做Vue的扩展?为什么不叫Vue Store呢?

commit/dispatch

这两个API就是用来改变state的,commit是同步执行,dispatch是异步执行actions。

小总结

  1. Vuex是在初始化vue时,混入一个函数,给当前Vue组件实例增加一个$store属性,所有在vue组件中可以通过this.$store.state.xxx访问store中的数据
  2. 通过new了一个Vue来实现state的响应式
  3. state在主应用中的模板中访问,所以state的响应式收集的依赖是主应用Vue实例的渲染函数,当state更新时,执行收集回调函数(主应用Vue实例的更新函数),所以可以更新视图

自己实现简单版Vuex

依葫芦画瓢,按照Vuex的实现方式,我们自己可以实现一个极简版的Vuex

  1. 定义Store类,定好框架
  • constructor

    • 接收options参数
  • 属性

    • state
  • 方法

    • install
    • commit
    • applyMixin
    • commit
    • dispatch
javascript 复制代码
class Store {
  state = {}

  constructor(options = {}) {
    this.state = options.state
  }

  install(vm) {
    this.applyMixin(vm)
  }

  applyMixin(Vue) {

  }

  commit(handler, payload) {

  }

  dispatch(action, payload) {

  }
}
  1. 实现插件需要的install方法,其实就是调用applyMixin,然后实现applyMixin方法,Vue.use(Vuex)时会执行install方法
javascript 复制代码
install(vm) {
  this.applyMixin(vm)
}

applyMixin(Vue) {
  // 缓存一份原来的_init方法
  const _init = Vue.prototype._init

  // 定义vuex初始化方法
  function vuexInit() {
    const options = this.$options
    // 赋值store到$store
    if(options.store) {
      this.$store = options.store
    } else if(options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }

  // 重新赋值_init
  Vue.prototype._init = function(options = {}) {
    // 混入vuexInit方法
    Vue.mixin({ beforeCreate: vuexInit })
    // 执行原来的_init方法
    _init.call(this, options)
  }
}
  1. 在constructor中实现state的响应式
javascript 复制代码
class Store {
  state = {}

  constructor(options = {}) {
    this.state = options.state
    this.options = options
    const store = this
    // 实现state的响应式
    store._vm = new Vue({
      name: 'DemoVuex',
      data: {
        // 你只管放数据到data,剩下的交给Vue
        $$state: options.state
      }
    })
  }

  // ...
}

OK,响应式实现了,很简单,直接new一个Vue就,把state放进data里就行了,你只管放数据到data,剩下的交给Vue。

  1. 实现commit
javascript 复制代码
// commit接收两个参数,handler-定义的mutations的名字,payload-提交的数据
commit(handler, payload) {
  // 取出mutations
  const {mutations = {}} = this.options
  // 执行取出mutations
  mutations[handler].call(this, this.state, payload)
}

commit接收两个参数

  • handler-定义的mutations的名字
  • payload-提交的数据
  1. 实现dispatch
javascript 复制代码
// 实现思路跟commit一样,使用Promise包裹了一下
// 接收两个参数,action-定义的action的名字,payload-提交的数据
dispatch(action, payload) {
  return new Promise((resolve) => {
    const {actions = {}} = this.options
    resolve(actions[action].call(this, this, payload))
  })
}

实现思路跟commit一样,使用Promise包裹了一下

dispatch接收两个参数

  • action-定义的action的名字
  • payload-提交的数据

效果演示

完整代码

代码仓库

index.html

html 复制代码
<!doctype html>
<html data-framework="vue">

<head>
  <meta charset="utf-8">
  <title>Vue.js • Simple Demo For Vuex</title>
  <style>
    [v-cloak] {
      display: none;
    }
  </style>
</head>

<body>

  <section id="app">
    <h1 @click="changeHello">{{$store.state.hello}}</h1>
  </section>

  <script src="../../dist/vue.js"></script>
  <script src="store.js"></script>
  <script src="app.js"></script>
</body>

</html>

app.js

javascript 复制代码
const storeConfig = {
  state: {
    hello: 'AlanLee'
  },
  mutations: {
    changeHello(state, newVal) {
      state.hello = newVal
    }
  },
  actions: {
    updateHello(ctx, payload) {
      ctx.commit('changeHello', payload)
    }
  }
}

const store = new Store(storeConfig)
Vue.use(store)

var app = new Vue({
  name: 'SimpleDemo_Vuex',
  store,
  methods: {
    changeHello() {
      // this.$store.commit('changeHello', Math.random())
      this.$store.dispatch('updateHello', Math.random())
    }
  }
})

app.$mount('#app')

console.log('alan-> app', app)
window.appVue = app

store.js

javascript 复制代码
class Store {
  state = {}

  constructor(options = {}) {
    this.state = options.state
    this.options = options
    const store = this
    // 实现state的响应式
    store._vm = new Vue({
      name: 'DemoVuex',
      data: {
        // 你只管放数据到dara,剩下的交给Vue
        $$state: options.state
      }
    })
  }

  install(vm) {
    this.applyMixin(vm)
  }

  applyMixin(Vue) {
    // 缓存一份原来的_init方法
    const _init = Vue.prototype._init

    // 定义vuex初始化方法
    function vuexInit() {
      const options = this.$options
      // 赋值store到$store
      if(options.store) {
        this.$store = options.store
      } else if(options.parent && options.parent.$store) {
        this.$store = options.parent.$store
      }
    }

    // 重新赋值_init
    Vue.prototype._init = function(options = {}) {
      // 混入vuexInit方法
      Vue.mixin({ beforeCreate: vuexInit })
      // 执行原来的_init方法
      _init.call(this, options)
    }
  }

  // commit接收两个参数,handler-定义的mutations的名字,payload-提交的数据
  commit(handler, payload) {
    // 取出mutations
    const {mutations = {}} = this.options
    // 执行取出mutations
    mutations[handler].call(this, this.state, payload)
  }

  // 实现思路跟commit一样,使用用Promise包裹了一下
  // 接收两个参数,action-定义的action的名字,payload-提交的数据
  dispatch(action, payload) {
    return new Promise((resolve) => {
      const {actions = {}} = this.options
      resolve(actions[action].call(this, this, payload))
    })
  }
}

总结

其实Vuex的实现原理很简单,Vuex的代码其实也很少。

实现原理就是给state去new了一个Vue放到data中管理。

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax