使用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
,能够扫描下方二维码加我微信,备注交流。