likes
comments
collection
share

来来来,我们手写个简单的Vuex吧,帮助了解Vuex是如何实现全局状态管理的

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

前言

作为前端开发,我们经常会遇到使用vuex的情况,那么大家有没有好奇vuex是怎么实现的呢,又是如何实现全局状态管理的能力的呢? 下面我来手撕一下vuex的实现,帮助你更好的理解vuex,并帮助你在后续的开发中遇到相关问题可以找到破局之法。

基础知识

在认识vuex之前我们需要了解下,什么是vuex,以及如何使用vuex,实现时涉及的api有哪些?

什么是vuex

vuex是为vue.js创建的一个状态管理工具。

我们可以在vuex中暂存一些组件状态,然后通过vuex使得各个组件获取这些组件状态,并支持在任意组件中更新组件状态,同步到其他的使用了这些组件状态的组件中。

如何使用vuex

在使用vuex之前我们需要初始化一个store文件

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: { // 数据源
      count: 1
  }, 
  getters: { // store的计算属性
    doneTodos (state) {
          return state.count + 1
    }
  }, 
  mutations: { // 更改数据源
      increment (state) {
          // 变更状态
          state.count++
      }
  }, 
  actions: { // 类似mutations,通过提交mutations更改数据源,但可以包含异步操作
    increment (context) {
          context.commit('increment')
    }
  }, 
  modules: {}, // 模块
});

在入口文件处再使用这个store文件

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

new Vue({
  router,
  store, // 使用初始化的store文件
  render: (h) => h(App),
}).$mount("#app");

vuex中主要包括state,getters,mutations,actions等功能,modules是模块但是通常使用情况不多,这里按下不表。

state

state代表的是数据源,我们使用vuex的时候读取的就是这里的参数,比如:

this.$store.state.count // 读取到上面的count字段

getters

getters可以看成是store的计算属性,我们可以定义一个函数将计算后的结果返回出去。这个函数第一个参数是state ,收store中内容也可以接收其他getter作为参数。比如:

 this.$store.getters.doneTodos // 读取上面count + 1的结果

mutations

mutations是更改vuex中状态的唯一方法,提供一个事件状态及回调,可以通过调用回调函数的方式去更新组件状态,只能包含同步操作,比如:

 this.$store.commit('increment') // 调用mutations的increment,使得count++

actions

actions同样是用于变更vuex状态,但不同的是actions通过提交mutations去更新vuex状态,actions可以包含异步操作,通过dispatch触发,比如:

 this.$store.dispatch('increment') // 通过action的increment 去触发 mutations的increment

涉及的知识

Vue.util.defineReactive

defineReactive这个方法接收三个参数

  • 第一个参数:要给哪个对象添加属性
  • 第二个参数:要给指定的对象添加什么属性
  • 第三个参数:要给这个属性添加什么值

通过这个方法,可以快速将一个数据变成双向绑定的数据

Vue.set

set这个方法接收三个参数

  • 参数说明:

    • target :要更改的数据源(可以是对象或数组)
    • propertyName/index:属性名或索引
    • value:新值

可以向响应式对象中加新的property,并保证这个property是响应式的

Vue.use()

我们在使用vuex的时候都知道,我们会先执行vue.use(),这一步做了啥呢?

  • 如果入参是一个对象,那就执行这个对象下的 install 方法
  • 如果入参是一个函数,那就直接执行这个函数

不妨联想下,为什么我们安装组件库的时候,可以通过vue.use(xxComponents)这种方式去导入按需引入组件。

实际上我们在vue.use(xxComponents)的时候,其实是传进去了一个对象。

import Vue from "vue";
import xxComponents from "./xxComponents";
export const xxComponents = {
    install() {
        Vue.components(xxComponents.name, xxComponents)
    }
}

在这里执行了install方法把xxComponents挂载到了全局

new Vue()

我们在使用Vue的时候都会用new Vue去进行初始化操作,包括执行init方法等。

其实new Vue()做了一个很主要的事情,就是将传入的选项挂载到了$option上,如下:

new Vue({
  router,
  store, // 使用初始化的store文件
  render: (h) => h(App),
}).$mount("#app");

后续store和router都会被挂载到this.$option下 相当于 this.$option.store 可以获取到 我们传入的 store

实现Vuex

初始化Vuex基本结构及挂载Vuex

在上面的例子中我们可以看到

我们这里抛出了一个实例化的Vuex.Store的对象。

import Vue from "vue";
import Vuex from "Vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  ...
});

然后在入口文件处,将store作为参数传入new Vue中

new Vue({
  router,
  store, // 使用初始化的store文件
  render: (h) => h(App),
}).$mount("#app");

到这里其实Vuex就已经完成了初始化操作。

下面我们来完成Vuex的初始化操作,并介绍初始化的时候Vuex做了什么。

一、创建自己的Vuex文件

需要创建一个Vuex,其中包含用于创建store的类。

myVuex.js

// 创建一个store类,用于初始化Vuex
class store {}

// 抛出自己的vuex
export default {
  store,
};

二、创建实例化的Vuex.Store的对象,并传入到new Vue中

因为在使用Vuex之前我们都需要实例化Vuex.Store对象,然后将结果传入到new Vue中,挂载到全局的\$option下。 这里创建一个Store.js 用于抛出 实例化Vuex.Store对象。

Store.js

import myVuex from "./myVuex";

export const myStore = new myVuex.Store({});

在main.js中将Store.js导入,并传入到new Vue中,这样myStore就挂载到全局的$option下。

main.js

import { myStore } from "./Store";

new Vue({
  router,
  myStore, // 传入初始化的store对象
  render: (h) => h(App),
}).$mount("#app");

在这一步之后实例化Vuex.Store对象就挂载到全局的\$option下了。 (重点)

三、创建install方法全局挂载store

因为我们知道Vuex的数据方法都可以通过this.$store去获取,因此我们需要想办法把 this.$store 挂载到全局让所有组件都可以访问到。

而且我们之前已经将 myStore 挂载到 全局的\$option下了

因此这里可以用 Vue.use 的方法, 去实现这个功能。

myVuex新增install方法

因为在Vue.use的时候,如果参数是对象,那么会执行参数的insatll方法,因此我们可以通过insatll方法将this.\$store挂载上去。

这里创建一个install方法,通过Vue.mixin去全局注入到每一个组件中,这样在组件beforeCreate的时候就会去将我们创建myStore对象存放到this.$store中

myVuex.js

import Vue from "vue";

const install = () => {
  Vue.mixin({
    beforeCreate() {
      // Vue创建实例的时候是递归创建的,因此先创建父组件,再创建子组件
      // 对于根组件而言,默认 myStore 就存在 this.$options下
      if (this.$options && this.$options.myStore) {
        this.$store = this.$options.myStore;
      }
      // 对于子组件而言,默认没有store,此时存储父组件的store即可
      else {
        this.$store = this.$parent.$store;
      }
    },
  });
};

// 创建一个store类,用于初始化Vuex
class Store {
  constructor(options) {
  }
}

// 抛出自己的vuex
export default {
  install,
  Store,
};


main.js中调用Vue.use()

因此我们在main.js中调用下Vue.use(myVuex) ,通过install方法,用Vue.mixin去全局注入到每一个组件中,这样在组件beforeCreate的时候就会去将我们创建myStore对象存放到this.$store中。

main.js

import { myStore } from "./Store";
import myVuex from "./myVuex";
import Vue from "vue";

Vue.use(myVuex) // 调用install方法全局混入

new Vue({
  router,
  myStore, // 传入初始化的store对象
  render: (h) => h(App),
}).$mount("#app");

到这里Vuex的基本初始化就完成了。

四、简单验证

将上述代码运行可以得到结果

展示 来来来,我们手写个简单的Vuex吧,帮助了解Vuex是如何实现全局状态管理的 打印

来来来,我们手写个简单的Vuex吧,帮助了解Vuex是如何实现全局状态管理的

其中 a: 1 是在构造函数中随意设置的值

来来来,我们手写个简单的Vuex吧,帮助了解Vuex是如何实现全局状态管理的

所以通过上面我们可以知道,已经完成了实例化myStore对象的全局挂载,这时候你就可以在任何组件中使用实例化myStore对象下的属性了。

完善store类,实现包括state,getters在内的功能

我们知道使用Vuex之前需要去初始化一个store文件,就是我们在Store.js中做的事情。

回到上面的例子,可以看到初始化文件的时候我们传入了一个配置项,里面包括了state,getters这些,下面我们就会去处理这部分的逻辑。

Store.js

export default new Vuex.Store({
  state: { // 数据源
      count: 1
  }, 
  getters: { // store的计算属性
    doneTodos (state) {
          return state.count + 1
    }
  }, 
  mutations: { // 更改数据源
      increment (state) {
          // 变更状态
          state.count++
      }
  }, 
  actions: { // 类似mutations,通过提交mutations更改数据源,但可以包含异步操作
    increment (context) {
          context.commit('increment')
    }
  }, 
  modules: {}, // 模块
});

myVuex中Store是一个类 用于实例化store对象,因此需要去接收传入的参数并进行处理

state

可以在构造函数中完成对state的接收,值得注意的是state中的数据都是响应式的 所以我们需要使用defineReactive将state中的数据转成响应式的

注:这里可能并不一定是用defineReactive将数据转成响应式,这只是其中一种方法

myVuex.js

// 创建一个store类,用于初始化Vuex
class Store {
  constructor(options) {
    // 将state挂载到store对象中,并转成双向绑定的数据
    Vue.util.defineReactive(this, "state", options.state);
  }
}

不妨测试下,在子组件中加入一个change事件去改变state的值。

注:mutation是唯一更改state的方式并不确定,其实是可以直接改的,但是严格模式下会报错,不建议这样子干

Vuex源码而言:其底层通过执行 `this._withCommit(fn)` 设置_committing标志变量为true,然后才能修改state,修改完毕还需要还原_committing变量。外部修改虽然能够直接修改state,但是并没有修改_committing标志位,所以只要watch一下state,state change时判断是否_committing值为true,即可判断修改的合法性。

<template>
  <div @click="change" class="item">
    子组件
    {{ this.$store.state.a }}
  </div>
</template>

<script>
export default {
  methods: {
    change() {
      this.$store.state.a = 2;
    },
  },
};

我们可以看到结果如下所示:

来来来,我们手写个简单的Vuex吧,帮助了解Vuex是如何实现全局状态管理的

我们在点击的时候父子组件同时完成了数据更新。

getters

上面我们已经处理完state了,这里处理下getters的逻辑。

getters相当于state的计算属性,我们可以通过getters对state做一些诸如计算等处理。

因此在接收完store传入的getters后,可以通过Object.defineProperty去对getters对象进行劫持,当get的时候通过key去匹配我们接收的getters,并执行对应的key的value函数即可

myVuex.js

class Store {
  constructor(options) {
    Vue.util.defineReactive(this, "state", options.state);
    this.initGetters(options);
  }
  // 初始化Getters
  initGetters(options) {
    // 获取传入的getters
    let initGetters = options.getters || {};
    // Store对象上创建一个getters属性
    this.getters = {};
    Object.keys(initGetters).forEach((key) => {
      // 通过 Object.defineProperty 去监听Store的getters 在get的时候执行函数并返回结果
      Object.defineProperty(this.getters, key, {
        get: () => {
          return initGetters[key](this.state);
        },
      });
    });
  }
}

可以看到我们在构造函数处调用了initGetters方法,在函数中我们初始化了getters,并对其完成了监听。

同样的我们来验证一下,在store.js中加入getters

Store.js

export const myStore = new myVuex.Store({
  state: {
    a: 1,
  },
  getters: {
    doneTodos(state) {
      return state.a + 1;
    },
  },
});

更新子组件代码

<template>
  <div @click="change" class="item">
    子组件
    <div>state{{ this.$store.state.a }}</div>
    <div>getters{{ this.$store.getters.doneTodos }}</div> // 新增getters
  </div>
</template>

<script>
export default {
  methods: {
    change() {
      this.$store.state.a = 2;
    },
  },
};

可以看到效果如下所示: 来来来,我们手写个简单的Vuex吧,帮助了解Vuex是如何实现全局状态管理的

getters成功计算出了doneTodos的结果

doneTodos(state) {
  return state.a + 1;
},

并且当state更新的时候,getters也同步更新,因此我们的getters就完成了。

mutations

mutations 是我们用来更新Vuex状态的工具,我们可以在里面设置函数,在函数的执行中去修改我们的state的结果。

值得注意的是mutations允许相同的方法名,会用方法名做key,去映射一个数组,当我们执行的时候就依次遍历数组里面的mutations方法 同样的我们可以创建一个initMutations方法,在myVuex文件中。

myVuex.js

// 创建一个store类,用于初始化Vuex
class Store {
  constructor(options) {
    Vue.util.defineReactive(this, "state", options.state);
    this.initGetters(options);
    this.initMutations(options);
  }
  // 初始化Getters
  initGetters(options) {
    ...
  }
  // 初始化mutations
  initMutations(options) {
    // 获取传入的mutations
    let initMutations = options.mutations || {};
    // Store对象上创建一个mutations属性
    this.mutations = {};
    // 遍历传入的mutations
    Object.keys(initMutations).forEach((key) => {
      // 初始化mutations,将key映射到一个数组上,允许同名key的存在
      this.mutations[key] = this.mutations[key] || [];
      this.mutations[key].push((payload) => {
        // 将对应的函数都push到数组中
        initMutations[key](this.state, payload);
      });
    });
  }
}

值得注意的是mutations通过commit去调用。 因此需要额外提供个commit方法,调用时映射到对应的mutations数组,遍历执行即可

commit(key, payload) { // 调用时映射到对应的mutations数组,遍历执行即可
    this.mutations[key].forEach((item) => {
      item(payload);
    });
}

因此我们不妨验证一下,通过mutations去更新state的参数。

store.js

export const myStore = new myVuex.Store({
  state: {
    a: 1,
  },
  getters: {
    doneTodos(state) {
      return state.a + 1;
    },
  },
  mutations: {
    // 更改数据源
    increment(state, data) {
      // 变更状态
      state.a = data;
    },
  },
});

更新子组件代码:(提供了一个mutations,调用之后state的a会变成10)

<template>
  <div @click="change" class="item">
    子组件
    <div>state: {{ this.$store.state.a }}</div>
    <div>getters: {{ this.$store.getters.doneTodos }}</div>
  </div>
</template>

<script>
export default {
  methods: {
    change() {
      this.$store.commit("increment", 10);
    },
  },
};
</script>

效果如下:

来来来,我们手写个简单的Vuex吧,帮助了解Vuex是如何实现全局状态管理的

调用之后state的a成功变成10,getters也成功更新。

注:为什么要用mutations去更新数据,因为mutations可以看成一个数据更新的最小单位操作,通过mutations去更新我们可以知道每一步更新了什么,方便后续的数据处理。

actions

actions 和 mutations 差不多,都是用于更新数据的,不过 actions 用来处理异步的操作,如请求等

通过上面的学习我们其实可以知道,如果想改state的值其实不一定要走mutations,但是为什么Vuex要创建一个mutations这样子的东西呢?主要是用于数据的追踪,因此 actions 实际上对某个 mutations 进行了包裹,当目标数据拿到时就走mutations这个最小的操作单位去更新,后续就可以方便追踪数据。

同样的我们可以创建一个initActions方法,在myVuex文件中,和前面的initMutations很相似,也是key映射到一个数组

myVuex.js

// 创建一个store类,用于初始化Vuex
class Store {
  constructor(options) {
    Vue.util.defineReactive(this, "state", options.state);
    this.initGetters(options);
    this.initMutations(options);
    this.initActions(options);
  }
  // 初始化Getters
  initGetters(options) {
    ...
  }
  // 调用更新
  commit(key, payload) {
    ...
  }
  // 初始化mutations
  initMutations(options) {
    ...
  }
  // 初始化actions
  initActions(options) {
    // 获取传入的actions
    let initActions = options.actions || {};
    // Store对象上创建一个mutations属性
    this.actions = {};
    // 遍历传入的actions
    Object.keys(initActions).forEach((key) => {
      // 初始化actions,将key映射到一个数组上,允许同名key的存在
      this.actions[key] = this.actions[key] || [];
      this.actions[key].push((payload) => {
        // 将对应的函数都push到数组中
        initActions[key](this, payload);
      });
    });
  }
}

值得注意的是actions通过dispatch去调用。 因此需要额外提供个dispatch方法,调用时映射到对应的actions数组,遍历执行即可

// 调用异步更新
dispatch(key, payload) {
    this.actions[key].forEach((item) => {
      item(payload);
    });
}

因此我们不妨验证一下,通过actions异步去更新state的参数。

store.js

export const myStore = new myVuex.Store({
  state: {
    a: 1,
  },
  getters: {
    doneTodos(state) {
      return state.a + 1;
    },
  },
  mutations: {
    // 更改数据源
    increment(state, data) {
      // 变更状态
      state.a = data;
    },
  },
  actions: {
    // 类似mutations,通过提交mutations更改数据源,但可以包含异步操作
    increment(context, data) {
      setTimeout(() => {
        context.commit("increment", data);
      }, 5000);
    },
  },
});

这里提供了actions调用之后在5s后会更新state.a的值

更新子组件代码:(提供了一个actions,调用dispatch之后state的a会变成100)

<template>
  <div @click="change" class="item">
    子组件
    <div>state: {{ this.$store.state.a }}</div>
    <div>getters: {{ this.$store.getters.doneTodos }}</div>
  </div>
</template>

<script>
export default {
  methods: {
    change() {
      this.$store.dispatch("increment", 100);
    },
  },
};
</script>

效果如下:

来来来,我们手写个简单的Vuex吧,帮助了解Vuex是如何实现全局状态管理的

五秒后成功更新了state.a的值

总结

到这里Vuex的基本原理就全部讲完了,这个是简化版的原理讲解,真实的Vuex实际上还做了很多特殊逻辑,对应模块处也做了很多处理,只是平时用模块的场景不多,这里就不展开说说。

Vuex原理

  1. 首先将store对象通过new Vue挂载到this.$options的store中
  2. 然后在Vuex抛出的对象中创建一个install方法,
  3. install方法中全局mixins了生命周期在beforecreate的一个函数。
  4. 通过这个函数对当前的组件实例对象创建一个this.$store的值
  5. 如果是根节点就指向this.options的store,如果是子节点就指向父亲的this.options的store,如果是子节点就指向父亲的this.optionsstore,如果是子节点就指向父亲的this.store
  6. 通过这种方法实现全局调用一个对象
  7. 在初始化store对象的时候将传入的state的值转成个响应式类型的数据,就实现了一处更新处处更新。

完整代码

import Vue from "vue";

const install = () => {
  Vue.mixin({
    beforeCreate() {
      // Vue创建实例的时候是递归创建的,因此先创建父组件,再创建子组件
      // 对于根组件而言,默认 myStore 就存在 this.$options下
      if (this.$options && this.$options.myStore) {
        this.$store = this.$options.myStore;
      }
      // 对于子组件而言,默认没有store,此时存储父组件的store即可
      else {
        this.$store = this.$parent.$store;
      }
    },
  });
};

// 创建一个store类,用于初始化Vuex
class Store {
  constructor(options) {
    Vue.util.defineReactive(this, "state", options.state);
    this.initGetters(options);
    this.initMutations(options);
    this.initActions(options);
  }
  // 初始化Getters
  initGetters(options) {
    // 获取传入的getters
    let initGetters = options.getters || {};
    // Store对象上创建一个getters属性
    this.getters = this.getters || {};
    Object.keys(initGetters).forEach((key) => {
      // 通过 Object.defineProperty 去监听Store的getters 在get的时候执行函数并返回结果
      Object.defineProperty(this.getters, key, {
        get: () => {
          return initGetters[key](this.state);
        },
      });
    });
  }
  // 调用更新
  commit(key, payload) {
    this.mutations[key].forEach((item) => {
      item(payload);
    });
  }
  // 初始化mutations
  initMutations(options) {
    // 获取传入的mutations
    let initMutations = options.mutations || {};
    // Store对象上创建一个mutations属性
    this.mutations = {};
    // 遍历传入的mutations
    Object.keys(initMutations).forEach((key) => {
      // 初始化mutations,将key映射到一个数组上,允许同名key的存在
      this.mutations[key] = this.mutations[key] || [];
      this.mutations[key].push((payload) => {
        // 将对应的函数都push到数组中
        initMutations[key](this.state, payload);
      });
    });
  }
  // 调用异步更新
  dispatch(key, payload) {
    this.actions[key].forEach((item) => {
      item(payload);
    });
  }
  // 初始化actions
  initActions(options) {
    // 获取传入的actions
    let initActions = options.actions || {};
    // Store对象上创建一个mutations属性
    this.actions = {};
    // 遍历传入的actions
    Object.keys(initActions).forEach((key) => {
      // 初始化actions,将key映射到一个数组上,允许同名key的存在
      this.actions[key] = this.actions[key] || [];
      this.actions[key].push((payload) => {
        // 将对应的函数都push到数组中
        initActions[key](this, payload);
      });
    });
  }
}

// 抛出自己的vuex
export default {
  install,
  Store,
};
转载自:https://juejin.cn/post/7230078756626743356
评论
请登录