使用vue做为主力开发技术栈的小伙伴,vuex你们在工做中必不可少,面试的时候面试官也多多少少会问一些关于vuex内部机制的问题,小伙伴们只能是去阅读vuex的源码,但不能否认,有些小伙伴们阅读起来源码多少有些吃力,so本文即将带着你们来实现一个简化版的vuexhtml
vuex的工做流程以下图所示 vue
我们来建立个myvuex
目录来编写我们的代码,而后打开终端执行yarn init -y
或者 npm init -y
。webpack
考虑到有些小伙伴对rollup
有些陌生,构建工具我们这里选用的是webpack
git
构建webpack开发环境,本文并不打算展开说webpack,so 我就把webpack用到的依赖包一气下完了github
$ yarn add webpack webpack-cli webpack-dev-server webpack-merge clean-webpack-plugin babel-loader @babel/core @babel/preset-env
复制代码
而后开始编写我们的webpack配置文件,并建立一个build目录存放web
// webpack.config.js
const merge = require("webpack-merge");
const baseConfig = require("./webpack.base.config");
const devConfig = require("./webpack.dev.config");
const proConfig = require("./webpack.pro.config");
let config = process.NODE_ENV === "development" ? devConfig : proConfig;
module.exports = merge(baseConfig, config);
// webpack.base.config.js
const path = require("path");
module.exports = {
entry: path.resolve(__dirname, "../src/index.js"),
output: {
path: path.resolve(__dirname, "../dist"),
filename: "myvuex.js",
libraryTarget: "umd"
},
module: {
rules: [
{
test: /\.js$/i,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}
}
]
}
};
// webpack.dev.config.js 开发环境配置
module.exports = {
devtool: 'cheap-module-eval-source-map'
}
// webpack.pro.config.js 生成环境配置
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
plugins: [
new CleanWebpackPlugin() // 构建成功后清空dist目录
]
};
// package.json
{
"name": "myvuex",
"version": "1.0.0",
"main": "src/index.js",
+ "scripts": {
+ "start": "webpack-dev-server --mode=development --config ./build/webpack.config.js",
+ "build": "webpack --mode=production --config ./build/webpack.config"
},
"files": [
"dist"
],
"license": "MIT",
"dependencies": {
"@babel/core": "^7.8.4",
"@babel/preset-env": "^7.8.4",
"babel-loader": "^8.0.6",
"clean-webpack-plugin": "^3.0.0",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.2",
"webpack-merge": "^4.2.2"
}
}
复制代码
webpack搭建好了以后,咱们在用vue-cli建立一个我们的测试项目面试
$ vue create myvuextest
复制代码
而后使用yarn link 创建一个连接,使咱们可以在myvuextest
项目中使用myvuex
,对yarn link不熟悉的小伙伴能够查看yarn linkvuex
myvuex 项目 vue-cli
myvuextest 项目shell
完事以后 咱们就能够经过import引入myvuex了,以下图所示
回到我们的myvuex 在根目录建立一个index.js 文件测试一下在myvuextest项目中是否能够正常引入
能够看到我们在myvuex项目中编写代码能够在myvuextest中正常使用了
准备工做完成以后,能够正式开始编写我们的项目代码了
建立一个src目录存放项目主要代码
而后建立一个store.js
接下来我们看看根据vuex的用法我们的myvuex该如何使用,我们根据需求完善逻辑
import Vue from "vue";
import MyVuex from "myvuex";
Vue.use(MyVuex)
const store = new MyVuex.Store({
state: {},
actions: {},
mutations: {},
getters: {}
})
export default store
复制代码
能够看到,我们须要一个 Store
类,而且还要使用Vue.use挂载到vue上面,这就须要咱们提供一个 install
方法供vue调用,Store类接受一系列参数state
、actions
,mutations
,getters
等...
我们先动手建立一个Store类,和一个install方法
// src/store.js
export class Store {
constructor() {
}
}
export function install() {
}
复制代码
并在index.js中导出供myvuextest
使用
import {
Store,
install
} from "./store";
export default {
Store,
install
}
复制代码
回过头来看下myvuextest
项目
接着我们该怎么让我们定义的state渲染到页面上呢
// myvuextest/store/index.js
import Vue from "vue";
import MyVuex from "myvuex";
Vue.use(MyVuex)
const store = new MyVuex.Store({
state: {
title: "hello myvuex"
}
})
export default store
// App.vue
<template>
<div id="app">{{ $store.state.title }}</div>
</template>
<script>
export default {
name: "app"
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
font-size: 30px;
margin-top: 60px;
}
</style>
复制代码
// myvuex/src/store.js
export class Store {
constructor(options = {}) {
this.state = options.state
this.actions = options.actions
this.mutations = options.mutations
this.getters = options.getters
}
}
export function install(Vue) {
Vue.mixin({
beforeCreate() {
const options = this.$options
if (options.store) {
/*存在store其实表明的就是Root节点,直接使用store*/
this.$store = options.store
} else if (options.parent && options.parent.$store) {
/*子组件直接从父组件中获取$store,这样就保证了全部组件都公用了全局的同一份store*/
this.$store = options.parent.$store
}
}
})
}
复制代码
install中使用的this.$options就是我们new Vue时传入的参数,我们的store就是在这传给vue的
写完后,我们的store中的参数都挂载到了vue身上,因此这个时候咱们打开页面就能够看到我们state中的数据了
首先在install方法中把我们Vue存一下
// myvuex/src/store.js
let Vue;
export function install(_Vue) {
Vue = _Vue
Vue.mixin({
beforeCreate() {
const options = this.$options
if (options.store) {
this.$store = options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
})
}
复制代码
而后把Store中的state修改一下
// myvuex/src/store.js
export class Store {
constructor(options = {}) {
let {
state
} = options
this.actions = options.actions
this.mutations = options.mutations
this.getters = options.getters
this._vm = new Vue({
data: {
$$state: state
}
})
}
// 访问state的时候,返回this._vm._data.$$state中的数据
get state() {
return this._vm._data.$$state
}
}
复制代码
这样咱们就基本实现了数据响应式更新
我们先写个定时器改下state中的title测试一下
可是我们的代码不能一直都堆在constructor里,我们把这个地方单独拿出来,放在一个函数里边
首先写一个包装getter的函数,把getter用的state以及getters参数传过去
function registerGetter(store, type, rawGetter) {
store._getters[type] = function () {
return rawGetter(store.state, store.getters)
}
}
复制代码
接着在把constructor中的this.getters = options.getters
改成this._getters = Object.create(null)
用来存放getters
而后调用我们的registerGetter
函数包装一下getter
而后把resetStoreVm函数改造为
function resetStoreVm(store, state) {
store.getters = {}
let computed = {}
let getters = store._getters
Object.keys(getters).forEach(key => {
// 把getter函数包装为computed属性
computed[key] = () => getters[key]()
// 监听是否用getters获取数据,computed是把我们的数据直接存到根结点的,全部直接在_vm上边获取到数据返回出去就行
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true
})
})
store._vm = new Vue({
data: {
$$state: state
},
computed
})
}
复制代码
到这里我们已经利用vue的computed属性实现了getter,来看一下效果
接下来实现mutation
mutation做为更改 Vuex 的 store 中的状态的惟一方法,可谓是重中之重,我们一块儿来实现一下 跟getter同样 也须要一个包装mutation的函数
function registerMutation(store, type, handler) {
store._mutations[type] = function (payload) {
return handler.call(store, store.state, payload)
}
}
复制代码
而后在constructor中把this._mutations改成this._mutations = Object.create(null),接着循环遍历options.mutations
Object.keys(options.mutations).forEach(type => {
registerMutation(this, type, options.mutations[type])
})
复制代码
commit(type, payload) {
const handler = this._mutations[type]
handler(payload)
}
复制代码
我们来看下效果
const handler = this._mutations[type]
这样在this上边获取mutation,正常使用虽然没有问题,可是保不齐this指向错误的地方,js的this有多头疼你懂的。。。我们来处理一下,把this固定到Store类上,改造以前我们先来模拟下this指向不对的状况
const store = this
let {
commit
} = this
this.commit = function boundCommit(type, payload) {
return commit.call(store, type, payload)
}
复制代码
function register(store, options) {
Object.keys(options.getters).forEach(type => {
registerGetter(store, type, options.getters[type])
})
Object.keys(options.mutations).forEach(type => {
registerMutation(store, type, options.mutations[type])
})
}
复制代码
constructor变成这样
function registerAction(store, type, handler) {
store._actions[type] = function (payload) {
handler.call(store, {
dispatch: store.dispatch,
commit: store.commit,
getters: store.getters,
state: store.state
}, payload)
}
}
复制代码
而后在我们的register函数中循环遍历options.actions
Object.keys(options.actions).forEach(type => {
registerAction(store, type, options.actions[type])
})
复制代码
以及把this固定到Store类上
let {
commit,
dispatch
} = this
this.commit = function boundCommit(type, payload) {
return commit.call(store, type, payload)
}
this.dispatch = function boundDispatch(type, payload) {
return dispatch.call(store, type, payload)
}
复制代码
我们的state就是vue的data,vue的vm.$watch
属性恰好就是观察Vue实例上的一个表达式或者一个函数计算结果的变化,我们能够借助vm.$watch
来作,在resetStoreVm函数中加上以下代码
store._vm.$watch(function () {
return this._data.$$state
}, () => {
throw new Error("state 只能经过mutation修改")
}, {
deep: true,
sync: true
})
复制代码
光监听一下的话,问题有来了,mutation也是直接修改state,那么这个watch连在mutation中修改的state也会报错,因此我们加一个状态来标示是否能够修改state
this._committing = false
复制代码
_withCommit(fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
复制代码
写完以后,修改下我们的commit方法,这样我们就是实如今只能经过mutation来修改state
总得来讲vuex实现起来仍是很简单的,在这个代码基础上很容易拓展出完整的vuex,哈哈,由于文中的代码就是参考vuex源码来写的,这样你们看完这篇文章再去阅读vuex源码就能轻松很多,也是考虑到实现完整版的意义不是很大,把vuex的实现方式和思想告诉你们才是最重要的,说白了,vuex的本质也是一个vue实例,它里面管理了公共部分数据state。
篇幅很大,感谢你们耐心观看,文中若有错误欢迎指正,若有什么好的建议也能够在评论区评论或者加我微信交流。祝你们身体健康
我是
Colin
,能够扫描下方二维码加我微信,备注交流。