使用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
,能夠掃描下方二維碼加我微信,備註交流。