前言:
一開始接觸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)的函式。
這裡使用的是計數器例子,我們通過action
的type
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是純函式,因此需要注意幾點:
- 不能改變引數
- 不能使用非純函式
接下來我們一起看一下怎麼處理reducer中對資料的處理。
reducer的state處理通常是兩種資料型別:
- Object
- Array
Object
// 注意,不能直接改變state中的屬性值
state.name = 'ken' // ❌這種對name屬性賦值操作是不允許的
// ---------------------------
// 正確✔️做法
// 1,使用Object的assign方法,需要對瀏覽器做polyfill
return Object.assign({}, state, { name: 'ken' });
// 或者使用es7的解構方法
return {
...state,
name: 'ken',
};
複製程式碼
Array
array的操作中,常用操作為:
- 增(push)
- 刪(splice)
- 指定位置元素運算操作,如
array[index]++
array[index] = array[index] * 2
等操作
注意上述3個操作都是會改變原陣列,因此需要使用“純”操作代替這些“非純”操作。
- push/pop/shift/unshift
如果需要使用push,我們可以使用concat方法代替。
concat方法返回一個新陣列,且不改變原陣列。
array.push(x); // ❌
array.concat(x); // ✅
// 或者使用解構方式
[...array, x];
複製程式碼
- splice 同理,splice操作也是會改變原陣列,我們可以用slice去代替。
array.splice(index, 1); // ❌
[...array.slice(0, index),
...array.slice(index + 1)]; // ✅
複製程式碼
- 指定位置元素運算操作
// 如
array[index]++ // ❌
// 可用以下方式代替 ✅
[...array.slice(0, index),
array[index] + 1,
...array.slice(index + 1)];
複製程式碼
5. createStore
createStore主要是生成redux中最核心的Store物件。action
描述“發生了什麼”,reducer
是響應action
並對state進行更新。而store
可以看成把action
和reducer
聯絡起來的一個物件。
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個方法,分別為getState
,dispatch
和subscribe
。其中subscribe
函式的返回值是一個函式,用於取消訂閱。
getState()
返回應用當前的state樹。dispatch(action)
分發action。這個是觸發state
改變的唯一方法。
action是描述應用變化的普通物件。按照約定,action 具有 type 欄位來表示它的型別。type 也可被定義為常量或者是從其它模組引入。subscribe(listener)
新增一個變化監聽器。listener是一個函式,每當執行dispatch方法時候就會執行listener。 在例子中,每次dispatch一個action,就會觸發render函式執行。如果需要解除繫結監聽,執行subscribe
返回的函式即可。
6. 實現一個簡單版的createStore
我們知道createStore返回的是一個store物件,其中包括getState
,dispatch
和subscribe
方法。
當然接下來的實現是最簡單的實現,去掉了很多引數校驗、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;
});
}
};
複製程式碼