Vuex3使用与基本实现

Vuex 源码

Vuex的核心、插件机制、模块机制,命名空间和辅助函数。

lua 复制代码
 vue create vuex-project --force

前言

在 Vue 的组件间通信时,有很多种形式,父传子,子传父,跨级传递,provied/inject 等,但是并不直观。

Vuex 是借鉴了 Redux,Flux 的思想,但是它不像 Redux 可以用在 vue 项目中,可以用在 react 项目中或者 jQuery 项目中。Vuex 是专门为 Vue 设计的,不能脱离 Vue。在 Vuex 中数据也是单向流动的。

为了组件之间的相互通信,现在将所有的数据都存放在 Vuex 生成的 store 中,store 中存放的是公共的状态 state。需要对应状态的组件可以直接去 store 内部取数据。但是取用数据的组件不能直接的修改 store 中的状态数据,它需要手动派发一个行为。组件派发事件到 mutations 中,通过 mutations 来改变 store 中的状态。在 store 中的状态更改之后,会触发用到对应状态的组件重新渲染而非创建(响应式变化)。 对于异步派发的行为,比如要先请求一个数据,然后将请求后的数据放在 store 中,这时就必须组件先 dispatch 一个 action,在对应的 action 中去请求后台接口,把接口请求回来的数据拿到后,再去提交一个 mutaition,再由 mutations 去更改 store,store 中的状态一变,再去重新渲染对应的组件。

使用

在项目中创建了 store 目录,在 store 目录中创建 store.js,在内部引入了 Vue 和 Vuex,并调用了 Vue.use(Vuex),然后创建一个 store 实例并导出(const store = new Vuex.Store({......}) )。在项目入口文件 main.js 中引入 store.js 并将 store 实例作为 new Vue( ) 根实例的配置对象(注入 store),这样每个组件内部都能通过自生的 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t o r e 属性访问到存放在 s t o r e 中的状态数据。( store属性访问到存放在store中的状态数据。( </math>store属性访问到存放在store中的状态数据。(store.state.xxx

Vue.use(Vuex)说明插件内部暴露了一个 install 方法。同时 new Vuex.Store({...}) 说明 Vuex 中有一个属性 Store,并且该属性对应的值是一个类。并且在创建实例的时候会传入一系列的配置对象的选项。创建的配置对象属性有:state,mutations,actions,getters 和 modules。

组件直接去修改 store 中的状态数据是能被修改成功的,但是并不建议这样使用的。如果组件想同步的更改状态,可以调用 commit 方法,提交给 mutations。

在组件中:@click="()=>{$store.commit('mutationFun1',{key:value})}"

在 store 中:mutations:{ mutationFun1(state,payload){ .... } }

在 mutations 中的各个方法中尽量不要有异步函数,虽然写了默认是没有问题的。但是 mutations 中的方法中有异步操作时,在严格模式下会报错。

store/index.js:

javascript 复制代码
 import Vue from 'vue'
 import Vuex from 'vuex'
 ​
 Vue.use(Vuex)  // Vue.use方法内部会调用Vuex的install方法
 ​
 export default const store = new Vuex.store({
   state:{
     count:1
   },
   // 等价于计算属性
   getters:{
     count2(state){
       return state.age * 2
     }
   },
   // 执行同步更改state的操作
   mutations:{
       changeCount(state, payload){
         state.age += payload
       }
   },
   // 执行异步更改state的操作
   actions:{
       asyncChangeCount({commit,dispatch},payload){
         setTimeout(()=>{
           commit('changeCount',payload)
         },1000)
       }
   }
 }) 

src/main.js:

javascript 复制代码
 import Vue from 'vue'
 import store from './store/index.js'
 import App from './App。vue'
 ​
 Vue.config.productionTip = false
 ​
 new Vue({
   store,
   render:(h)=>h(App)
 }).$mount('#app')

App.vue:

xml 复制代码
 <template>
   <div id='app'>
     <p>
       {{$store.state.count}}
     </p>
     <p>
       {$store.getters.count2}}
       </p>
     <butoon @click="$stroe.commit('changeCount',2)">+2</butoon>
     <butoon @click="$stroe.dispatch('asyncChangeCount',2)">async+2</butoon>
   </div>
 </template>

原理

在 Vuex 内部:

这是简版的 Vuex 原理,其中并没有涉及到模块的情况。

kotlin 复制代码
 let Vue
 ​
 const forEach = (object,callback)=>{
   Object.keys(object).forEach(item=>{
     callback(item,object[item])
   })
 }
 ​
 class Store{
   //options就是用户new实例时传入的配置对象
   constructor(options){ 
     //这种做法直接将state对象的属性挂载到store实例上,虽然组件是可以获取到state中对应的属性数据了,但是一旦这些属性的属性值发生改变将无法响应视图做出改变。如果想要响应式的更新页面视图,那需要使用Vue的依赖收集。将state中的数据做成响应式的。
     //let state = options.state
     //this.state = state
 ​
     const computed = { }
     
      let getters = options.getters
     //把开发者传递的getters对象中的每个函数,转为当前实例的store上作为属性存在。(这可以作为一道面试问题:如果一个对象内部全是方法,每个方法都对应有一个返回值,那你能否在获取这些函数返回值时,给没有返回结果前面加上一个 '~'字符作为前置了?)
     this.getters = {}
     forEach(getters,(getterName,value)=>{
       computed[getterName] = ()=>{
         return value(this.state)
       }
       Object.defineProperty(this.getters,getterName,{
         get:()=>{  //注意使用箭头函数,否则this指向出错
           return this._vm[getterName]
         }
       })
     })
     
 ​
     this._vm = new Vue({   //Vuex中的核心源码,所以Vuex才是强依赖于Vue的,创建Vue的实例,保证状态更新可以刷新视图
       data:{
         // 在Vue中以$开头的数据不会被挂载到vue实例上
         $$state:options.state
       },
       computed
     })
    
 ​
     let mutations = options.mutations
     this.mutations = {}
     forEach(mutations,(mutationName,value)=>{
       this.mutations[mutationName] = (payolad)=>{
         value(this.state, payolad)
       }
     })
 ​
     let actions = options.actions
     this.actions = {}
     forEach(actions,(actionName,value)=>{
       this.actions[actionName] = (payload)=>{
         value(this,payload)
       }
     })
   }
 ​
   // 本质是发布订阅模式
   commit = (mutationName,payload)=>{
     this.mutations[mutationName](payload)
   }
 ​
   // 本质是发布订阅模式
   dispatch = (actionName,payload)=>{
     this.actions[actionName](payload)
   }
 ​
   //类的属性访问器,获取实例上的state属性就会执行此方法。为这么这样写了?这样写的话,可以在获取该属性之前做一些逻辑任务的处理。
   get state(){   
     //.... 逻辑处理
     return this._vm._data.$$state
   }
 }
 ​
 // 插件安装
 const install = (_Vue)=>{
   Vue = _Vue
   Vue.mixin({
     beforeCreate(){
       //把父组件的store属性放到每个组件实例及自身身上
       if(this.$options.store){  //根实例
         this.$store = this.$options.store
       }else{   //后代实例
         this.$store = this.$parent && this.$parent?.$store
       }
     }
   })
 }
 //目的是让当前插件不再手动引入Vue构造函数,这样就避免的在插件打包时再次打包一次Vue
 ​
 export default {
   Store,  //容器初始化
   install
 }

在项目入口文件中引入 store 的实例,并挂载到根实例上了。根实例上的 store 属性会被放到根实例的所有后代组件实例上,用$store 属性表示。(但是并不是通过放在 Vue 的原型对象上实现的)不放在原型上的原因是,在该项目中如果还创建了其他的根实例,但是该根实例并不需要使用 store,但是由于是放在 Vue 的原型对象上的,所以这个根实例也被迫能访问到该 store 实例了。

应该达到的效果是只有传了 store 配置项的根实例及其后代组件才能访问到 store 实例。为了实现该功能,内部采用了 Vue.mixin 方法。该方法的作用是抽离公共的逻辑,放一些方法,这些方法在每创建一个实例时都会被混入到 Vue 实例中。

Vue 中的 Mixin 机制允许开发者定义被多个组件共享的方法、计算属性、生命周期钩子等选项。当一个 mixin 被使用时,它的选项将被"混入"到组件的选项中。这种机制有助于代码的复用,尤其是在处理多个组件需要共享相同功能时。

Mixin 的原理主要基于 JavaScript 的对象合并策略。当组件和 mixin 包含相同选项时,这些选项将以特定的方式合并到组件中。Vue 内部使用的合并策略如下:

  1. 数据对象 (data): 组件和 mixin 中的 data 对象会被合并。如果有冲突,组件中的数据优先级更高。
  2. 生命周期钩子 :组件和 mixin 中的生命周期钩子函数会被合并到一个数组中,且 mixin 中的钩子函数会先于组件中的钩子函数被调用。
  3. 方法 (methods), 计算属性 (computed), 和 侦听器 (watch): 如果组件和 mixin 包含相同名称的方法、计算属性或侦听器,组件中的选项将优先。
  4. 组件选项 :如 components, directivesfilters 等选项会被合并。如果有冲突,组件中的选项优先。
  5. 自定义选项 : 对于自定义选项,Vue 允许通过全局 Vue.config.optionMergeStrategies 来定义自定义合并策略。

在 Vuex 中最核心的功能是,不把所有的状态都放在一个 state 中,而是能对状态进行分模块的管理。

注意点:

  • 在使用 Vue.use(插件名,{ key1:value1, key2:value2 })还可以选择性的传入第二个及以上的参数,在 intall 函数内部也能获取到。
  • extends 与 mixin,extends 可以继承于指定类的所有属性,mixin 是混合所有的方法。
  • for(let key in obj){ ... }的性能不好,因为会遍历原型对象,最好写成 Object.keys(obj).forEach(item=>{.....}) ,并且 Object.keys 就不必再使用 hasOwnproperty 进行判断是否是私有属性。
  • 子模块的模块名不能和父模块 state 中的状态一致,如果一致的话,子模块的模块名会覆盖父模块 state 中同名的状态

嵌套的组件之间的beforeCreate生命周期函数的执行顺序是先父再子的。

完整实现

vuex/index.js

javascript 复制代码
 import { Store, install } from './store';
 ​
 export default {
     Store,
     install
 }

store.js:

javascript 复制代码
 import applyMixin from './mixin'
 ​
 let Vue;
 ​
 export class Store {
   constructor(options){
     let state = options.state;
     this._vm = new Vue({
       data:{
         $$state:state,  // 定义$开头的变量不会被代理到实例上
       }
     });
   }
   get state(){
     return this._vm._data.$$state
   }
 }
 ​
 export const install = (_Vue) =>{
   Vue = _Vue;
   applyMixin(Vue);
 }

当使用Vue2的use方法注册插件时,默认会执行插件暴露的install方法并传入Vue的构造函数。

mixin.js:

php 复制代码
 const applyMixin = (Vue) => {
   Vue.mixin({
     beforeCreate: vuexInit
   })
 }
 ​
 function vuexInit() {
   // this就是一个个的vue实例对象
   const options = this.$options;
   if (options.store) { 
     // 给根实例增加$store属性
     this.$store = options.store;
   } else if (options.parent && options.parent.$store) {
     // 给组件增加$store属性
     this.$store = options.parent.$store;
   }
 }
 export default applyMixin

实现getter

typescript 复制代码
 ​
 ​
 const forEachValue = (object,callback)=>{
   Object.keys(object).forEach(item=>{
     callback(object[item], item)
   })
 }

store.js

javascript 复制代码
 import { forEachValue } from '../util'
 let Vue;
 ​
 export class Store {
   constructor(options){
     let state = options.state;
 ​
     this.getters = {};
     const computed = {}
     forEachValue(options.getters, (fn, key) => {
       computed[key] = () => {
         return fn(this.state);
       }
       Object.defineProperty(this.getters,key,{
         get:()=> this._vm[key]
       })
     });
 ​
     this._vm = new Vue({
       data:{
         $$state:state,  // 定义$开头的变量不会被代理到实例上
       },
       computed // 利用计算属性实现缓存
     });
   }
   get state(){
     return this._vm._data.$$state
   }
 }

实现mutations

typescript 复制代码
 export class Store {
   constructor(options) {
     // ...
     
     this.mutations = {};
     forEachValue(options.mutations, (fn, key) => {
       this.mutations[key] = (payload) => fn.call(this, this.state, payload)
     });
   }
   commit = (type, payload) => {
     this.mutations[type](payload);
   }
 }

实现actions

typescript 复制代码
 export class Store {
   constructor(options) {
     this.actions = {};
     forEachValue(options.actions, (fn, key) => {
       this.actions[key] = (payload) => fn.call(this, this,payload);
     });
   }
   dispatch = (type, payload) => {
     this.actions[type](payload);
   }
 }

Vuex4

Vue3中使用vuex

store.js

javascript 复制代码
 import { createStore } from 'vuex'
 ​
 export default createStore({
   state: { // 状态
     count: 0
   },
   getters: { // 计算属性
     double(state) {
       return state.count * 2
     }
   },
   mutations: { // 同步方法
     add(state, payload) {
       state.count += payload;
     }
   },
   actions: { // 异步方法
     asyncAdd({ commit }, payload) {
       setTimeout(() => {
         commit('add', payload)
       }, 1000);
     }
   }
 });

main.js

javascript 复制代码
 import store from './store'
 createApp(App).use(store /*injectKey*/).mount('#app')

App.vue

xml 复制代码
 <template>
   <div>当前数量:{{count}} {{$store.state.count}}</div>
   <div>翻倍 :{{double}} {{$store.getters.double}}</div>
   <button @click="add">+</button>
   <button @click="asyncAdd">异步+</button>
 </template>
 ​
 <script>
   import {computed} from 'vue'
   import {useStore} from 'vuex'
   export default {
     name: 'App',
     setup(){
       const store = useStore();
       return {
         count:computed(()=>store.state.count),
         double:computed(()=>store.getters.double),
         add:()=>store.commit('add',1),
         asyncAdd:()=>store.dispatch('asyncAdd',2)
       }
     }
   }
 </script>
相关推荐
小白学前端66632 分钟前
React Router 深入指南:从入门到进阶
前端·react.js·react
苹果醋333 分钟前
React系列(八)——React进阶知识点拓展
运维·vue.js·spring boot·nginx·课程设计
web130933203981 小时前
前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案
前端
王小王和他的小伙伴1 小时前
解决 vue3 中 echarts图表在el-dialog中显示问题
javascript·vue.js·echarts
outstanding木槿1 小时前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
好名字08212 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
隐形喷火龙2 小时前
element ui--下拉根据拼音首字母过滤
前端·vue.js·ui
m0_748241122 小时前
Selenium之Web元素定位
前端·selenium·测试工具
风无雨2 小时前
react杂乱笔记(一)
前端·笔记·react.js
前端小魔女2 小时前
2024-我赚到自媒体第一桶金
前端·rust