深入淺出redux學習

kenzung發表於2018-10-25

前言:
一開始接觸redux的時候最令我記住的一句話是:You Might Not Need Redux(那我還寫這篇文章幹嘛?手動滑稽)
迴歸正題,本文主要是圍繞redux的作者Dan的視訊,由淺入深瞭解redux

redux基礎用法


1. Action

如果需要改變state(狀態),我們需要使用到Action。Action是一個普通的JavaScript物件描述state變化。action的屬性可以自定義,但是必須有一個叫type的屬性,值是string型別(方便序列化)。每一個action都是對state的一個(minimal change)最小修改,在應用裡什麼東西發生了變化。

  //建立一個加法器,減法器
  const INCREMENT = 'INCREMENT';
  
  {
    type: INCREMENT
  }

  const DECREMENT = 'DECREMENT';
  
  {
    type: DECREMENT
  }
  
  //-----------------------------

  //比如新增新todo任務
  // action type字元常量
  const ADD_TODO = 'ADD_TODO';

  // ADD_TODO action
  {
    type: ADD_TODO,
    text: 'Learn Redux'
  }
複製程式碼

2. 純函式和非純函式

什麼是純函式?

純函式就是沒有副作用的函式,不包含資料庫查詢、網路請求等操作。只要輸入的值不變,每次輸出都是一樣的值。

//pure function
function square(x) {
  return x * x;
}

// Impure function
function square(x) {
  updateXInDatabase(x);
  return x * x;
}
複製程式碼

為什麼需要說純函式?
因為在redux中,有些函式必須是純函式,比如reducer,所以在編碼的時候有必要注意。

3. reducer

**注意!**reducer必須是純函式,不能更改state,每次都必須返回一個新的state。
reducer,其實就是描述舊狀態(previous state)如何轉換為當前狀態(current state)的函式。

這裡使用的是計數器例子,我們通過actiontype

const counter = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}
複製程式碼

reducer是需要固定的形式的,需要傳入2個引數,一個是state,另一個是action。
我們這裡先不要管為什麼要這麼寫,記住這是redux的規定即可。我們可以通過es6的預設引數方式賦予state初始預設值。

4. 編寫reducer注意點

因為reducer是純函式,因此需要注意幾點:

  1. 不能改變引數
  2. 不能使用非純函式

接下來我們一起看一下怎麼處理reducer中對資料的處理。
reducer的state處理通常是兩種資料型別:

  1. Object
  2. Array

Object

// 注意,不能直接改變state中的屬性值
state.name = 'ken' // ❌這種對name屬性賦值操作是不允許的

// ---------------------------
// 正確✔️做法
// 1,使用Object的assign方法,需要對瀏覽器做polyfill
return Object.assign({}, state, { name: 'ken' });

// 或者使用es7的解構方法
return {
  ...state,
  name: 'ken',
};
複製程式碼

Array

array的操作中,常用操作為:

  1. 增(push)
  2. 刪(splice)
  3. 指定位置元素運算操作,如 array[index]++ array[index] = array[index] * 2等操作

注意上述3個操作都是會改變原陣列,因此需要使用“純”操作代替這些“非純”操作。

  1. push/pop/shift/unshift 如果需要使用push,我們可以使用concat方法代替。
    concat方法返回一個新陣列,且不改變原陣列。
array.push(x); // ❌
array.concat(x); // ✅
// 或者使用解構方式
[...array, x];
複製程式碼
  1. splice 同理,splice操作也是會改變原陣列,我們可以用slice去代替。
array.splice(index, 1); // ❌
[...array.slice(0, index),
 ...array.slice(index + 1)]; // ✅
複製程式碼
  1. 指定位置元素運算操作
// 如 
array[index]++ // ❌
// 可用以下方式代替 ✅
[...array.slice(0, index),
array[index] + 1,
...array.slice(index + 1)];
複製程式碼

5. createStore

createStore主要是生成redux中最核心的Store物件。action描述“發生了什麼”,reducer是響應action並對state進行更新。而store可以看成把actionreducer聯絡起來的一個物件。

import { createStore } from redux;

const store = createStore(counter)

console.log(store.getState());
// output 0

store.dispatch({ type: 'INCREMENT'} );
console.log(store.getState());
// output 1

// ------------------------------
const render = () => {
  document.body.innerText = store.getState();
};
store.subscribe(render);

document.addEventListener('click', () => {
  store.dispatch({ type: 'INCREMENT' });
});
複製程式碼

createStore生成的store物件包含3個方法,分別為getStatedispatchsubscribe。其中subscribe函式的返回值是一個函式,用於取消訂閱。

  • getState()返回應用當前的state樹。
  • dispatch(action)分發action。這個是觸發state改變的唯一方法。
    action是描述應用變化的普通物件。按照約定,action 具有 type 欄位來表示它的型別。type 也可被定義為常量或者是從其它模組引入。
  • subscribe(listener)新增一個變化監聽器。listener是一個函式,每當執行dispatch方法時候就會執行listener。 在例子中,每次dispatch一個action,就會觸發render函式執行。如果需要解除繫結監聽,執行subscribe返回的函式即可。

6. 實現一個簡單版的createStore

我們知道createStore返回的是一個store物件,其中包括getStatedispatchsubscribe方法。
當然接下來的實現是最簡單的實現,去掉了很多引數校驗、store enhancer(即中介軟體)和類RxJS等reactive庫支援。有興趣更深瞭解的同學可以看看redux的createStore原始碼(ps:我打算下一篇文章寫關於redux原始碼☺️)

const CreateStore = (reducers, initState = {}) {
  let currentState = initState;
  let listeners = [];

  // getState就是簡單的把當前的state返回給呼叫者
  const getState = () => currrentState;

  // subscribe:
  // 如果對觀察者模式(釋出-訂閱模式)比較熟悉的同學就會發現,這其實就是做訂閱
  // 
  const subscribe = listener => {
    listeners.push(listener);
    return function unsubscribe() {
      listeners = listeners.filter(currentListener => currentListener !== listener)
    };
  };

  // 同理,這個是觀察者模式中的釋出
  // 接收到action後,通過reducer更新state,並且把listeners執行一遍
  const dispatch = action => {
    reducers(currentState, action);
    listens.forEach(listener => listener());
  }

  // 初始化
  // 讓reducers返回他們的初始state
  dispatch({});

  return {
    getState,
    subscribe,
    dispatch,
  };
};
複製程式碼

我們很簡單就實現了createStore,當然實際上還是會稍微複雜一點。

7. combineReducers

從上述我們實現的createStore原始碼可以看出,傳入的reducer只有一個
真實的應用中reducer是多個的,因此我們需要把reducer組合起來。combineReducers就是幫我們完成這個任務。

import { combineReducers } from 'redux';

const todosReducer = (state = {}, action) => {
  switch (action.type) {
    ...
  }
};

const visibilityFilterReducer = (state = {}, action) => {
  switch (action.type) {
    ...
  }
};

const reducer = combineReducers({
  todos: todosReducer,
  visibilityFilter: visibilityFilterReducer,
});

const store = createStore(reducer, {});
複製程式碼

程式碼主要完成了把todosReducer和visibilityFilterReducer合併為一個reducer;todos和visibilityFilter分別是兩個變數接收兩個reducer處理的結果。

其實我們可以這麼理解

const todosAndVisibilityFilterReducer = (state = {
  todos: {},
  visibilityFilter: {}
}, action) => {
  switch (action.type) {
    ...
    // 合併todosReducer和visibilityFilterReducer的case即可
  }
}

const store = createStore(todosAndVisibilityFilterReducer, {});
複製程式碼

當然真實專案不能這麼搞,既然redux提供了combineReducers,我們就應該使用。

8. 實現一個簡單版的combineReducers

和createStore一樣,我們寫一個簡單版本的combineReducers

//可以理解combineReducers就是一個reducer,因此其返回值是一個可以傳入(state, action)的函式
const combineReducers = (reducers = {}) => {
  return (state = {}, action) => {
    // 和普通的reducer一樣,返回一個新的state作為返回值
    // 此處選擇Array.reducer方法更加簡潔;forEach還要建立一個臨時變數儲存新值。
    return Object.keys(reducers).reduce((nextState, key) => {
      // 這裡的key可以套入todos/visibilityFilter
      // 執行每個reducer方法,並取得其返回的state值,放入nextState中
      nextState[key] = reducers[key](state[key], action);
      return nextState;
    });
  }
};
複製程式碼

參考

  1. redux官網
  2. Dan的Getting Started With Redux系列課程(非常贊,5星級推薦)

相關文章