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