Redux的核心概念,實現程式碼與應用示例
Redux是一種JavaScript的狀態管理容器,是一個獨立的狀態管理庫,可配合其它框架使用,比如React。引入Redux主要為了使JavaScript中資料管理的方便,易追蹤,避免在大型的JavaScript應用中資料狀態的使用混亂情況。Redux 試圖讓 state 的變化變得可預測,為此做了一些行為限制約定,這些限制條件反映在 Redux 的三大原則中。
本文會介紹Redux的幾個基本概念和堅持的三大原則,以及完整的迴路一下Redux中的資料流。在瞭解以上這些概念之後,用自己的程式碼來實現一個簡版的Redux,並且用自己實現的Redux結合React框架,做一個簡單的TodoList應用示例。希望本文對於初識Redux的同學有一個清晰,全面的認識。
Redux的幾個基本概念
一、資料儲存 - state
Redux就是用來管理狀態資料,所以第一個概念就是狀態資料,state就是存放資料的地方,根據應用需要,一般定義成一個物件,比如:
{ todos: [], showType: 'ALL', lastUpdate: '2019-10-30 11:56:11' }
二、行為觸發 - action
web應用,所有的資料狀態變更,都是由一個行為觸發的,比如使用者點選,網路載入完成,或者定時事件。在簡單應用裡面,我們一般都是在行為觸發的時候,直接修改對應的資料狀態,但是在大型複雜的應用裡面,修改同一資料的地方可能很多,每個地方直接修改,會造成資料狀態不可維護。
Redux引入了action的概念,每個要改變資料狀態的行為,都定義成一個action物件,用一個type來標誌是什麼行為,行為附帶的資料,也都直接放在action物件,比如一個使用者輸入的行為:
{ type: 'INPUT_TEXT', text: '今天下午6點活動碰頭會議' }
然後通過dispatch觸發這個action,dispatch(action)
三、行為響應 - reducer
狀態,action的概念瞭解了,當action觸發的時候,肯定要修改state資料,在講解action的時候有說過,不能直接修改state,我們需要定義一個reducer來修改資料,這個reducer就是一個行為響應函式,他接收當前state,和對應的action物件,根據不同的action,做相應的邏輯判斷和資料處理,然後返回一個新的state。
注意,一定是返回一個新的state,不能直接修改引數傳入的原state,這是redux的原則之一,後面會講到。
function reducer ( state = [], action ) { switch ( action.type ) { case 'INPUT_TEXT': return [...state, {text: action.text, id: Math.random() }] default: return state; } }
四、資料監聽 - subscribe
資料的更新已經在reducer中完成了,在一些響應式的web應用中,我們往往需要監聽資料狀態的變化,這個時候就可以用subscribe了
redux內部儲存一個監聽佇列,listeners,可以呼叫subscribe來往listeners裡面增加新的監聽函式,每次reducer修改完state之後,會逐個執行監聽函式,而監聽函式可以獲取已經更新過的state資料了
listeners = []; subscrible( listener ) { listeners.push( listener ); return function () { let index = listeners.index( listener ); listeners.splice( index, 1 ); } } dispatch( action ) // 觸發 action reducer(state, action) listeners.map( ( listener ) => { listener() } )
Redux的幾大原則
一、單一資料原則
整個應用的資料都在state,並且只有這一個state,這麼做的目的是方便管理,整個應用的資料就這一份,除錯方便,開發也方便,可以在開發的時候用本地的資料。而且開發同構應用也很方便,比如服務端渲染,把服務端的資料全部放在state,作為web端初始化時候的資料
二、state只讀
state的資料對外只讀,不能直接修改state,唯一可以修改的方式是觸發action,然後通過reducer來處理。
因為所有的修改都被集中化處理,且嚴格按照一個接一個的順序執行,因此不用擔心競態條件(race condition)的出現。 Action 就是普通物件而已,因此它們可以被日誌列印、序列化、儲存、後期除錯或測試時回放出來。
三、使用純函式
先說明下什麼是純函式,純函式指的是函式內部不修改傳入的引數,無副作用,在傳參一定的情況下,返回的結果也是一定的。Redux中的Reducer需要設計成存函式,不能直接操作傳入的state,需要把改變的資料以一個新的state方式返回。
Redux中的資料流
其實上面講Redux基本概念的時候已經大概的說了下資料流向方式了,就是: view->action->reducer->state->view,用文字來表述就是,首先由於頁面上的某些事件會觸發action,通過dispatch(action)來實現,然後通過reducer處理,reducer(state, action)返回一個新的state,完成state的更新,當然對於響應式的應用,會觸發listener(),在listener裡面獲取最新的state狀態,完成對應檢視(view)的更新。這就是整個redux中的資料流描述,如下圖所示:
Redux的實現程式碼(非官方)
在對Redux的基本概念和幾大原則熟悉了之後,可以實現一個自己的Redux了,當然我們一般都直接用官方的npm包,這裡自己實現的比較簡單,沒有做什麼入參驗證,異常處理之類的,主要是加深下對Redux的理解。下面直接貼程式碼了,對應的概念都有註釋。
// redux.js // 建立state的函式 // 傳入reducer 和初始化的state function createStore( reducer, initState ) { let ref = {}; let listeners = []; let currentState = initState; // dispath函式,用來觸發action function dispatch ( action ) { // 觸發的action,通過reducer處理 currentState = reducer( currentState, action ) // 處理完成後,通知listeners for ( let i in listeners ) { let listener = listener[ i ]; listener(); } return action; } // 返回當前的state function getState () { return currentState; } // 訂閱state變化, 傳入listener,返回取消訂閱的function function subscribe ( listener ) { listeners.push( listener ); return function () { let index = listeners.indexOf( listener ); if ( index > -1 ) { listeners.splice( index, 1 ); } } } ref = { dispatch: dispatch, subscribe: subscribe, getState: getState }; return ref; } function combineReducers( reducers ) { return function ( state, action ) { let finalState = {}; let hasChanged = false; for ( let key in reducers ) { let reducer = reducers[ key ] if ( typeof reducer === 'function' ) { let keyState = reducer( state && state[ key ], action ); hasChanged = hasChanged || keyState !== state[ key ]; finalState[ key ] = keyState; } } return hasChanged ? finalState : state; } } export { createStore, combineReducers }
是不是覺得怎麼才這麼點程式碼,就是這麼點程式碼,而且還包含了一個combineReducers輔助函式,下面再貼一點使用示例程式碼
// reducer函式,用於處理action function reducer( state = [], action ) { switch( action.type ) { case 'INPUT_TEXT': return [ ...state, { text: action.text, key: Math.random(), isDo: false }]; case 'TOGGLE_TODO': return state.map( ( item ) => { if ( item.key === action.id ) { return {...item, isDo: !item.isDo }; } } ); default: return state; } } let store = createStore( reducer ); // 在使用者輸入一條Todo時候 console.log(store.getState()); store.dispatch( { type: 'INPUT_TEXT', text: '這裡是一條待辦事項' } ); console.log(store.getState()); //在使用者點選一條Todo Item的時候,切換完成狀態 console.log(store.getState()); store.dispatch( { type: 'TOGGLE_TODO', id: item.key } ) console.log(store.getState());
Redux與React的結合應用示例
下面,利用Redux結合React開發一個簡單的Todo工具,頁面主要功能點
1、可以新增Todo事項
2、點選事項會切換事項的完成狀態
3、可以切換展示全部/已完成/待完成事項
這個例項是基於react,react-redux完成的,專案搭建用的是create-react-app,利用react-redux提供的介面,將redux中的state和action整合到元件中,需要讀者熟悉create-react-app的使用,以及react-redux的主要介面功能,以下貼出主要程式碼,感興趣的同學可以自己搭建實現
首先定義好state資料結構和action以及對應的reducer
state包含兩部分,一是todos,待辦事項列表,二是showType,展示型別
action包含這麼三種,一是新增新的Todo,二是切換事項完成狀態,三是切換展示型別,分別定義好
actions.js
// actions.js let nextTodoId = 0 export const addTodo = text => { return { type: 'ADD_TODO', id: nextTodoId++, text }; }; export const setShowType = showType => { return { type: "SET_SHOW_TYPE", showType }; }; export const toggleTodo = id => { return { type: 'TOGGLE_TODO', id }; };
reducers.js
const todos = ( state = [], action ) => { switch ( action.type ) { case 'ADD_TODO': return [ ...state, { id: action.id, text: action.text, isDo: false } ]; case 'TOGGLE_TODO': return state.map( todo => { return todo.id === action.id ? {...todo, isDo: !todo.isDo } : todo; } ); default: return state; } } const showType = ( state = 'SHOW_ALL', action ) => { switch ( action.type ) { case 'SET_SHOW_TYPE': return action.showType; default: return state; } } const todoList = combineReducers({ todos, showType }) export { todoList }
至此,資料狀態redux部分算完成了,接下來實現對應的Component和入口檔案了,準備分這麼幾個元件
1、待辦事項Todo
2、輸入框 AddTodo
3、待辦事項列表TodoList
4、底部展示型別切換Tab
// component.js import { connnect } from 'react-redux'; import { addTodo, setShowType, toggleTodo } from './actions' const Todo = ( { onClick, completed, text } ) => ( <li onClick={onClick} style={{ textDecoration: completed ? 'line-through' : 'none' }}> {text} </li> ) const AddTodo = ( { dispatch } ) => { let input; return ( <div> <form onSubmit={ e => { e.preventDefault() if ( !input.value.trim() ) { return; } dispatch( addTodo( input.value ) ) input.value = '' }} > <input ref={ node => {input = node } } /> <button type='submit'>Add Todo</button> </form> </div> ) } AddTodo = connect()( AddTodo ); const TodoList = ( { todos, onTodoClick } ) => { return ( <ul> {todos.map( todo => ( <Todo key={todo.id} {...todo} onClick={ () => onTodoClick( todo.id ) } /> ) )} </ul> ) }; const getShowTodoList = ( todos, showType ) => { switch( showType ) { case 'SHOW_ISDO': return todos.filter( item => item.isDo ); case 'SHOW_ACTIVE': return todos.filter( item => !item.isDo ); case 'SHOW_ALL': default : return todos; } } const mapStateToProps = state => { return { todos: getShowTodoList ( state.todos, state.showType) }; }; const mapDispatchToProps = dispatch => { return { onTodoClick: id => { dispatch( toggleTodo( id ) ); } }; } const ShowTodoList = connect( mapStateToProps, mapDispatchToProps )( TodoList ); const Tab = () => ( <p> Show: { ' ' } <FilterLink filter='SHOW_ALL'>ALL</FilterLink> { ', ' } <FilterLink filter='SHOW_ACTIVE'>ACTIVE</FilterLink> { ', ' } <FilterLink filter='SHOW_ISDO'>ISDO</FilterLink> </p> ) export { AddTodo, ShowTodoList, Tab }
入口檔案 index.js
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { createStore } from './redux'; import todoList from './reducers' import {AddTodo, ShowTodoList, Tab } from './component' let store = createStore( todoApp ); ReactDOM.render( <Provider store={store}> <div> <AddTodo /> <ShowTodoList /> <Tab /> </div> </Provider> , document.getElementById('root'));
主要程式碼完成,npm start 執行,功能截圖如下
文章同步釋出: https://www.geek-share.com/detail/2783420870.html
參考文章:
原生實現一個react-redux的程式碼示例
用React實現一個完整的TodoList的示例