0
点赞
收藏
分享

微信扫一扫

手写 Vuex4.x 核心(Vuex源码实现)

陌岛 2022-09-20 阅读 77

通过 vuex 实现 TodoList :

我们先做一个小的 TodoList 的案例,应用 vuex 来实现,然后逻辑跑通之后再在此的基础上我们一点点手写自己的 vuex ,进而实现和原来一样的效果。

采用 vite 创建项目:

yarn create vite vuex-core-dev --template vue

安装vuex:

yarn add vuex@next --save

删掉 hellowolrd.vue,在 src 目录下新建 store 文件夹,完善 vuex 结构:

在 main.js 中引入:

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import store from './store'

createApp(App).use(store).mount('#app')

根据 TodoList 的逻辑,我们来编写 vuex 的实现:

state.js:

export default {
    todos: [],
    filter: 'all', //all finished unfinished
    id: 0
}

在 state 中 todos 存储着所有信息,filter 则是当前整体的 TodoList 处于什么状态,是 all 的话就显示所有 list,是 finished 就显示全部已完成的 list,unfinished 就显示全部未完成的 list。并且每条 list 都有对应的 id。

actions.js:

export default {
    addTodo ({ commit }, text) {
        commit('addTodo', text)
    },
    toggleTodo ({ commit }, id) {
        commit('toggleTodo', id)
    },
    removeTodo ({ commit }, id) {
        commit('removeTodo', id)
    }
}

这应该不难理解,就是通过 actions 来提交指定的 mutation 进而改变 state 中的数据将。

mutations.js:

export default {
    addTodo (state, text) {
        state.todos.push({
            id: state.id++,
            text,
            isFinished: false
        })
    },
    toggleTodo (state, id) {
        state.todos = state.todos.map(item => {
            if (item.id === id) {
                item.isFinished = !item.isFinished
            }
            return item
        })
    },
    removeTodo (state, id) {
        state.todos = state.todos.filter(item => {
            if (item.id !== id) {
                return true
            }
        })
    },
    setFilter (state, filter) {
        state.filter = filter
    }
}

getters.js:

export default {
    finishedTodos (state) {
        return state.todos.filter(todos => todos.isFinished)
    },
    unfinishedTodos (state) {
        return state.todos.filter(todos => !todos.isFinished)
    },
    filteredTodos (state, getters) {
        switch (state.filter) {
            case 'finished':
                return getters.finishedTodos
            case 'unfinished':
                return getters.unfinishedTodos  
            default:
                return state.todos     
        }
    }
}

getters 就类似于计算属性,我们在 getters 中通过 filteredTodos 来根据当前状态筛选数据。

最后在 index.js 中我们把这些传入一个对象再导出出去:

import state from "./state";
import getters from "./getters";
import mutations from "./mutations";
import actions from "./actions";

import { createStore } from 'vuex'

export default createStore ({
    state,
    getters,
    mutations,
    actions
})

重点:

因为我们要实现自己的 vuex 。所以我们要非常清楚我们提交的 mutation ,派发的 action 和 vuex 中的 actions,mutations 他们的参数都是什么

编写 TodoList 视图:

在 components 目录下新建 TodoList 文件夹,创建 Form.vue,index.vue,Tab.vue,Todos.vue。分别别编写这四个组件的内容:

Form.vue:

<template>
    <div>
       <input
       type="text"
       placeholder="Please input something"
       v-model="inputRef"
       />
       <button @click="addTodo">ADD TODO</button>
    </div>
</template>

<script>
import { ref } from 'vue'
import { useStore } from 'vuex'
    export default {
        setup () {
            const store = useStore()
            const inputRef = ref('')
        
            const addTodo = () => {
                store.dispatch('addTodo', inputRef.value)
                inputRef.value = ''
            }
            return {
                inputRef,
                addTodo
            }
        }
        
    }
</script>

<style lang="scss" scoped>

</style>

Tab.vue:

<template>
    <div>
        <a href="javascript:;"
        @click="setFilter('all')"
        :class="{ active: store.state.filter === 'all' }"
        >All</a>
        <a href="javascript:;"
        @click="setFilter('finished')"
        :class="{ active: store.state.filter === 'finished' }"
        >Finished</a>
        <a href="javascript:;"
        @click="setFilter('unfinished')"
        :class="{ active: store.state.filter === 'unfinished' }"
        >unFinished</a>
    </div>
</template>

<script>
import { useStore } from 'vuex'
    export default {
        setup () {
            const store  = useStore()

            const setFilter = (filter) => {
                store.commit('setFilter',filter)
            }
            return {
                store,
                setFilter
            }
        }
    }
</script>

<style lang="scss" scoped>
    a {
        margin-right:15px;
    }
    .active {
        text-decoration: none;
        color: #000;
    }
</style>

Todos.vue:

<template>
    <div>
        <div v-for="item of store.getters.filteredTodos"
          :key="item.id"
        >
            <input type="checkbox"
            :checked="item.isFinished"
            @click="toggleTodo(item.id)"
            >
            <span :class="{ finished: item.isFinished }">{{item.text}}</span>
            <button @click="removeTodo(item.id)">DELETE</button>
        </div>
    </div>
</template>

<script>
import { useStore } from 'vuex'
    export default {
        setup () {
            const store = useStore()

            const toggleTodo = (id) => {
                store.dispatch('toggleTodo',id)
            }

            const removeTodo = (id) => {
                store.dispatch('removeTodo',id)
            }
            return {
                store,
                toggleTodo,
                removeTodo
            }
        }
    }
</script>

<style lang="scss" scoped>
.finished {
    text-decoration: line-through;
}
</style>

index.vue 作为入口文件将这三个组件导入:

<template>
    <div>
        <todo-tab></todo-tab>
        <todo-form></todo-form>
        <todos></todos>
    </div>
</template>

<script>
import TodoTab from './Tab'
import TodoForm from './Form'
import Todos from './Todos'
    export default {
        components: {
            TodoTab,
            TodoForm,
            Todos
        }
    }
</script>

<style lang="scss" scoped>

</style>

修改一下 voite.config.js,否则运行会报错:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    extensions: ['.vue', '.js'],
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
})

现在我们的 TodoList 就做好了,启动项目:

点击“吃饭”,当选择  All 时:

选择已完成:

选择未完成:

实现自己的 vuex

在实现我们自己的 vuex 之前,我们先把 store 对象做一个输出,看看 vuex 里的 store d都有啥:

这里的 actions,mutations,state 的前面都加了 " _ ",这就是 vuex 把我们写的 actions,mutations,state给包装了。

在 _state 中,我们的数据保存在 data 属性里,这个 data 是哪来的呢,这也是 vuex 帮我们做的,其实就是 reactive 它保存了一个对象,这个对象里面是 data = 我们的 state。

所以这就是为什么他要单独保存 mutations 和 actions,因为我们要首先定义一个函数,把 commit 和 dispatch 的 payload 给传进来,然后再调用 mutations 和 actions,再把 state,payload 或 store,payload 传递给他们。

下面我们先在 src 目录下新建 my-vuex 文件夹,把之前所有用到 vuex 的地方都换成我们的 my-vuex:

现在我们首先要实现的就是 store 里的 index.js 中的 createStore 这个函数:

它接收一个 options 参数,我们要把用户写的东西先拿进来并且在用户使用 vuex 的过程中,会从 useStore 里抛出一个 store 对象:

 那我们就先写抛给用户用的这两个方法,createStore 和 useStore。我们在 my-vuex 下新建 store.js 来实现这两个方法。

createStore 的实现:

class Store {
    constructor (options) {
        const {
            state,
            getters,
            mutations,
            actions
        } = options
    }
}


export function createStore (options) {
    return new Store(options)
}

因为 createStore 接收用户传进来的 options,所以我们可以写一个 Store 类,返回的就是 Store 的实例对象,这样就非常易于扩展和维护。

那我们在 main.js 中引入的就是这个实例,use 中传入的也是这个实例,因为我们之前说过 use 了,就必须 install。所以我们得在 Store 类中添加这个方法 

class Store {
    constructor (options) {
        const {
            state,
            getters,
            mutations,
            actions
        } = options
    }

    install (app) {
        
    }
}

因为它找这个 install 就是先在实例对象上找,没有的话就去它的原型对象上找,而我们的 install 就是挂在类的原型对象上的。

在我们打印出的 store 实例中,_mutations 和 _actions 里面只有我们定义的函数,没有什么链,他不想让我们继承。所以我们就可以直接创建对象就行了。

constructor (options) {
        const {
            state,
            getters,
            mutations,
            actions
        } = options

        this._state = reactive({ data: state })
        this._mutations = Object.create(null)
        this._actions = Object.create(null)
    }

用户在创建 mutation 和 action 的时候都会被添加到对应的 _mutations 和 _actions,所以我们就新建一个 creator.js ,来把用户传进来的 mutations 和 actions 都给添加进 _mutations 和 _actions。

因为我们肯定需要遍历用户传进来的对象并且拿到他的 key,value,来做下面的逻辑,所以我们再新建一个 utils.js :

// 对 mutations,actions 做一个循环。传入一个回调函数,参数就是我们获取的键值对,然后在里面做一些事情
export function forEachValueKey (obj, callback) {
    Object.keys(obj).forEach(key => callback(obj[key], key))
}

现在回到我们的 creator.js,下面看一下处理 mutations 的代码:

export function createMutations (store, mutations) {
    forEachValueKey(mutations, (mutationFn, mutationKey) => {
        store._mutations[mutationKey] = (payload) => {
            mutationFn.apply(store, [ store.state, payload ])
        }
    })
}

下面 createActions 是一样的逻辑:

export function createActions (store, actions) {
    forEachValueKey(actions, (actionFn, actionKey) => {
        store._actions[actionKey] = (payload) => {
            actionFn.apply(store, [ store, payload ])
        }
    })
}

值得注意的是 createGetters :

export function createGetters (store, getters) {
    store.getters = {}
    forEachValueKey(getters, (getterFn, getterKey) => {
        Object.defineProperty(store.getters, getterKey, {
            get: () => computed(() =>  getterFn(store.state, store.getters)).value
        })
    })
}

因为我们要通过 getters 来访问 state 中的数据,所以可以用 Object.defineProperty 来做一层代理。

现在我们回到 store 类中,调用刚才定义好的三个函数:

这样整个逻辑就清晰多了,现在我们定义用户提交的 commit 方法和 dispatch 方法:

// 用户提交 commit 就是调用指定的 mutations
    commit(type, payload) {
        this._mutations[type](payload)
    }
    dispatch(type, payload) {
        this._actions[type](payload)
    }

因为我们可以通过 store.state 来访问到 state 里的数据,所以现在只有一个 _state 是不行的,因为这个 _state 是方便我们的 vuex 做其他事情的,用户并不是通过 store._state 来调用的,所以我们还得再做一层代理:

get state() {
        return this._state.data
    }

这样我们返回的就是 state 中的数据了

install (app) {
        app.provide('store', this)
        app.config.globalProperties.$store = this
    }

这里 app.config.gloabalPropertries.$store 是因为还要兼容 vue2,我们可以在项目中用 this.$store.state 来访问 state 中的数据。

下面是 useStore 方法:

export function useStore () {
    return inject('store')
}

现在我们整个写的已经差不多了,启动项目:

发现报错了,他说 _mutations 访问不到,也就是这块它调不到:

那我们在 commit 里输出一下 this 看看会不会打印 store:

控制台上输出的是 undefined。那他为啥是 undefined 呢?

因为用户在调用 commit 的时候,commit 里的 this 指向已经改变了,不再指向当前 this 实例了,所以我们得对 commit 和 dispatch 函数做一个绑定当前 this 环境的处理,在 creator 中我们实现这块的代码:


export function createCommitFn (store, commit) {
    store.commit = function (type, payload) {
        commit.apply(store, [ type, payload ])
    }
}

export function createDispatchFn (store, dispatch) {
    store.dispatch = function (type, payload) {
        dispatch.apply(store, [ type, payload ])
    }
}

然后在 constructor 中我们从 store 中解构出 commit 和 dispatch 作为参数传入这两个函数:

class Store {
    constructor (options) { //把用户传进来的 options 做初始化
        const {
            state,
            getters,
            mutations,
            actions
        } = options
        const store = this
        const { commit, dispatch } = store
        store._state = reactive({ data: state })
        // 定义两个池子装用户定义的 Mutaions 和 actions
        store._mutations = Object.create(null)
        store._actions = Object.create(null)

        createMutations(store, mutations)
        createActions(store, actions)
        createGetters(store, getters)
        createCommitFn(store, commit)
        createDispatchFn(store, dispatch)
    }
    // 用户提交 commit 就是调用指定的 mutations
    commit(type, payload) {
        console.log(this);
        this._mutations[type](payload)
    }
    dispatch(type, payload) {
        this._actions[type](payload)
    }
    get state() {
        return this._state.data
    }
    install (app) {
        app.provide('store', this)
        app.config.globalProperties.$store = this
    }
}

启动项目:

现在我们整个项目都能正常运行了,我们的 mini-vuex 的基本功能也就实现了。

举报

相关推荐

0 条评论