实现一个简单的Vuex,了解状态管理模式

  • 可能你使用过Vuex,但是不是很了解内部的实现原理。
  • 这篇文章带你动手造一个Vuex,理解状态管理模式。
  • 先来体验一下效果:vuex-demo
  • 需要的知识:Vuex官方文档

一、整体结构

  • 开始之前,先来对vuex有一个整体的认识。
  • 代码主要两个部分,Store类是主体,install()方法是用于全局挂载。
// Store 类
class Store {
  // 先完成构造方法,构造方法接收一个对象
  constructor(options) {
  	//...待实现
  }
}

// install方法
// 因为Vuex 需要 Vue.use() 安装,所以我们必须要有个 install 方法 传入 Vue
// 第二个参数是一个可选对象
function install(Vue) {
	//... 待实现
}

// 导出 install 和 Store
export default {
  install,
  Store,
}

二、store类

1. 初始化

实例化该类时会传入statemutation等,需要在constructor中获取到

class Store {
  // 先完成构造方法,构造方法接收一个对象
  constructor(options) {
    const state = options.state || {}
    const mutations = options.mutations || {}
    const actions = options.actions || {}
    const getters = options.getters || {}
  }
}

2. state

  • 简介:用于数据的存储,是store中的唯一数据源
  • 调用Vue的全局方法observable(),将state中的数据转化为响应式数据
// 初始化一个变量,用于后面在install方法中保存全局的Vue
let _Vue = null
class Store {
  constructor(options) {
  	//...其他细节
    // 把 state 中的数据转为 响应式,这里的_Vue已经赋值为全局的Vue
	this.state = _Vue.observable(state)
  }
}

3. getters

  • 简介:getters如vue中的计算属性一样,基于state数据的二次包装,常用于数据的筛选和多个数据的相关性计算。
  • 使用Object.definePropertygetters的属性进行劫持,设置get方法。
  • 当获取this.getters.xx时,就调用getters.xx.call(this, this.state)。也就是改变this指向,执行里面的方法。在组件中使用this.$store.getters.xx去执行对应操作并得到返回值。

不过,这里的代码没有实现缓存。

class Store {
  constructor(options) {
    //...其他细节
    // Object.create(null)好处是不用考虑会和原型链上的属性重名问题
    this.getters = Object.create(null)
    // 我们要为 getters 添加一个 get 方法,这里就要使用 数据劫持
    Object.keys(getters).forEach((key) => {
      Object.defineProperty(this.getters, key, {
        // 为 this.getters 每一项都添加 一个 get 方法
        get: () => {
          return getters[key].call(this, this.state)
        },
      })
    })
  }
}

4. mutations和actions

  • mutations:类似函数,改变state数据的唯一途径,且不能用于处理异步事件
  • actions:类似于mutation,用于提交mutation来改变状态,而不直接变更状态,可以包含任意异步操作
  • 通过后面定义的commit()dispatch()触发mutations和actions中的方法。
  • 触发时传递的时指令类型和参数,修改操作在mutations中完成。actions中完成异步操作后,通过commit触发mutations中的修改操作。
class Store {
  constructor(options) {
    //...其他细节
    // mutations
    this.mutations = {}
    Object.keys(mutations).forEach((key) => {
      this.mutations[key] = (params) => {
        // 改变this指向 ,默认是要传入 state
        mutations[key].call(this, this.state, params)
      }
    })
    // actions
    this.actions = {}
    Object.keys(actions).forEach((key) => {
      this.actions[key] = (params) => {
        // 改变this指向 ,默认是要传入 store也就是 this
        actions[key].call(this, this, params)
      }
    })
  }
}

其实,mutations和actions内的代码要比这个复杂得多。目前实现的两个方法没有什么区别,实际上是有的。

5. commit和dispatch

  • commit方法用于 触发mutations中的修改操作
  • dispatch方法用于触发actions中的异步操作
class Store {
  constructor(options) {
    //...其他细节
  }
  
  // commit
  // 第一个参数是事件名 ,第二个是参数
  commit = (eventName, params) => {
    this.mutations[eventName](params)
  }
  
  // dispatch
  // 第一个参数是事件名 ,第二个是参数
  dispatch = (eventName, params) => {
    this.actions[eventName](params)
  }
}

三、install方法

  • install()vue.use()时会调用
  • vue.mixin({...})是将内容注入到所有组件中,作为组件的一部分
  • 这样,在Vue组件内就能调用$store访问到store中的属性和方法

注意:store实例是通过new Vue()传入(这部分在入口文件中)

function install(Vue) {
  // 保存全局到 _Vue
  _Vue = Vue
  _Vue.mixin({
    beforeCreate() { // beforeCreate 生命周期
      if (this.$options.store) { // 判断Vue实例化是否以后store传入
        _Vue.prototype.$store = this.$options.store // 把 store 挂载到 Vue 原型上
      }
    },
  })
}

到这一步,就完成了简单的Vuex。下面将该Vuex放入到项目中,用实际的例子测试一下。

四、搭建项目

  • 这里搭建一个小项目,简单配置一下,测试一下前面的代码。

1. 项目结构

在这里插入图片描述

  • 将上面的代码放入到myVuex文件夹中

2. 测试用例

// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
  // 挂载到vue 中
  store,
  render: (h) => h(App),
}).$mount('#app')
// App.vue
<template>
  <div id="app">
    <!-- 案例一 -->
    <div>
      <h3>案例一</h3>
      <h1>state测试: <span>{{ this.$store.state.name }}</span></h1>
      <h1>getters测试: <span>{{ this.$store.getters.decorationName }}</span></h1>
      <button @click="changeName()">立刻修改</button>
      <button @click="changeNameAsync()">1s后修改</button>
    </div>
    <div>
      <h3>案例二</h3>
      <div>
        <h1>价格:<span>{{ this.$store.state.price }}</span> 元</h1>
        <h1>数量:<span>{{ this.$store.state.number }}</span> 个</h1>
        <h1>总价:<span>{{ this.$store.getters.total }}</span> 元</h1>
      </div>
      <div>
        <button @click="changePrice()">价格+1</button>
        <button @click="changeNumberAsync()">数量+1</button>
        <span v-if="this.$store.state.numberBack">正在查询库存,请稍等...</span>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'App',
    methods: {
      changeName() {
        this.$store.commit('changeName', '李四')
      },
      changeNameAsync(){
        this.$store.dispatch('changeNameAsync', '王五')
      },
      changePrice() {
        this.$store.commit('changePrice')
      },
      changeNumberAsync() {
        this.$store.dispatch('changeNumberAsync')
      }
    }
  }
</script>

<style  scoped>
  h3 {
    color: rgb(81, 24, 141)
  }
  h1 span{
    color: #4f4f4f;
  }
</style>
// store.js
import Vue from 'vue'
// 导入 我们自己写的 Vuex 插件
import Vuex from './myVuex'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name: '张三',
    high: 199,
    price: 5,
    number: 6,
    numberBack: false
  },
  mutations: {
    changeName(state, newName) {
      state.name = newName
    },
    changePrice(state) {
      state.price++
    },
    changeNumber(state) {
      state.number++
    },
    changeNumberBack(state, newBack) {
      state.numberBack = newBack
    }
  },
  actions: {
    changeNameAsync(context, newName) {
      setTimeout(() => {
        // 在这里调用 mutations 中的处理方法
        context.commit('changeName', newName)
      }, 1000)
    },
    changeNumberAsync(context) {
      context.commit('changeNumberBack', true)
      setTimeout(() => {
        context.commit('changeNumberBack', false)
        context.commit('changeNumber')
      }, 2000)
    }
  },
  getters: {
    decorationName(state) {
      return `我叫${state.name},身高${state.high}cm`
    },
    total(state) {
      return state.price * state.number
    }
  },
})

3. 项目配置

  • 这一步不是必须的,可以通过vue-cli快速创建项目。

  • 不过这个项目比较简单,所以可以尝试自己配置一下,将项目运行起来。

  • 毕竟前面造了个webpack的轮子,所以我了解一点打包原理。有兴趣可以看我前面的文章。

(1)下载webpack

yarn add -D webpack webpack-cli webpack-dev-server

注意:下载webpack的这几个依赖需要注意版本,有版本匹配限制。如果版本不匹配会报错。我项目中用到的是:

"webpack": "4.40.0",
"webpack-cli": "4.9.0",
"webpack-dev-server": "4.0.0"
  • 配置webpack出口文件和入口文件
// webpack.config.js
const path = require('path'); //node内置模块
module.exports = {
  mode: 'development',
  entry: path.join(__dirname, "/src/main.js"), // 入口文件
  output: {
    path: path.join(__dirname, "/dist"), //打包后的文件存放的地方
    filename: "bundle.js" //打包后输出文件的文件名
  }
}
  • 配置打包和启动服务命令
// package.json
// 省略其他内容
"scripts": {
  "build": "webpack",
  "start": "webpack-dev-server"
},

(2)下载vue

  • 项目中用到了Vue,所以先下载vue相关的依赖
yarn add -D vue vue-loader vue-template-compiler

还有css-loader,项目中使用了CSS

yarn add -D vue-style-loader css-loader
  • 配置.vue文件的规则
// webpack.config.js
const { VueLoaderPlugin } = require('vue-loader') // 引入插件
module.exports = {
//省略前面内容...
 module: { // 模块解析
    rules: [{
        test: /\.vue$/, // .vue文件
        loader: 'vue-loader'
      },
      {
        test: /\.css$/, // .css文件
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      }
   }]
  },
  plugins: [
    new VueLoaderPlugin(), // 需要使用插件才能解析
  ]
}

(3)下载bable

  • es6转为es5JS代码解析
yarn add -D babel-loader @babel/core @babel/preset-env
  • 配置.js文件的规则
// webpack.config.js
module.exports = {
 module: { // 模块解析
    rules: [
    // 省略...
     {
        test: /\.js$/, // 解析.js文件
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"]
          }
        }
      }
   }]
 }
}
  • 到这里就可以打包了,执行yarn build 可以打包出bundle.js文件,将文件引入html中就可以使用。

  • 但是这样太麻烦了,所以直接将html也一起放进项目中。

(4)下载html插件

yarn add -D html-webpack-plugin
  • 在项目根目录中,新建public文件
// index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>
    <%=htmlWebpackPlugin.options.title%>
  </title>
</head>

<body>
  <div id="app"></div>
</body>

</html>
  • 配置解析html规则
// webpack.config.js
const HtmlWbpackPlugin = require('html-webpack-plugin')
module.exports = {
  plugins: [
    new VueLoaderPlugin(),// vue插件
    new HtmlWbpackPlugin({
      template: path.join(__dirname, 'public', 'index.html'),// 模板位置
      title: 'index'
    })
  ]
}

(5)配置启动服务

// webpack.config.js
module.exports = {
  // 省略...
  devServer: {
    static: {
      directory: path.join(__dirname, './'),
    },
    port: 8000,
    compress: true,
  },
  plugins: [
  // ...
  ]
}

到这里,就完成项目的配置了。

五、启动项目

执行yarn start,可以查看结果

在这里插入图片描述

六、实现mapState

先将案例一的代码优化一下

  // App.vue
<!-- 案例一 -->
<div>
  <h3>案例一</h3>
  <h1>state测试: <span>{{ compute_name }}</span></h1>
  <h1>getters测试: <span>{{ compute_decorationName }}</span></h1>
  <button @click="changeName()">立刻修改</button>
  <button @click="changeNameAsync()">1s后修改</button>
</div>
    
    // 通过computed获取
computed: {
  compute_name () {
    return this.$store.state.name
  },
  compute_decorationName () {
    return this.$store.getters.decorationName
  }
},

但是如果写多个computed比较麻烦,可以通过mapState方法简化一下。

// myVuex
const mapState = (arr) => { // 传入数组
  if (!Array.isArray(arr))
    throw new Error('当前只支持数组参数')
  let obj = {}
    // 就是接收传递的的参数,去this.$store寻找
  arr.forEach((item) => {
    obj[item] = function() {
      return this.$store.state[item]
    }
  })
  return obj
}

// 导出
export { mapState }
<div>
  <h3>案例二</h3>
  <div>
    <h1>价格:<span>{{ price }}</span> 元</h1>
    <h1>数量:<span>{{ number }}</span> 个</h1>
    <h1>总价:<span>{{ this.$store.getters.total }}</span> 元</h1>
  </div>
  <div>
    <button @click="changePrice()">价格+1</button>
    <button @click="changeNumberAsync()">数量+1</button>
    <span v-if="numberBack">正在查询库存,请稍等...</span>
  </div>
</div>

// 在computed中使用mapState()
computed: {
  compute_name () {
	// ...
  ...mapState(['price', 'number', 'numberBack'])
}

还有mapMutations()mapActions()mapGetters()几个方法也类似,可以自行实现。

七、总结

代码和相关配置可以查看我的github仓库
目前项目整体实现过程比较简单,不过也实现了基本功能,后面有空再改进。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值