[Study Notes] 基于Vuex的状态管理

Module Introduction

Previous state management:

  • props
  • custom event
  • event bus

vue-x:

  • better way of state management
  • suitable for bigger app

Why a Different State Management May Be Needed

  • Traditional way (child A-> parent -> child B): becomes harder with more layers
  • Event Bus (child A -> sibling child B): One Bus will quickly get crowded with a lot of different emits
    • It is hard to track changes
    • suitable for medium sized app

What we need:

  • clear separation of concerns
  • easy to track changes

Understanding “Centralized State”

Using a Central State
Using a central state: we have one file where we store our application state. Any state shared in different components should be stored and fetching in the file.

Using the Centralized State

  1. create store folder in src, and create store.js
  2. install vuex

    npm install –save vuex

  3. Create store instance in store.js

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export const store = new Vuex.Store({
    state: {
        counter: 0
    }
});
  1. register store instance in root vue (App.vue)
... // same to sample
import { store } from './store/store';

new Vue({
    el: '#app',
    store,
    render: h => h(App)
})
  1. use store: store the state in the centralize store, without taking the route over App.vue file, it can communicate with it directly.
e.g.,
increment(){
    this.$store.state.counter++;
}

Why a Centralized State Alone Won’t Fix It

Visit it directly is not enough safe ➔ hard to find error

Understanding Getters

Using Getter to Get the State

Using Getters

export const store = new Vuex.Store({
    state: {
        counter: 0
    },
    getters:{
        doubleCounter: state => {
            return state.counter * 2;
        }
    }
});
export default{
    computed: {
        counter(){
            return this.$store.getters.doubleCounter;
        }
    }
}

save time to write duplicate code.

Mapping Getters to Properties

Use mapGetters to fetch states(getters) to set the properties in vue instance automatically and give access to all needed states ➔ convenient and save time

import { mapGetter } from 'vuex';
export default{
    computed: mapGetters([ // it is able to pass an object and map Getters to different names
        'doubleCounter',
        'stringCounter'
    ])
}

Merge getters and local properties:
1. merging with ES6’s feature, install babel-preset-stage-2

npm install –save-dev babel-preset-stage-2

  1. set presets in .babelrc
{
    "presets": [
        ["es2015", { "modules": false }],
        ["stage-2"]
    ]
}
  1. merge mapGetters and custom local properties:
import { mapGetter } from 'vuex';
export default{
    computed: {
        ...mapGetters([ 
            'doubleCounter',
            'stringCounter'
        ]),
        ourOwn(){
            ...
        }
    }
}

Understanding Mutations

It is hard to track the changes by accessing the state and manipulating it directly.

Mutation: committed, change state; its usage is similar to getters(like a setter)
If basically we commit such a mutation from one point or component, it will update the state and all components listening to getters will automatically receive the update states.

Using Mutations to Change the State

Using Mutations

Define:

export const store = new Vuex.Store({
    state: {
        counter: 0
    },
    getters:{
        doubleCounter: state => {
            return state.counter * 2;
        }
    },
    mutations:{
        increment: state => {
            state.counter++;
        }
    }
});

Use:

methods:{
    increment(){
        this.$store.commit('increment'); // passing a string
    }
}

Merge mutations and custom methods:

import { mapMutation } from 'vuex';
export default{
    methods: {
        ...mapMutations([ 
            'increment',
            'decrement'
        ]),

    }
}

change the operation in one central place. more convenient, easy access,
有点像面向对象的get, set, method这样封装数据。

Why Mutations have to run Synchronously

Mutations always have to be synchronously ➔ must not run any asynchronous task in sets up a mutation.
Since state gets change from different sources and you cannot tell order of changes matches to the order of mutations because some mutation might take longer that others.
Mutations must runs changes of state immediately.

How Actions improve Mutations

Action: we dispatch action form the component or by the component where we then we commit the mutation and we only commit the mutation once the asynchronous task is done.
Using Actions to Commit Mutation
All the changes to our state still happen synchronously but with action we are still able to execute async code for making this change.

(like response to server, long taking async calculation, etc)

Using Actions

Define actions in the store instance:

export const store = new Vuex.Store({
    state: {
        counter: 0
    },
    getters:{
        doubleCounter: state => {
            return state.counter * 2;
        }
    },
    mutations:{
        increment: state => {
            state.counter++;
        }
    },
    actions: {
        increment: context => { // take context as an argument which give access to the commit method
            context.commit('increment'); // put out the commit of change
        },
        // increment: ({commit}) => { 
        //      commit('increment'); // put out the commit of change
        },
        asyncIncrement: ({commit}) => { 
            setTimeout(() => {
                commit('increment'); // put out the commit of change
            }, 1000);    
        }
    }
});

Use in the vue instance:

import { mapActions } from 'vuex';
export default{
    methods: {
        ...mapActions([ 
            'increment',
            'asyncIncrement'
        ]),

    }
}

Mapping Actions to Methods

  1. Call the actions in vue instance’s method
methods:{
    ...,
    increment(){
        this.$store.dispatch('increment');
    }
}
  1. Passing a payload with action to mutations
    It can pass either a number or string.
    It can pass multiple arguments by an object as a payload where all key values are set up as the properties need to use in the mutation.
export const store = new Vuex.Store({
    state: {
        counter: 0
    },
    mutations:{
        increment: (state, payload) => {
            state.counter += payload;
        }
    },
    actions: {
        increment: ({commit}, payload) => { 
            commit('increment', payload);
        }
    }
});
<template>
    <button .. @click="increment(100)">Increment</button>
</template>

A Summary of Vuex

  • everything set up in store
  • set up initial state in state
  • getters:
    • reusable code for accessing
    • mapGetters: map getters automatically to access state or property
  • mutations: adjust or change state
    • take the state as input and optionally to get a payload
    • in mutation directly manipulate the state and then overwrite the old state
    • can commit mutations directly from components (non-asyn)
    • mapMutations
  • actions: if run asynchronous task
    • set up: first argument is context which is almost the same as the store but does have some minor differences
    • need to commit method which is used to commit the mutation
    • async code isn’t in mutation: use async code in our actions as long as committing is always done synchronously
    • get a payload to pass parameter
    • use mapActions to map automatically to access action

no using asynchronous mutations ➔ no need to use action

Two-Way-Binding (v-model) and Vuex

  1. separate v-model: bind the getter value to value, and add an input listener to make mutation to the state

  2. Set up get() and set() in computed property

<template>
    <input type="text" v-model="value">
    <p>{{ value }} </p>
</template>
export default{
    computed: {
        value: {
            get(){
                return this.$store.getters.value;
            },
            set(value){
                this.$store.dispatch('update'Value', value);
            }
        }
    }
}

It is rare case, just for v-model. This solution should be used with caution.

Improving Folder Structures

Use modules: create a modules folder in store folder

Modularizing the State Management

Split up over multiple modules which already makes it easier to read

  1. extra properties (functions) to a new js file in modules
const state = {...};
const getters = {...};
...
  1. export the properties
export default{
    state, // state: state
    getters,
    ...
}
  1. merge properties in store.js ➔ we only need one centralized store
export const store = new Vuex.Store({
    state: ...,
    getters: ...,
    ...,
    modules: {
        counter, // counter: counter
    }
})

Using Separate Files

Sometimes we have some state, actions, getters which don’t really belong into one module.

Separate actions, mutations, getters in different files and then centralize them in the store.js

e.g.,

// actions.js
export const updateValue = ({commit}, payload) => {
    commit('updateValue', payload);
}

Import all properties in store.js;

import * as actions from './actions'; // import all data exported in actions.js
import * as mutations from './mutations';
import * as getters from './getters';

export const store = new Vuex.Store({
    state: {
        value: 0
    }
    getters,
    mutations,
    actions,
    modules: {
        counter
    }
})

Now we have a lean store.js. We outsource our centralized tasks and actions, getters, mutations file and we have modules for more specialized things.

Using Namespaces to Avoid Naming Problems

Duplicated keys may happen because properties are defined in different files:

There are two solution to avoid it:
1. create a new file where we can set up some constants which will get unique names which are all stored in this file. // not necessary, it is one of solutions.
But in large app, it might be worth since it really ensure that you are not using same name twice.
2. be careful when assigning names // no examples here

// types.js // it is in store folder
export const DOUBLE_COUNTER = 'counter/DOUBLE_COUNTER' ;
export const CLICK_COUNTER = 'counter/DOUBLE_COUNTER' ;
// counter.js
import * types from '../types';

const getters = {
    [types.DOUBLE_COUNTER]: state => {
        return state.counter * 2
    }
},
...
// use in vue instance
<script>
    import {mapGeters} from 'vuex';
    import * types from '../types';
    export default{
        ...mapGetters({
            doubleCounter: types.DOUBLE_COUNTER,
            stringCounter: types.CLICK_COUNTER
        })
    }
</script>

Auto-namespacing with Vuex 2.1

If you’re using Vuex version 2.1 or higher, you may use its auto-namespacing feature to avoid having to set up all the namespaces manually: https://github.com/vuejs/vuex/releases/tag/v2.1.0

Modules can now be auto-namespaced by using the new namespaced: true option in store instance.

Summary:

  • communication between different components
  • centralized store
  • modularing
  • separating properties in different files:
    • getters
    • mutations
    • actions
  • It’s up to you to decide if it’s worth the extra effort especially when diving into name spaces
    • It is worth for JC project (very big, almost most of us should be in that team). But not need for GI MWA(just me develop it)

Useful links:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
很抱歉,我无法在这里提供完整的代码。但是,我可以为你提供一些代码示例以供参考。 以下是一个简单的笔记管理APP的代码示例,其中包括笔记的添加、编辑、删除、分类和标签等功能: ```java public class MainActivity extends AppCompatActivity { private RecyclerView recyclerView; private NoteAdapter noteAdapter; private List<Note> noteList; private FloatingActionButton addNoteButton; private EditText noteTitleEditText, noteContentEditText; private Spinner categorySpinner, tagSpinner; private Button saveNoteButton, cancelNoteButton; private NoteDatabase noteDatabase; private int noteId = -1; private List<String> categoryList, tagList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = findViewById(R.id.recycler_view); recyclerView.setLayoutManager(new LinearLayoutManager(this)); noteList = new ArrayList<>(); noteAdapter = new NoteAdapter(this, noteList); recyclerView.setAdapter(noteAdapter); addNoteButton = findViewById(R.id.add_note_button); addNoteButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showNoteDialog(); } }); noteDatabase = new NoteDatabase(this); loadNotes(); } private void showNoteDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); LayoutInflater inflater = getLayoutInflater(); View dialogView = inflater.inflate(R.layout.note_dialog, null); dialogBuilder.setView(dialogView); noteTitleEditText = dialogView.findViewById(R.id.note_title_edit_text); noteContentEditText = dialogView.findViewById(R.id.note_content_edit_text); categorySpinner = dialogView.findViewById(R.id.category_spinner); tagSpinner = dialogView.findViewById(R.id.tag_spinner); saveNoteButton = dialogView.findViewById(R.id.save_note_button); cancelNoteButton = dialogView.findViewById(R.id.cancel_note_button); categoryList = loadCategories(); ArrayAdapter<String> categoryAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, categoryList); categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); categorySpinner.setAdapter(categoryAdapter); tagList = loadTags(); ArrayAdapter<String> tagAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, tagList); tagAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); tagSpinner.setAdapter(tagAdapter); final AlertDialog alertDialog = dialogBuilder.create(); alertDialog.show(); saveNoteButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { String title = noteTitleEditText.getText().toString(); String content = noteContentEditText.getText().toString(); String category = categorySpinner.getSelectedItem().toString(); String tag = tagSpinner.getSelectedItem().toString(); if (noteId == -1) { Note note = new Note(title, content, category, tag); noteDatabase.addNote(note); noteList.add(note); } else { Note note = noteList.get(noteId); note.setTitle(title); note.setContent(content); note.setCategory(category); note.setTag(tag); noteDatabase.updateNote(note); noteId = -1; } noteAdapter.notifyDataSetChanged(); alertDialog.dismiss(); } }); cancelNoteButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { alertDialog.dismiss(); } }); } private void loadNotes() { noteList.clear(); noteList.addAll(noteDatabase.getAllNotes()); noteAdapter.notifyDataSetChanged(); } private List<String> loadCategories() { List<String> categories = new ArrayList<>(); categories.add("Personal"); categories.add("Work"); categories.add("Study"); return categories; } private List<String> loadTags() { List<String> tags = new ArrayList<>(); tags.add("Important"); tags.add("Urgent"); tags.add("ToDo"); return tags; } public void editNote(int position) { Note note = noteList.get(position); noteId = position; noteTitleEditText.setText(note.getTitle()); noteContentEditText.setText(note.getContent()); categorySpinner.setSelection(categoryList.indexOf(note.getCategory())); tagSpinner.setSelection(tagList.indexOf(note.getTag())); showNoteDialog(); } public void deleteNote(int position) { Note note = noteList.get(position); noteDatabase.deleteNoteById(note.getId()); noteList.remove(position); noteAdapter.notifyDataSetChanged(); } } ``` 以上代码仅为示例,可能存在一些问题,需要根据具体情况进行修改和完善。希望能对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值