文章目录
一、整体结构
- 开始之前,先来对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. 初始化
实例化该类时会传入state
、mutation
等,需要在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.defineProperty
对getters
的属性进行劫持,设置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
转为es5
和JS
代码解析
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
- 在项目根目录中,新建
publi
c文件
// 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仓库
目前项目整体实现过程比较简单,不过也实现了基本功能,后面有空再改进。