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

相关文章
相关标签/搜索