Handwritten Vuex4.x core (Vuex source implementation)

Implement TodoList through vuex:

Let's make a small TodoList case first, apply vuex to realize it, and then after the logic runs through, we will write our own vuex bit by bit on this basis, and then achieve the same effect as before.

Use vite to create a project:

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

Install vuex:

yarn add vuex@next --save

Delete hellowolrd.vue, create a new store folder under the src directory, and improve the vuex structure:

Introduce in main.js:

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

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

The object we return in index.js under the store directory must contain an install(app). This is determined according to the plugin of vue, and you must install it after using it. It's just that this has been implemented for us inside vuex, but we should pay attention in our own vuex, if we sue in main.js, we must include install in the returned object.

According to the logic of TodoList, let's write the implementation of vuex:

state.js:

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

In the state, todos stores all the information, and the filter indicates the state of the current overall TodoList. If it is all, it will display all the lists, if it is finished, it will display all the completed lists, and if it is unfinished, it will display all the unfinished lists. And each list has a corresponding id.

actions.js:

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

It should not be difficult to understand, that is to submit the specified mutation through actions to change the data in the 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 are similar to computed properties, we use filteredTodos in getters to filter data according to the current state.

Finally, in index.js we pass these into an object and then export it:

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
})

focus:

Because we are going to implement our own vuex. So we need to be very clear about the mutations we submit, the dispatched actions, and the actions and mutations in vuex. What are their parameters?

Submit mutation dispatch action

mutations -> commit(type, payload) type is the name of the mutation

actions -> dispatch(type, payload)

execute actions

action -> (store, payload)

execute mutations

mutation -> (state, payload)

Write the TodoList view:

Create a new TodoList folder in the components directory, and create Form.vue, index.vue, Tab.vue, and Todos.vue. Write the content of these four components separately:

form.view:

<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.view:

<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>

All.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 imports these three components as the entry file:

<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>

Modify voite.config.js, otherwise an error will be reported when running:

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')
    }
  }
})

Now our TodoList is ready, start the project:

Click "Eat", when All is selected:

Selection is done:

Selection not done:

Implement your own vuex

Before implementing our own vuex, let's make an output of the store object to see what the store d in vuex has:

Actions, mutations, and state here are all preceded by "_", which means that vuex wraps the actions, mutations, and state we wrote.

In _state, our data is stored in the data attribute. Where does this data come from? This is also what vuex does for us. In fact, it is reactive which saves an object, and this object contains data = our state.

We mentioned a very important issue earlier, that is, when we call the corresponding mutation and action, the parameters passed in are type and payload. And our two functions of mutations and actions are passed in state, payload and store, payload respectively. In this case, the parameter passing is not uniform, we need to convert it, we can define a function to pass the payload first, then call the mutation, change its this point to store, and then pass in its own state, payload or store , payload.

So this is why he has to save mutations and actions separately, because we need to define a function first, pass in the payload of commit and dispatch, then call mutations and actions, and then pass state, payload or store, and payload to them.

Next, we first create a new my-vuex folder in the src directory, and replace all the places where vuex was used before with our my-vuex:

Now the first thing we need to implement is the createStore function in index.js in the store:

It receives an options parameter, we need to bring in the things written by the user first and throw a store object from useStore when the user uses vuex:

 Then we will first write these two methods for users, createStore and useStore. We create a new store.js under my-vuex to implement these two methods.

The implementation of createStore:

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


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

Because createStore receives the options passed in by the user, we can write a Store class and return the instance object of the Store, which is very easy to expand and maintain.

Then what we introduced in main.js is this instance, and this instance is also passed in use, because we said use before, we must install it. So we have to add this method in the Store class 

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

    install (app) {
        
    }
}

Because it looks for this install first on the instance object, if not, it looks on its prototype object, and our install is hung on the prototype object of the class.

In the store instance we printed out, there are only functions we defined in _mutations and _actions, there is no chain, he doesn't want us to inherit. So we can just create the object directly.

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

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

When users create mutations and actions, they will be added to the corresponding _mutations and _actions, so we create a new creator.js to add the mutations and actions passed in by users to _mutations and _actions.

Because we definitely need to traverse the object passed in by the user and get its key and value to do the following logic, so we create a new utils.js:

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

Now back to our creator.js, let's take a look at the code that handles mutations:

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

Here we call the forEachValueKey defined in utils to traverse the incoming mutations, we get our own defined _mutations in the store, the mutationKey is the type passed in by the user by submitting the commit, and the payload is the second parameter of the commit. Then we call mutationFn in the callback, here we need to change the point of this through apply, because we want this in mutationFn to point to the instance of the class we defined, and then the parameters passed in are state and payload.

The following createActions is the same logic:

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

It is worth noting that 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
        })
    })
}

Because we want to access the data in the state through getters, we can use Object.defineProperty as a layer of proxy.

Now we go back to the store class and call the three functions we just defined:

Now when the user calls createStore, it will return to him the store instance we defined, and then receive the options passed in by the user, then pass the mutations into the _mutations pool, pass the actions into the _actions pool, and call createGetters to define a store.getters object, and then make a layer of proxy.

In this way, the whole logic is much clearer. Now we define the commit method and dispatch method submitted by the user:

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

Because we can access the data in the state through store.state, it is not enough to only have one _state now, because this _state is convenient for our vuex to do other things, and the user does not call it through store._state. So we have to do another layer of proxy:

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

In this way, what we return is the data in the state

In the project, we can get the store instance by calling the useStore method provided by vuex, so how can we make other components in the project get the store instance? In fact, this is cross-component value passing. We want to pass this store instance , we can call the app.provide method in the install method to pass out this instance, and then inject it in the useStore method and receive it, because the install method corresponds to the use in mian.js, we are in the root of vue Using the use method on a component is equivalent to providing a piece of data in the root component, so those who want to use this data in the subcomponent can directly call the useStore method.

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

Here app.config.gloabalPropertries.$store is because it is also compatible with vue2, we can use this.$store.state in the project to access the data in the state.

Here is the useStore method:

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

Now that we have almost written the whole thing, start the project:

Found that an error was reported, he said that _mutations cannot be accessed, that is, this piece cannot be adjusted:

Then let's output this in the commit to see if the store will be printed:

The output on the console is undefined. Then why is he undefined?

Because when the user calls commit, the this point in the commit has changed and no longer points to the current this instance, so we have to do a process of binding the current this environment to the commit and dispatch functions, and we implement this in the creator the code:


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 ])
    }
}

Then in the constructor we deconstruct commit and dispatch from the store and pass in these two functions as parameters:

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
    }
}

Startup project:

Now our entire project can run normally, and the basic functions of our mini-vuex have been realized.

Guess you like

Origin blog.csdn.net/qq_49900295/article/details/126563641