likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 40

前言

本篇文章分析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

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

  1. 执行打包命令npm run build:main,打包好后dist文件夹就多了对应的xxx.map的源码映射文件,方便调试
  2. 在examples文件中选择一个示例项目作为分析对象,这里我选择todomvc

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

  1. 修改示例代码中store.js引入的Vuex为dist文件夹下的打包出来的vuex

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

  1. 修改webpack.config.js的配置,增加devtool: 'eval-source-map',打包出源码映射文件
  2. 运行示例代码,执行npm run dev
  3. 最后在示例代码中打断点,然后进行调试分析源码

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

源码分析

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

Demo示例代码

app.js

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

<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,触发视图更新

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

Vuex的核心概念

Vuex的核心概念有:

  • state
  • mutations
  • actions
  • getters
  • module

其中我们主要关注statemutationsactions即可

调试源码分析

install

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

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

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

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函数中

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,只提供了datacomputedstate作为data,以实现state的响应式。

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

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

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

实现视图更新

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

  1. 其实就是在模板中访问store时<span>{{ $store.state.hello }}</span>,会触发defineReactive中设置的Object.defineProperty的get

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

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

  1. 在get中触发dep.depend()收集依赖,收集依赖时会把当前的这个Vue实例的渲染函数作为Watcher的回调函数

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

  1. 当更新state的值时,触发set,执行depend收集的依赖(Watcher的回调函数),也就是主应用Vue实例的渲染函数,所以主应用Vue对应的视图也会更新

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

  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
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方法
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的响应式
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
// 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
// 实现思路跟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-提交的数据

效果演示

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

完整代码

代码仓库

index.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

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


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中管理。

转载自:https://juejin.cn/post/7274555781486788665
评论
请登录