vue2源码学习(2) - 手写 vuex

197 阅读3分钟

1. vuex 是什么?

Vuex 集中式 存储管理应⽤的所有组件的状态,并以相应的规则保证状态以 可预测 的⽅式发⽣变化。 为什么是可预测?可预测 是为了能在改变前或改变后进行一些操作。

什么样的数据会存放在 vuex 的 store 中呢?

  • 一般 跨组件共享的数据 才会存放到 vuex 中
  • 组件之间的状态共享

先来看一下下面这张图(大家应该都不陌生) —— 单向数据流的图: image.png

一般情况下单向数据流会更加简洁,更容易找问题,如果数据中出现问题更加容易调试。

vuex 中有个非常特殊的类叫 store,所有数据都在 store 这个类中去存储。存储器 store 中的状态(state)是响应式的,所以当用户触发 mutations 时,修改 state 的数据,会让使用它的组件重新 render。

2. vuex 的使用示例

安装依赖:npm install vuex

import Vue from 'vue';
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {},
  // 改变状态只能通过 mutations 
  mutations: {},
  // actions:可以做一些非常复杂的业务逻辑
  actions: {},
  modules: {},
})

src/store/index.js

import Vue from 'vue';
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    counter: 1,
  },
  mutations: {
    // state 从何而来?
    add(state){
      state.counter++
    },
  },
  actions: {
    syncAdd({ commit }){
      setTimeout(() => {
        commit('add')
      }, 1000)
    },
  },
  modules: {},
})

访问全局状态:

<template>
  <div>
    <p @click="$store.commit('add')">{{ $store.state.counter }}</p>
    <p @click="$store.dispatch('syncAdd')">{{ $store.state.counter }}</p>
  </div>
</template>

3. 需求:

  • 实现 Store 类:
    • 需要有一个响应式状态 state
    • 需要一个改状态的 commit() 方法
    • 复杂业务逻辑的 dispatch() 方法
    • getters
  • 挂载 store(同挂载store (同 挂载router 一样,可以参考 vue2源码学习(1) - 手写 vue-router

4. 开始

在开始前先准备两个文件:src/myStore/index.js 、 src/myStore/myVuex.js

4.1 src/myStore/index.js

import Vue from 'vue'
import Vuex from './myVuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    counter: 1
  },
  mutations: {
    // state从何而来?
    add(state) {
      state.counter++
    }
  },
  actions: {
    // context: {commit, dispatch, state, rootState}
    add({commit}) {
      setTimeout(() => {
        commit('add')
      }, 1000);
    }
  },
})

4.2 src/myStore/myVuex.js

// 1.实现插件
let Vue
class Store {
  constructor(options) {
    this._mutations = options.mutations
    this._actions = options.actions
    // 需要对options.state做响应式处理
    // Vue.util.defineReactive()
    this._vm = new Vue({
      data() {
        return {
          // $$state是不会代理到_vm上的
          $$state: options.state
        }
      },
    })

    this.commit = this.commit.bind(this)
    this.dispatch = this.dispatch.bind(this)
  }
  get state() {
    return this._vm._data.$$state
  }
  set state(v) {
    console.error('please use replaceState reset state');
  }

  // 修改状态
  // store.commit('add', 1)
  commit(type, payload) {
    const entry = this._mutations[type]
    if (!entry) {
      console.error('unknown mutation: ' + type);
      return
    }
    entry(this.state, payload)
  }

  dispatch(type, payload) {
    const entry = this._actions[type]
    if (!entry) {
      console.error('unknown action: ' + type);
      return
    }
    entry(this, payload)
  }
}
function install(_Vue) {
  Vue = _Vue

  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store
      }
    }
  })
}
export default { Store, install };

4.3 对 myVuex.js 内容进行简单解说

先看一下 vuex 使用结构:

Vue.use(Vuex)

export default new Vuex.Store({
})

以此可以看出 myVuex.js 插件的名字叫 Vuex 而不是叫 store。

根据 vue2源码学习(1) - 手写 vue-router 的学习,可以得知 myVuex 插件的基本结构应该是:

class Store {
  constructor(options){
  }
}

function install(_Vue) {
}

export default {
  Store,
  install,
}

4.3.1 挂载 $store

Vue.mixin({
  beforeCreate(){
    if(this.$options.store) {
      Vue.prototype.$store = this.$options.store;
    }
  }
})

vue2源码学习(1) - 手写 vue-router 中挂载 $router ,这里就不再赘述。

4.3.2 state 响应式处理

根据需求分析, Store 类需要有一个响应式状态 state,这里对 options.state 响应式处理如下:

  constructor(options) {
    
    this._vm = new Vue({
      data() {
        return {
          // $$state是不会代理到_vm上的
          $$state: options.state
        }
      },
    })

  }

1、为什么要这么处理?是否可以换成其他方案?

  • 方案一:在 vue2源码学习(1) - 手写 vue-router 中有提到过 Vue.util.defineReactive() 可以创建一个响应式数据;
  • 方案二:在 data 中直接 return options.state;
    •   constructor(options){
            this.state = new Vue({
              data(){
                return options.state;
              },
            })
          }
      

2、方案二为什么可以实现响应式? 先来看一段大家都很熟悉的代码:

const app = new Vue({
  el: '#app',
  router,
  store,
  data(){
    return {
      foo: '哈哈哈',
    };
  },
  components: { App },
  template: '<App/>'
})

new Vuedata return 的这个对象直接会做响应式处理。

vue 在做响应式处理的时候还会做一层代理,直接把它挂载到将来创建出来的那个 vue 实例里。

所以这里的 this.state 就是一个 vue 实例,vue 实例中一定会挂着 data 中的所有 key。

因为 vue 把 data 下的 key 都代理到了 vue 实例上,所以一般会这么使用:app.foo;而不会这么使用:app.$data.foo

3、方案二的问题

用户直接对 tihs.state 的数据进行修改,这会导致状态的变更将变得不可预测,我们希望用户只提交 commit。

那知道问题了,怎么解决呢? 可以将这个 vue 实例隐藏起来,不直接暴露出去。把 this.state 改为 this._vm

这时我们可以做一个存取器:

constructor(options = {}) {
  this._vm = new Vue({
    data: {
      state: options.state
    }
  });
}
// 存取器
get state() { 
  return this._vm._data.$$state
}
set state(v) {
  console.error('please use replaceState to reset state');
}

当用户想通过 this.state 去访问的时候,实际上 return 的是 this._vm 里面 data 中的值。

我也不希望用户直接通过 _vm 看到 data 中的数据,那就得把数据藏起来,不做代理,不做代理的方式:

constructor(options = {}) {
  this._vm = new Vue({
    data: {
      state: options.state
    }
  });
}
// 改为
constructor(options = {}) {
  this._vm = new Vue({
    data: {
      // $$state 是不会代理到 _vm 上的,
      // 就是说在 key 前面加两个 $$ 可以让其不代理到 _vm 上
      $$state: options.state
    }
  });
}

vue 规定在 key 前面加上 $$ 或者 下划线 可以让其不代理到 vue 实例上

这样便可将 data 下面的响应式数据藏起来,不让用户这么容易找到。这样可以很有限制性的去访问,访问 this.state 的时候,得到的数据是我们想让其访问到的 this._vm._data.$$state。当然如果想接触还是可以通过 this._vm._data.$$state 或者 this._vm.$data.$$state

如果用户直接 set state,应该禁止并提示用户这个行为是错误的,而不是去变更状态,想改状态的话只有一种方式:

// 修改状态
commit(type, payload) {}

必须通过提供的 API 去修改,这才是可预测。

4.3.3 commit

我们平时使用 commit 是这么使用的:store.commit('add', 1)。关于它的参数:

  • type:提交 mutations 的类型
  • payload:参数载荷,执行 mutations 的方法的参数

commit 的作用其实就是让 mutations 的方法执行一次,可以在 constructor 中先将 mutations 和 action 先保存下来,然后在 commit 中可以直接 this._mutations[type] 拿 mutations 里面的函数,如果这个函数不存在则直接报错。

constructor(options = {}) {
  this._mutations = options.mutations
  this._actions = options.actions
}

// 修改状态
commit(type, payload) {
  const entry = this._mutations[type]
  if(!entry) {
    console.error('unknown mutation: ' + type)
  }
  entry(this.state, payload)
}

4.3.4 dispatch

dispatch 与 commit 如出一辙

constructor(options = {}) {
  this._mutations = options.mutations
  this._actions = options.actions
}

dispatch(type, payload) {
  const entry = this._actions[type]
  if(!entry) {
    console.error('unknown action: ' + type)
  }
  entry(this, payload) // 第一个参数可不可以传 this ?
}

先看看 actions 中的上下文 context 是一个什么对象,context: {commit, dispatch, state, rootState} 包含了 commit 提交、dispatch 派发,这样可以在 actions 进行不同的业务逻辑组合。

上面这段代码 entry 的第一个参数能传 this 吗?因为这里的 this 正好有 commit、dispatch 等。答案当然是不可以的,因为 this 的指向问题,在 commit 的时候内部的 this 已经丢失了。

那怎么解决这个 this 的指向问题呢? 可以在 constructor 锁死 commit 和 dispatch。

constructor(options = {}) {

  // 锁死 this
  this.commit = this.commit.bind(this)
  this.dispatch = this.dispatch.bind(this)
}

对 myVuex.js 内容进行简单解说就到这里,欢迎大家指教~