從0到1實現一個簡易版Vuex

前言

使用vue做爲主力開發技術棧的小夥伴,vuex你們在工做中必不可少,面試的時候面試官也多多少少會問一些關於vuex內部機制的問題,小夥伴們只能是去閱讀vuex的源碼,但不能否認,有些小夥伴們閱讀起來源碼多少有些吃力,so本文即將帶着你們來實現一個簡化版的vuexhtml

vuex的工做流程以下圖所示 vue

組件內經過dispatch調用actions
actions經過commit提交mutation
mutation修改state
state響應式更新 到組件上
我們根據這個流程開始寫代碼

準備工做

我們來建立個myvuex目錄來編寫我們的代碼,而後打開終端執行yarn init -y 或者 npm init -ywebpack

考慮到有些小夥伴對rollup有些陌生,構建工具我們這裏選用的是webpackgit

構建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項目中是否能夠正常引入

而後啓動我們的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類接受一系列參數stateactions,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項目

咱們來打印一下store

能夠看到我們的store已經正常打印出來了

接着我們該怎麼讓我們定義的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>
複製代碼

一執行發現並無效果 並向咱扔出了一個錯誤, 彆着急,我們一步步來 ,上文中提到的install方法我們和Store類只是定義了尚未進一步完善,因此vue實例中並無我們的$store

// 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中的數據了

固然了,目前這樣確定不行,vuex中的state也是響應式的,我們也得想辦法把我們的state處理一下,實現數據響應式的方案有不少,好比說Object.defineProperty、Proxy...,可是這些寫起來仍是很麻煩的,其實我們可使用一個更加巧妙的方式,那就是藉助vue自己的數據響應式來處理,既簡單又高效 接下來改造一下我們代碼

首先在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裏,我們把這個地方單獨拿出來,放在一個函數裏邊

接下來考慮一下,vuex的getter是否是和vue的computed很像,都是在獲取數據的時候有機會把數據修改成頁面中想要的格式,既然是這樣,我們的getter也就很好實現了,下面接着來實現我們的getter

首先寫一個包裝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,來看一下效果

能夠看到用法基本與vuex一致且已經有了效果 OK 到這裏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])
})
複製代碼

在vuex中不能直接調用mutation,而是須要使用store.commit
咱們在Store類中加入commit方法
這個commit函數實現很簡單,就是把我們包裝後的mutation執行一下而已

commit(type, payload) {
    const handler = this._mutations[type]
    handler(payload)
}
複製代碼

我們來看下效果

效果雖然出來了,可是這樣真的就能夠了麼??在我們的commit函數裏我們直接 const handler = this._mutations[type]這樣在this上邊獲取mutation,正常使用雖然沒有問題,可是保不齊this指向錯誤的地方,js的this有多頭疼你懂的。。。我們來處理一下,把this固定到Store類上,改造以前我們先來模擬下this指向不對的狀況
點擊button以後,程序就蹦了,這樣確定是不行的
動手改造,在constructor裏增長以下代碼

const store = this
let {
    commit
} = this
this.commit = function boundCommit(type, payload) {
    return commit.call(store, type, payload)
}
複製代碼

代碼很好理解,不作贅述了。
代碼執行👌,可是循環遍歷包裝getters和mutations的時候,代碼仍是散在constructor裏的,雖然這樣也能夠,隨着代碼量的增長,這樣會顯得很亂,增長閱讀代碼的難度,我們給他抽出來單獨放在一個函數裏

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變成這樣

而後就是Action
Action 函數接受一個與 store 實例具備相同方法和屬性的 context 對象,所以你能夠調用 context.commit 提交一個 mutation,或者經過 context.state 和 context.getters 來獲取 state 和 getters
老規矩

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)
}
複製代碼

測試一下

看似沒有問題,可是這樣真的行了麼?? 在vuex中爲了數據流向可控,在嚴格模式中只能經過mutation來修改state,在其餘地方修改state會報錯,這塊我們尚未處理。

我們的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
複製代碼

這個_committing爲true的時候能夠修改state,爲false的時候不可修改,這樣我們在mutation中修改的state時候先改變下_committing這個狀態就能夠了,由於在內部修改state的時候也須要修改_committing,這裏我們把代碼單獨拉出來寫,封裝爲一個類方法,其餘地方用的時候也方便

_withCommit(fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
}
複製代碼

寫完以後,修改下我們的commit方法,這樣我們就是實如今只能經過mutation來修改state

測試一下

Ok,我們本身的myvuex就實現了,代碼還有不少優化和待實現的地方,你們能夠從github上把代碼clone下來,優化和完善代碼, github地址

總結

總得來講vuex實現起來仍是很簡單的,在這個代碼基礎上很容易拓展出完整的vuex,哈哈,由於文中的代碼就是參考vuex源碼來寫的,這樣你們看完這篇文章再去閱讀vuex源碼就能輕鬆很多,也是考慮到實現完整版的意義不是很大,把vuex的實現方式和思想告訴你們纔是最重要的,說白了,vuex的本質也是一個vue實例,它裏面管理了公共部分數據state。

Thank You

篇幅很大,感謝你們耐心觀看,文中若有錯誤歡迎指正,若有什麼好的建議也能夠在評論區評論或者加我微信交流。祝你們身體健康

在這裏插入圖片描述

我是 Colin,能夠掃描下方二維碼加我微信,備註交流。

相關文章
相關標籤/搜索