理解 vuex 实现原理

12,171 阅读6分钟

文章已同步至【个人博客】,欢迎访问【我的主页】😃
文章地址:blog.fanjunyang.zone/archives/vu…

Vuex是什么?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,

这个状态管理应用包含以下几个部分:

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化。

给出一张官方的“单向数据流”理念的简单示意:

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。

Vuex 和单纯的全局对象有以下两点不同:

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。(也就是所谓的MVVM)
  • 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutations

看图了解工作原理:

如果理解了这张图,你就能知道vuex的工作原理了

需要注意的点:

  • 改变状态的唯一途径就是提交mutations
  • 如果是异步的,就派发(dispatch)actions,其本质还是提交mutations
  • 怎样去触发actions呢?可以用组件Vue Components使用dispatch或者后端接口去触发
  • 提交mutations后,可以动态的渲染组件Vue Components

觉得是不是少了什么,没错,就是getters 下面原理实现的时候会说

原理实现

准备工作

首先把不需要的文件和代码全删了,经典化结构,如下:

App.vue代码:

<template>
   <div>
      <!-- vuex 把状态放到一个公共的地方,哪个组件使用,就直接可以从公共的地方获取状态 -->
   </div>
</template>
<script>
export default {
   name:'app',
}  
</script>

main.js代码:

import Vue from 'vue'
import App from './App.vue'
import store from './store'
import router from 'vue-router'

Vue.config.productionTip = false

new Vue({
  name:'main',
  router, //封装了 router-view router-link $router $route
  store,  //写到这里,说明全部的组件都可以使用store
  render: h => h(App)
}).$mount('#app')

store.js代码:

import Vue from 'vue'
//把里面的全删了,自己写

// 引入自己的写的vuex,里面有一个对象{install},当你use时,会自动调用这个方法
//导入vuex {install Store}
import Vuex from './vuex'

Vue.use(Vuex) 

//需要创建一个仓库并导出
//当new的时候,给Vuex.js中传入了一堆的东西
export default new Vuex.Store({
    state:{
        name:'Fan'
    },
    //getters中虽然是一个方法,但是用时,可以把他当作属性
    getters:{   // 说白了,就是vue中data中的computed
        
    },
    // 改变状态:异步请求数据  事件 
    mutations:{

    },
    actions:{
        
    }
})

vuex.js文件中的代码先不写,下面开始写

实现state

上面准备工作做好,接下来就实现我们的state

vuex.js中写如下代码(具体说明和操作已在代码中注释):

//定义一个Vue,让全局都可以使用这个Vue
let Vue;

class Store{
    //当new的时候,给Vuex.js中传入了一堆的东西,在这里接收需要用constructor
    constructor(options){
        // console.log(options);   //打印出{state: {…}, getters: {…}, mutations: {…}, actions: {…}},就可以拿到里面的数据了
        
/*-------------------------------state原理-------------------------------------------------------------*/
        //给每个组件的$store上挂一个state,让每个组件都可以用  this.$store.state
        this.state = options.state

        //在state上面传入一个name:'Fan'打印一下
        // console.log(this.state);    //打印结果  {name: "Fan"}
/*-------------------------------------------------------------------------------------------------*/

    }
}

//install本质上就是一个函数
const install = (_Vue)=>{
    // console.log('......');  //测试能不能调到这个方法,经测试可以调到
    //把构造器赋给全局Vue
    Vue = _Vue;

    //混入
    Vue.mixin({
        beforeCreate() {    //表示在组件创建之前自动调用,每个组件都有这个钩子
            // console.log(this.$options.name) //this表示每个组件,测试,可以打印出mian.js和App.vue中的name main和app
            
            //保证每一个组件都能得到仓库
            //判断如果是main.js的话,就把$store挂到上面
            if(this.$options && this.$options.store){
                this.$store = this.$options.store
            }else{
                //如果不是根组件的话,也把$store挂到上面,因为是树状组件,所以用这种方式
                this.$store = this.$parent && this.$parent.$store

                //在App.vue上的mounted({console.log(this.$store)})钩子中测试,可以得到store ---> Store {}
            }
        },
    })
}

//导出
export default {
    install,
    Store
}

这样的话,全部的组件都可以使用this.$store.state这个方法了

实现getters

首先在store.js中的getters中定义两个方法,用来测试:

//getters中虽然是一个方法,但是用时,可以把他当作属性
getters:{   // 说白了,就是vue中data中的computed
    myName(state){
        return state.name+'Jun'
    },
    myAge(){
        
    }
},

然后在vuex.js文件中的Store类的constructor中来写我们的代码,如下:

class Store{
    //当new的时候,给Vuex.js中传入了一堆的东西,在这里接收需要用constructor
    constructor(options){
        // console.log(options);   //打印出{state: {…}, getters: {…}, mutations: {…}, actions: {…}},就可以拿到里面的数据了
        
/*------------------------------------state原理--------------------------------------------------------*/
        //给每个组件的$store上挂一个state,让每个组件都可以用  this.$store.state
        // this.state = options.state
/*-------------------------------------------------------------------------------------------------*/

/* --------------------------------状态响应式原理---------------------------------------------------------------- */
        // 上面那种写法不完美,当改变数据的时候,不能动态的渲染,所以需要把data中的数据做成响应式的
        //_s在下面的 get state方法中使用
        this._s = new Vue({
            data:{
                // 只有data中的数据才是响应式
                state:options.state
            }
        })

        
        //在state上面传入一个name:'Fan'打印一下
        // console.log(this.state);    //打印结果  {name: "Fan"}
/* ------------------------------------------------------------------------------------------------ */

/*---------------------------------getters原理-----------------------------------------------------------*/
        //得到仓库中的getters,如果人家不写getters的话,就默认为空
        let getters = options.getters || {}
        // console.log(getters);   //打印出一个对象,对象中是一个方法  {myName: ƒ}

        //给仓库上面挂载一个getters,这个getters和上面的那一个getters不一样,一个是得到,一个是挂载
        this.getters = {}

        //不好理解,因为人家会给你传多个方法,所以使用这个api处理得到的getters,得到一个数组
        //把store.js中的getters中再写一个方法myAge,用来测试
        // console.log(Object.keys(getters));  //打印出  ["myName", "myAge"]

        //遍历这个数组,得到每一个方法名
        Object.keys(getters).forEach((getter)=>{
            // console.log(getter);    //打印出  myName   myAge
            Object.defineProperty(this.getters,getter,{
                //当你要获取getter的时候,会自动调用get这个方法
                //一定要用箭头函数,要不然this指向会出现问题
                get:()=>{
                    console.log(this);
                    return getters[getter](this.state)
                }
            })
        })
/*-------------------------------------------------------------------------------------------------*/

    } 
    get state(){
        return this._s.state
    }  
}

然后在App.vue中测试:

<template>
   <div>
      <!-- vuex 把状态放到一个公共的地方,哪个组件使用,就直接可以从公共的地方获取状态 -->
      {{this.$store.state.name}}
      <!-- 打印出 Fan -->
      {{this.$store.getters.myName}}   
      <!-- 打印出 FanJun -->
      
   </div>
</template>
<script>
export default {
   name:'app',
   mounted(){
      console.log(this.$store);
   }
}  
</script>

实现mutations

先用人家的试一下:

App.vue中定义一个add方法,上面定义一个按钮用来触发这个方法,代码:

<template>
   <div>
      <!-- vuex 把状态放到一个公共的地方,哪个组件使用,就直接可以从公共的地方获取状态 -->
      {{this.$store.state.name}}
      <!-- 打印出 Fan -->
      {{this.$store.getters.myName}}   
      <!-- 打印出 FanJun -->
      <hr>
      {{this.$store.state.age}}
      <button @click="add()">Add</button>
      
   </div>
</template>
<script>
export default {
   name:'app',
   mounted(){
      console.log(this.$store);
   },
   methods:{
      add(){
         //commit一个mutations
         this.$store.commit('add',10)
      }
   }
}  
</script>

store.js中用人家的vuex:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
    state:{
        name:'Fan',
        age:10
    },
    //getters中虽然是一个方法,但是用时,可以把他当作属性
    getters:{   // 说白了,就是vue中data中的computed
        myName(state){
            return state.name+'Jun'
        },
        myAge(){

        }
    },
    // 改变状态:异步请求数据  事件 
    mutations:{
        add(state,payload){
            state.age += payload
        }
    },
})

这次当点击Add按钮的时候,就能实现 加10 操作

然后自己写:

store.js中写上mutations,并且定义两个方法:

// 改变状态:异步请求数据  事件 
mutations:{
	add(state,payload){
		state.age += payload
	},
	sub(){

	}
},

然后在vuex.js中的类Store中实现:

class Store{
    //当new的时候,给Vuex.js中传入了一堆的东西,在这里接收需要用constructor
    constructor(options){
        // console.log(options);   //打印出{state: {…}, getters: {…}, mutations: {…}, actions: {…}},就可以拿到里面的数据了
        
/*-------------------------------state原理-------------------------------------------------------------*/
        //给每个组件的$store上挂一个state,让每个组件都可以用  this.$store.state
        // this.state = options.state
/*----------------------------------------------------------------------------------------------------*/

/* --------------------------------状态响应式原理---------------------------------------------------------------- */
        // 上面那种写法不完美,当改变数据的时候,不能动态的渲染,所以需要把data中的数据做成响应式的
        //_s在下面的 get state() 方法中使用
        this._s = new Vue({
            data:{
                // 只有data中的数据才是响应式
                state:options.state
            }
        })
        
        //在state上面传入一个name:'Fan'打印一下
        // console.log(this.state);    //打印结果  {name: "Fan"}
/* ----------------------------------------------------------------------------------------------------------------- */

/* ----------------------------------getters原理------------------------------------------------------------- */    

        //得到仓库中的getters,如果人家不写getters的话,就默认为空
        let getters = options.getters || {}
        // console.log(getters);   //打印出一个对象,对象中是一个方法  {myName: ƒ}

        //给仓库上面挂载一个getters,这个getters和上面的那一个getters不一样,一个是得到,一个是挂载
        this.getters = {}

        //不好理解,因为人家会给你传多个方法,所以使用这个api处理得到的getters,得到一个数组
        //把store.js中的getters中再写一个方法myAge,用来测试
        // console.log(Object.keys(getters));  //打印出  ["myName", "myAge"]

        //遍历这个数组,得到每一个方法名
        Object.keys(getters).forEach((getter)=>{
            // console.log(getter);    //打印出  myName   myAge
            Object.defineProperty(this.getters,getter,{
                //当你要获取getter的时候,会自动调用get这个方法
                //一定要用箭头函数,要不然this指向会出现问题
                get:()=>{
                    // console.log(this);
                    return getters[getter](this.state)
                }
            })
        })
/* -------------------------------------------------------------------------------------------------- */
    
/* ---------------------------------------mutatios原理----------------------------------------------------------- */
        //和getters思路差不多

        //得到mutations
        let mutations = options.mutations || {}
        // console.log(mutations);     //{add: ƒ}

        //挂载mutations
        this.mutations = {}

        //拿到对象中的一堆方法
        Object.keys(mutations).forEach((mutation)=>{
            // console.log(mutation);  //add sub
            this.mutations[mutation] = (payload)=>{
                mutations[mutation](this.state,payload)
            }
        })

        //打印看一下,正确
        // console.log(mutations);     //{add: ƒ, sub: ƒ}
        
        //但是他比较恶心,需要实现commit,在下面实现
/* -------------------------------------------------------------------------------------------------- */

    } 

    //给store上挂一个commit,接收两个参数,一个是类型,一个是数据
    commit(type,payload){
        //{add: ƒ, sub: ƒ}
        //把方法名和参数传给mutations
        this.mutations[type](payload)
    }

    get state(){
        return this._s.state
    }  
}

App.vue中测试:

<template>
   <div>
      <!-- vuex 把状态放到一个公共的地方,哪个组件使用,就直接可以从公共的地方获取状态 -->
      {{this.$store.state.name}}
      <!-- 打印出 Fan -->
      {{this.$store.getters.myName}}   
      <!-- 打印出 FanJun -->
      <hr>
      {{this.$store.state.age}}
      <button @click="add()">Add</button>
      
   </div>
</template>
<script>
export default {
   name:'app',
   mounted(){
      // console.log(this.$store);
   },
   methods:{
      add(){
         //commit一个mutations
         this.$store.commit('add',10)
      }
   }
}  
</script>

因为代码比较冗余,所以我简化了代码,就是把公共的方法Object.keys(obj).forEach(key => { callback(key, obj[key]) })抽离出来。

可以下载源码看一下,这里就不多说了

实现actions

同样的,在vuex.js中的类Store中实现,因为我简化了代码,所以整体复制下来看一下,

这里把dispatchcommit方法换成了箭头函数,防止this指向出现问题

//定义一个Vue,让全局都可以使用这个Vue
let Vue;

// forEach是用来循环一个对象
const forEach = (obj, callback) => {
    // 把数组中的每一个key得到  objc[key] 
    // key  value  ----> callback
    Object.keys(obj).forEach(key => {
        callback(key, obj[key])
    })
}

class Store {
    //当new的时候,给Vuex.js中传入了一堆的东西,在这里接收需要用constructor
    constructor(options) {
        // console.log(options);   //打印出{state: {…}, getters: {…}, mutations: {…}, actions: {…}},就可以拿到里面的数据了
        
/*-------------------------------state原理-------------------------------------------------------------*/
        //给每个组件的$store上挂一个state,让每个组件都可以用  this.$store.state
        // this.state = options.state
/*----------------------------------------------------------------------------------------------------*/

/* ---------------------------------------状态响应式原理--------------------------------------------------------- */
        // 上面那种写法不完美,当改变数据的时候,不能动态的渲染,所以需要把data中的数据做成响应式的
        //_s在下面的 get state方法中使用
        this._s = new Vue({
            data: {
                // 只有data中的数据才是响应式
                state: options.state
            }
        })
/* ----------------------------------------------------------------------------------------------------------------- */

/* ----------------------------------------getters原理------------------------------------------------------- */
        //在state上面传入一个name:'Fan'打印一下
        // console.log(this.state);    //打印结果  {name: "Fan"}

        //得到仓库中的getters,如果人家不写getters的话,就默认为空
        let getters = options.getters || {}
        // console.log(getters);   //打印出一个对象,对象中是一个方法  {myName: ƒ}

        //给仓库上面挂载一个getters,这个getters和上面的那一个getters不一样,一个是得到,一个是挂载
        this.getters = {}

        //不好理解,因为人家会给你传多个方法,所以使用这个api处理得到的getters,得到一个数组
        //把store.js中的getters中再写一个方法myAge,用来测试
        // console.log(Object.keys(getters));  //打印出  ["myName", "myAge"]

        forEach(getters, (getterName, value) => {
            Object.defineProperty(this.getters, getterName, {
                get: () => {
                    return value(this.state)
                }
            })
        })
/* -------------------------------------------------------------------------------------------------- */

/* ----------------------------------------mutatios原理---------------------------------------------------------- */
        //和getters思路差不多

        //得到mutations
        let mutations = options.mutations || {}
        // console.log(mutations);     //{add: ƒ}

        //挂载mutations
        this.mutations = {}

        forEach(mutations, (mutationName, value) => {
            this.mutations[mutationName] = (payload) => {
                value(this.state, payload)
            }
        })

        //打印看一下,正确
        // console.log(mutations);     //{add: ƒ, sub: ƒ}
        //但是他需要实现commit,在下面实现
/* -------------------------------------------------------------------------------------------------- */

/* ---------------------------------------------actions原理----------------------------------------------------- */
        //和上面两种大同小异,不多注释了
        let actions = options.actions || {}
        this.actions = {};
        forEach(actions, (action, value) => {
            this.actions[action] = (payload) => {
                value(this, payload)
            }
        })
/* -------------------------------------------------------------------------------------------------- */

    }
    // type是actions的类型  
    dispatch = (type, payload) => {
        this.actions[type](payload)
    }

    //给store上挂一个commit,接收两个参数,一个是类型,一个是数据
    commit = (type, payload) => {
        //{add: ƒ, sub: ƒ}
        this.mutations[type](payload)
    }

    get state() {
        return this._s.state
    }
}

//install本质上就是一个函数
const install = (_Vue) => {
    // console.log('......');  //测试能不能调到这个方法,经测试可以调到
    //把构造器赋给全局Vue
    Vue = _Vue;

    //混入
    Vue.mixin({
        beforeCreate() { //表示在组件创建之前自动调用,每个组件都有这个钩子
            // console.log(this.$options.name) //this表示每个组件,测试,可以打印出mian.js和App.vue中的name main和app

            //保证每一个组件都能得到仓库
            //判断如果是main.js的话,就把$store挂到上面
            if (this.$options && this.$options.store) {
                this.$store = this.$options.store
            } else {
                //如果不是根组件的话,也把$store挂到上面,因为是树状组件,所以用这种方式
                this.$store = this.$parent && this.$parent.$store

                //在App.vue上的mounted()钩子中测试,可以得到store ---> Store {}
            }
        },
    })
}

//导出
export default {
    install,
    Store
}

mutations中添加一个异步方法:

mutations: {
	add(state, payload) {
		state.age += payload
	},
	sub() {

	},
	asyncSub(state, payload) {
		state.age -= payload
	}
},

store.js中写一个actions

actions: {
	asyncSub({commit}, payload) {
		setTimeout(() => {
			commit("asyncSub", payload)
		}, 2000)
	}
} 

最后在App.vue中定义方法测试:

<template>
   <div>
      <!-- vuex 把状态放到一个公共的地方,哪个组件使用,就直接可以从公共的地方获取状态 -->
      {{this.$store.state.name}}
      <!-- 打印出 Fan -->
      {{this.$store.getters.myName}}
      <!-- 打印出 FanJun -->
      <hr> {{this.$store.state.age}}
      <!-- 同步加 -->
      <button @click="add">Add</button>
      <!-- 异步减 -->
      <button @click="sub">Async Sub</button>
   </div>
</template>
<script>
export default {
  name: "app",
  mounted() {
    // console.log(this.$store);
    // 是异步的
    setTimeout(() => {
      this.$store.state.age = 666;
    }, 1000);
    // 是同步的
    console.log(this.$store.state);
  },
  methods: {
    add() {
      //commit一个mutations
      this.$store.commit("add", 10);
    },
    sub(){
      this.$store.dispatch("asyncSub",10)
    }
  }
};
</script>

删去注释的vuex.js代码

其实并没有多少代码

let Vue;
const forEach = (obj, callback) => {
    Object.keys(obj).forEach(key => {
        callback(key, obj[key])
    })
}
class Store {
    constructor(options) {
        this._s = new Vue({
            data: {
                state: options.state
            }
        })
        let getters = options.getters || {}
        this.getters = {};
        forEach(getters, (getterName, value) => {
            Object.defineProperty(this.getters, getterName, {
                get: () => {
                    return value(this.state)
                }
            })
        })
        let mutations = options.mutations || {}
        this.mutations = {};
        forEach(mutations, (mutationName, value) => {
            this.mutations[mutationName] = (payload) => {
                value(this.state, payload)
            }
        })
        
        let actions = options.actions || {}
        this.actions = {};
        forEach(actions,(actionName,value)=>{
            this.actions[actionName] = (payload)=>{
                value(this,payload)
            }
        })
    }
    dispatch=(type,payload)=>{
        this.actions[type](payload)
    }
    commit=(type, payload)=>{
        this.mutations[type](payload)
    }
    get state() {
        return this._s.state
    }
}
const install = _Vue => {
    Vue = _Vue
    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.store) {
                this.$store = this.$options.store
            } else {
                this.$store = this.$parent && this.$parent.$store
            }
        }
    })
}
export default { install, Store }

总述

因为注释太多,显得很复杂,所以最好把源码下载下来,自己去尝试写一下

附上源码地址:Vuex实现原理


^_<