React-Redux與Vuex使用對比

一. 概述

ReactVue是我們熟悉的兩大前端主流框架,來自官方的解釋,Vue是一套用于構建用戶界面的漸進式框架,React是一個用于構建用戶界面的JavaScript庫,兩個框架都使用各自的語法,專注于用戶UI界面的構建.那我們會有疑問,這兩個框架都專注于UI界面的構建,但是隨著JavaScript單頁應用開發日趨復雜,我們如何進行更多數據的管理呢?比如網絡請求的數據、緩存數據、本地生成尚未持久化到服務器的數據,UI狀態數據,激活的路由,被選中的標簽等等. 基于上面的疑問,兩個框架都有各自的解決方案:React-ReduxVuex.

二.使用

1.Redux

使用react-redux之前我們先來了解一下ReduxRedux是 JavaScript 狀態容器,提供可預測化的狀態管理,ReduxFlux演變而來,當然除了和React一起用外,還支持其它界面庫,不過我們這里主要介紹它配合React進行使用.先來了解下它的幾個核心概念:

(1) 核心概念

  • State: 所謂的state就是React組件所依賴的狀態對象。你可以在里面定義任何組件所依賴的狀態。比如一個簡單的todo應用的state可能是這樣
{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}
  • Action: action就是一個普通JavaScript對象,用來描述發生了什么.比如
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

你可以把action理解為一個描述發生了什么的指示器。在實際應用中,我們會dispatch(action),通過派發action來達到修改state的目的。這樣做的好處是可以清晰地知道應用中到底發生了什么。

  • Reducer:reducer的作用是用來初始化整個Store,并且串起stateaction, 它是一個接收stateaction并返回新state的函數.我們可以通過區分不同的action類型,來處理并返回不同的state.
const StoreAction = (state = defaultState,action) => {
    switch (action.type) {
        case HOME_ACTION_UPDATE_STATE:
            return {...state,...action.data};
        case ADD_ARTICLELIST_STATE:
            let newState = JSON.parse(JSON.stringify(state));/// 深拷貝
            newState.articleList = newState.articleList.concat(action.data);
            return newState;
        default:
            return state;
    }
}

(2) 使用原則

使用Redux進行數據管理時有三個原則需要注意

  • 單一數據源
    整個應用的state被儲存在一棵object tree中,并且這個object tree只存在于唯一一個store中。

  • State 是只讀的
    唯一改變state的方法就是觸發actionaction是一個用于描述已發生事件的普通對象。

  • 使用純函數來執行修改
    我們通過reducer接收先前的stateaction,并返回新的state, Reducer必須是一個純函數,所謂的純函數就是一個函數的返回結果只依賴于它的參數,并且在執行過程中沒有副作用。

(3)React Redux

react-reduxRedux官方提供的React綁定庫.他的使用也遵循上面的redux原則。

  • 安裝
npm install --save react-redux
  • 流程


    image.png

通過上面的流程圖可以很清晰的明確Redux的使用:

React組件首先調用ActionCreators里事先定義好的方法,得到一個actoion,通過dispatch(action)達到派發actionReducer的目的。Reducer通過接受的不同的action來對state數據進行處理,處理完成后,返回一個新的state,state變化后React組件進行重新渲染。

  • 使用

    • 入口文件index.js

      import React from 'react'
      import { render } from 'react-dom'
      import { Provider } from 'react-redux'
      import { createStore } from 'redux'
      import todoApp from './reducers'
      import App from './components/App'
      
      let store = createStore(todoApp)
      
      render(
        <Provider store={store}>
          <App />
        </Provider>,
        document.getElementById('root')
      )
      
  • 創建store/reducer.js

    import { ADD_TODO_LIST_VALUE } from "./actionTypes";
    
    /// 初始化數據
    const defaultState = {
        todos: [{
            text: 'Eat food',
            completed: true
          }, {
            text: 'Exercise',
            completed: false
          }],
          visibilityFilter: true,
    }
    /// Reducer 可以接受state,但是不能修改State !
    export default (state = defaultState , action) => {
      switch (action.type) {
          case ADD_TODO_LIST_VALUE:
            const newState = JSON.parse(JSON.stringify(state));///將原來的state 做一次深拷貝
            newState.todos.push(action.value);
            return newState;
          default:
            return state;
      }
    }
    
  • 根據reducer創建store/index.js,

    import { createStore,compose,applyMiddleware } from 'redux';
    import reducer from './reducer';
    
    const store = createStore(reducer);
    export default store;
    
  • 創建actionCreatorsstore/actionCreators.js

    import { ADD_TODO_LIST_VALUE } from "./actionTypes"
    
    export const addItemListAction = (value) => ({
          type:ADD_TODO_LIST_VALUE,
          value
    })
    
  • 創建actionTypes專門用來存儲action的type值

    export const ADD_TODO_LIST_VALUE = 'add_todo_list_value';
    
  • React組件中使用

    import React, { Component } from 'react';
    import { addItemListAction } from '../pages/home/store/actionCreators';
    import {connect} from 'react-redux';
    
    class Customtodolist extends Component {
        render() {
            return (
                <div className='todo-list' onClick={()=>{this.props.addListItem()}}>
                  <ul>
                      { 
                        this.props.todos.map((item,index) => 
                          <li key={index}>{index}:{item}</li>
                        )
                      }
                    </ul>
                </div>
            );
        }
    }
    const mapStateToProps = (state) => {
        return {
            todos: state.todos
        }
    }
    
    const mapDispatchToProps = (dispatch) => {
        return {
            addListItem: () => {
                const item = {text: 'Eat food',completed: true}
                const actionCreator = addItemListAction(item);
                dispatch(actionCreator);
            }
        }
    }
    
    export default connect(mapStateToProps, mapDispatchToProps)(Customtodolist);
    

    我們通過react-reduxconnect方法,將mapStateToProps與mapDispatchToProps 方法與組件鏈接,然后直接在類組件中通過this.props.XXX的方式進行訪問Store中的state.

  • React hooks中使用

    /// hooks 中使用react-redux
    import { useSelector, useDispatch } from 'react-redux';
    const Home = (props)=> {
        // hook 獲取store state 方式
        const storeCount = useSelector(state => state.home.count);
        // 獲取actionCreator方法
        const dispatch = useDispatch();
      
        return (
          <div className={style['home-content']}>
              <div className='home-content-detail'>
                  StoreCount數據展示{storeCount}
                  <div>
                    {/* addStoreCount是在actionCreators中定義的方法 */}
                    <button onClick={()=> {dispatch(addStoreCount(0))}}>點擊storeCount+1</button>
                  </div>
              </div>
          </div>
      )
    }
    
  • redux-thunk的使用

    redux-thunkredux中間件.他的主要作用是可以使用異步派發action。例如我們要進行網絡請求,那么可以在actionCreators里面異步派發action.

    安裝與使用

    npm install --save redux-thunk
    

    (1).在store/index.js 中添加引入

    import thunk from "redux-thunk";/// redux-thunk 中間件 需要引入的
    

    (2). 使用thunk初始化store

    /// 下面的代碼是固定寫法 /// redux-thunk 中間件 需要引入的
    const composeEnhancers =
      typeof window === 'object' &&
      window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?   
        window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
        }) : compose;
    
    /// 中間件都需要用這個方法
    const enhancer = composeEnhancers(
        applyMiddleware(thunk),/// redux-thunk 中間件 需要引入的
    );
    /// 創建
    const store = createStore(
        reducer,
        enhancer//使用Redux-thunk 中間件
    );
    

    (3).在actionCreater.js 中添加異步派發函數,注意:在獲取到異步處理的結果后,我們仍然需要調用actionCreater.js中的其他創建action方法,來對其進行dispatch.

    export const initListAction = (data) => ({
        type:INIT_LIST_ACTION,
        data
    })
    
    /// 將異步請求 放在 Action 中執行
    export const getToList = () => {
       /// 這里返回一個函數,能獲取到 dispatch 方法 這就是 redux-thunk 的作用,可以返回一個函數
        return (dispatch) => {
            axios.get('/api/todolist').then((res) => {
                // alert(res.data);
                const data = res.data;
                const action = initListAction(data);
                dispatch(action);
            }).catch((error)=>{
                console.log('網絡請求錯誤了---thunk----》');
            })
        }
    }
    
  • reducer的拆分與合并

    隨著項目功能模塊越來越多,如果只有一個reducer來維護state,會使其變動越來越大,從而導致難以維護。combineReducer應運而生, 它將根reducer分拆成多個 reducer,拆分之后的 reducer 都是相同的結構(state, action),并且每個函數獨立負責管理該特定切片 state 的更新。多個拆分之后的reducer可以響應一個 action,在需要的情況下獨立的更新他們自己的切片 state,最后組合成新的 state。

    使用

    import { combineReducers } from 'redux';/// 將小的Reducer 合并成大的reducers
    /// 需要拆分 
    import headerReducer from '../common/header/store/reducer'
    import mainReducer from './mainReducer';
    import {reducer as homeReducer} from '../pages/home/store';
    import {reducer as loginReducer} from '../pages/login/store';
    
    /// 進行 reducer的合并
    const reducer = combineReducers({
        header:headerReducer,
        main:mainReducer,
        login:loginReducer,
        home:homeReducer,
    })
    
    export default reducer;
    

    react組件中使用,要加上reducer名稱,例如我們在Home組件中這樣獲取其state

    const mapStateToProps = (state, ownProps) => {
        return {
            showScroll: state.home.showScroll,//state后面添加reducer名稱
        }
    }
    

2.Vuex

Vuex是一個專為 Vue.js 應用程序開發的狀態管理模式 + 庫。它采用集中式存儲管理應用的所有組件的狀態,并以相應的規則保證狀態以一種可預測的方式發生變化。它主要用來解決多個組件共享狀態的問題。

image.png

VuexRedux的數據管理模式很相似,如果理解Redux,那么Vuex也很容易理解了,只不過Vuex 是專門為 Vue.js 設計的狀態管理庫,使用起來要更加方便。

(1) 核心概念

  • State: 就是組件所依賴的狀態對象。我們可以在里面定義我們組件所依賴的數據。可以在Vue組件中通過this.$store.state.XXX獲取state里面的數據.

  • Getter:從 store 中的 state 中派生出一些狀態,可以把他理解為是store的計算屬性.

    const store = createStore({
      state: {
        todos: [
          { id: 1, text: '...', done: true },
          { id: 2, text: '...', done: false }
        ]
      },
      getters: {
        doneTodos: (state) => {
          return state.todos.filter(todo => todo.done)
        }
      }
    })
    

    例如我們定義了上面的store,在Vue組件中通過store.getters.doneTodos訪問它的getter.

  • Mutation:更改 Vuex 的 store 中狀態的唯一方法是提交 mutation,我們通過在mutation中定義方法來改變state里面的數據。

    const store = createStore({
      state: {
        count: 1
      },
      mutations: {
        increment (state) {
          // 變更狀態
          state.count++
        }
      }
    })
    

    在Vue組件中,我們通過store.commit('increment'),來提交。需要注意的是,Mutation 必須是同步函數。在實際使用中我們一般使用常量替代 Mutation 事件類型。例如:

    export const INCREMENT_MUTATION = 'INCREMENT_MUTATION'
    
    const store = createStore({
      state: {
        count: 1
      },
      mutations: {
        // 我們可以使用 ES2015 風格的計算屬性命名功能來使用一個常量作為函數名
        [INCREMENT_MUTATION] (state) {
          // 變更狀態
          state.count++
        }
      }
    })
    
  • Action: Action 類似于 Mutation,不同在于:

    • Action 提交的是 mutation,而不是直接變更狀態。
    • Action 可以包含任意異步操作。
    const store = createStore({
      state: {
        count: 0
      },
      mutations: {
        increment (state) {
          state.count++
        }
      },
      actions: {
        incrementAsync ({ commit }) {
        setTimeout(() => {
          commit('increment')
        }, 1000)
       }
      }
    })
    

    在組件中我們通過store.dispatch('incrementAsync')觸發action。

  • Module: 當我們的應用較大時,為了避免所有狀態會集中到一個比較大的對象中,Vuex 允許我們將 store 分割成模塊(module),你可以把它理解為Redux中的combineReducer的作用.

    const moduleA = {
    state: () => ({ ... }),
    mutations: { ... },
    actions: { ... },
    getters: { ... }
    }
    const moduleB = {
      state: () => ({ ... }),
      mutations: { ... },
      actions: { ... }
    }
    
    const store = createStore({
      modules: {
        a: moduleA,
        b: moduleB
      }
    })
    

在Vue組件中我們使用store.state.a, store.state.b來分別獲取兩個模塊的狀態.

(2) 使用

  • 安裝

    npm install vuex@next --save
    OR 
    yarn add vuex@next --save
    
  • main.js中掛載store

    import { createApp } from 'vue';
    import App from './App.vue';
    import router from './router';
    import store from './store';
    
    createApp(App).use(store).use(router).mount('#app');
    
  • 創建store/index.js

    import { createStore } from 'vuex';
    import { ADD_ITEM_LIST, REDUCE_ITEM_LIST, CHANGE_ITEM_LIST_ASYNC } from './constants';
    
    export default createStore({
      state: {
        itemList: [
          { text: 'Learn JavaScript', done: true },
          { text: 'Learn Vue', done: false },
          { text: 'Build something awesome', done: false },
        ],
      },
      getters: {
        doneItemList: (state) => state.itemList.filter((todo) => todo.done),
      },
      mutations: {
        // 使用ES2015風格的計算屬性命名功能 來使用一個常量作為函數名
        [ADD_ITEM_LIST](state, item) {
          console.log('增加數據', item);
          state.itemList.push(item);
        },
        [REDUCE_ITEM_LIST](state) {
          console.log('減少數據');
          state.itemList.pop();
        },
      },
      actions: {
        [CHANGE_ITEM_LIST_ASYNC]({ commit, state }, todoItem) {
          /// 模擬網絡請求
          setTimeout(() => {
            commit(ADD_ITEM_LIST, todoItem);
            console.log('state===', state);
          }, 1000);
        },
      },
      modules: {
      },
    });
    

注意我們這里仍然使用常量來作actionsmutations的方法名,使用時候要加上[].

  • 在選項式API中使用

    <template>
      <div class="about">
        <div>{{counter}}</div>
        <button @click="handleClick">按鈕</button>
        <div>     
         <div>
           <TodoItem :itemList="$store.state.itemList"/>
         </div>
          <div>
            完成的Todo
           <TodoItem :itemList="$store.getters.doneItemList"/>
         </div>
         <div class="btn-content">
           <button @click="addClick">增加Item</button>
           <button @click="reduceClick">減少Item</button>
           <button @click="changeClickAsync">調用Action</button>
         </div>
        </div>
      </div>
    </template>
    
    <script>
    import TodoItem from '../components/TodoItem.vue';
    import {
      ADD_ITEM_LIST, REDUCE_ITEM_LIST, CHANGE_ITEM_LIST_ASYNC,
    } from '../store/constants';
    
    export default {
      name: 'About',
      components: {
        TodoItem,
      },
      data() {
        return {
          counter: 0,
        };
      },
      computed: {
        todos() {
          return this.$store.getters.doneItemList;
        },
      },
      methods: {
        handleClick() {
          console.log('handleClick--->');
          this.counter += 1;
        },
        addClick() {
          const item = { text: 'add_item_list success!', done: true };
          /// 提交mutations
          this.$store.commit(ADD_ITEM_LIST, item);
        },
        reduceClick() {
          this.$store.commit(REDUCE_ITEM_LIST);
        },
        changeClickAsync() 
          const item = { text: 'async_add_item_list success!', done: true };
          ///派發actions
          this.$store.dispatch(CHANGE_ITEM_LIST_ASYNC, item);
        },
      },
    };
    </script>
    
  • 在組合式API中使用

    在組合式API中通過調用 useStore 函數,來在 setup 鉤子函數中訪問 store。這與在組件中使用選項式 API 訪問 this.$store 是等效的.

    import { useStore } from 'vuex'
    import { computed } from 'vue'
    
    export default {
      setup () {
        const store = useStore()
        return {
          // 在 computed 函數中訪問 state
          count: computed(() => store.state.count),
    
          // 在 computed 函數中訪問 getter
          double: computed(() => store.getters.double)
          
           // 使用 mutation
          increment: () => store.commit('increment'),
    
          // 使用 action
          asyncIncrement: () => store.dispatch('asyncIncrement')
        }
      }
    }
    

三. 總結

通過對比React ReduxVuex可以發現,兩者的封裝與使用有很大的相似性,它們都借鑒了 Flux的設計思想,通過使用對比可以讓我們更容易掌握他們。

一些參考:

Vuex

Redux中文文檔

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容