Redux學習筆記(1)簡單使用

GitHub:https://github.com/BadWaka/redux-demo

參考文章:Redux中文文檔

1. Redux是什么?

是一種狀態(tài)管理容器,用來管理狀態(tài)。

Redux 是 JavaScript 狀態(tài)容器,提供可預(yù)測化的狀態(tài)管理。

可以讓你構(gòu)建一致化的應(yīng)用,運行于不同的環(huán)境(客戶端、服務(wù)器、原生應(yīng)用),并且易于測試。不僅于此,它還提供 超爽的開發(fā)體驗,比如有一個時間旅行調(diào)試器可以編輯后實時預(yù)覽

Redux 除了和 React 一起用外,還支持其它界面庫。它體小精悍(只有2kB,包括依賴)。

不明白?繼續(xù)看下面

2. 為什么用Redux?

React中,數(shù)據(jù)的流向是單向的

props只能從父組件向下傳遞到子組件
而state只能在組件內(nèi)部使用

拋出一個問題,如果子組件想把數(shù)據(jù)傳遞給父組件,咋辦?

這簡單啊,可以讓父組件通過props給子組件傳遞一個函數(shù);子組件想傳數(shù)據(jù)的時候,只要調(diào)用父組件傳進來的函數(shù),把數(shù)據(jù)當參數(shù)傳進去并調(diào)用該函數(shù),這樣父組件就能收到了

那如果要傳遞給父組件的父組件的父組件呢?把函數(shù)一層一層傳進來么?

0.0
可以是可以,就是太麻煩了。。。

那如果一個組件不想和父組件通訊,想和其他的組件通訊,咋辦?

0.0
。。。。。。。

所以這就是問題所在了,完整版請看下方鏈接

引用知乎上的一段答案

理解React,但不理解 Redux,該如何通俗易懂的理解 Redux? - 編程 -...

作者:Wang Namelos
鏈接:https://www.zhihu.com/question/41312576/answer/90782136
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。

解答這個問題并不困難:唯一的要求是你熟悉React。不要光聽別人描述名詞,理解起來是很困難的。從需求出發(fā),看看使用React需要什么:

  1. React有props和state: props意味著父級分發(fā)下來的屬性,state意味著組件內(nèi)部可以自行管理的狀態(tài),并且整個React沒有數(shù)據(jù)向上回溯的能力,也就是說數(shù)據(jù)只能單向向下分發(fā),或者自行內(nèi)部消化。理解這個是理解React和Redux的前提。
  2. 一般構(gòu)建的React組件內(nèi)部可能是一個完整的應(yīng)用,它自己工作良好,你可以通過屬性作為API控制它。但是更多的時候發(fā)現(xiàn)React根本無法讓兩個組件互相交流,使用對方的數(shù)據(jù)。然后這時候不通過DOM溝通(也就是React體制內(nèi))解決的唯一辦法就是提升state,將state放到共有的父組件中來管理,再作為props分發(fā)回子組件。
  3. 子組件改變父組件state的辦法只能是通過onClick觸發(fā)父組件聲明好的回調(diào),也就是父組件提前聲明好函數(shù)或方法作為契約描述自己的state將如何變化,再將它同樣作為屬性交給子組件使用。這樣就出現(xiàn)了一個模式:數(shù)據(jù)總是單向從頂層向下分發(fā)的,但是只有子組件回調(diào)在概念上可以回到state頂層影響數(shù)據(jù)。這樣state一定程度上是響應(yīng)式的。
  4. 為了面臨所有可能的擴展問題,最容易想到的辦法就是把所有state集中放到所有組件頂層,然后分發(fā)給所有組件。
  5. 為了有更好的state管理,就需要一個庫來作為更專業(yè)的頂層state分發(fā)給所有React應(yīng)用,這就是Redux。

3. Redux怎么用?

用之前首先要明白Redux的思想,使用方法倒是次要的

重要的是這種把所有數(shù)據(jù)的狀態(tài),都拿出來放到最頂層的思想
做個不恰到的比喻就相當于計劃經(jīng)濟。。。

簡單描述一下,具體還是要看官方文檔:

  • 首先把一個應(yīng)用里所有的數(shù)據(jù)都拿出來,放到最頂層的一個叫store的容器里,這個里面的state變量存儲了整個App的數(shù)據(jù)狀態(tài)

  • Redux規(guī)定了一些規(guī)則,store里的數(shù)據(jù)不能隨便改,要改的話要發(fā)送一個action,這個action包含了一些操作信息和數(shù)據(jù)信息,比如有一個添加用戶的action,它的操作信息就是添加用戶,附帶了一些用戶的基本信息

  • 發(fā)送了信息總得有人收到吧 這個接受的人就叫reducer,它會收到action,然后做處理,返回新的數(shù)據(jù)

  • React拿到了新的數(shù)據(jù)state就會變化,React檢測到了state變化就自動更新UI,這就是基本流程了

其實Redux單另拿出來用還是挺簡單的,使用教程戳這里https://github.com/react-guide/redux-tutorial-cn#redux-tutorial

主要是和React一起用,需要使用到react-redux這個模塊
這個模塊封裝了幾個方法,在React項目里要按他這個套路來
,然而這個套路要寫的東西有點繁雜,就被繞進去了

官網(wǎng)的Demo就大概講了這么一個套路,然而他又直接使用了一種高級的設(shè)計思想 容器組件和展示組件相分離 ,分成很多個組件,所以看的云里霧里的

下面我們就不用他官方的Demo,自己按正常的邏輯順序把Redux接入React項目

可以下載我的Demo
GitHub:https://github.com/BadWaka/redux-demo

npm install 
npm start

4. 代碼簡述

我要做一個習慣應(yīng)用,用戶可以添加習慣,點擊習慣可以改變習慣的完成狀態(tài),就是這么簡單的一個功能

我建立了一個文件夾redux用來存放狀態(tài)管理的相關(guān)文件,redux/customcomponent/custom相對應(yīng),代表是custom模塊的組件部分和狀態(tài)部分,方便管理

action

redux/custom/下建立一個actions.js文件,用來存放custom習慣模塊的所有action

/**
 * Action的集合
 * Created by BadWaka on 2017/3/15.
 */

/**
 * 修改習慣項完成狀態(tài)Action
 * @param customItemId  習慣項的id
 * @return {{type: string, id: *}}
 */
export const changeCustomItemCompletionStatusAction = (customItemId) => {
    return {
        type: 'CHANGE_CUSTOM_ITEM_COMPLETION_STATUS_ACTION',
        id: customItemId
    };
};

/**
 * 添加習慣項Action
 * @param customItemText    要添加的習慣項的文本
 * @return {{type: string, text: *}}
 */
let nextId = 0;
export const addCustomItemAction = (customItemText) => {
    return {
        type: 'ADD_CUSTOM_ITEM_ACTION',
        text: customItemText,
        id: nextId++
    };
};

該文件暴露出兩個函數(shù),這兩個函數(shù)用來生成對應(yīng)的action
這里的id是自己維護的一個id,講道理這都應(yīng)該去服務(wù)器獲得的,但是那有涉及到異步了。。更復雜,先了解Redux同步使用再說

reducer

可以看到有3個reducer


indexReducer是總的reducer,因為一個store只能由一個reducer創(chuàng)建,而React中,我們只需要一個最上層的store就夠了;所以這時候就需要reducer的合并,indexReducer就是干這個事情的

  • indexReducer.js:
/**
 * 最終的Reducer
 * Created by BadWaka on 2017/3/15.
 */
import {combineReducers} from 'redux';  // 需要調(diào)用redux中的combineReducers這個合并reducer的方法
import customListReducer from './custom/customListReducer'; // 引入習慣列表Reducer

// 合并Reducer
const appReducer = combineReducers({
    customList: customListReducer   // 這里把state中的customList字段與customListReducer對應(yīng)起來,相當于customListReducer就只在乎customList這個字段就行了
});

// 導出一個最終的appReducer
export default appReducer;
  • customListReducer.js
// 引入customItemReducer
import customItemReducer from './customItemReducer';

/**
 * 習慣列表的Reducer
 * @param state 這里收到的state是一個習慣項的數(shù)組
 * @param action
 * @return {*}
 */
const customListReducer = (state = [], action) => { // 這里使用ES6的語法,如果state是undefined的話,給state設(shè)置一個默認值;為什么要這樣?因為reducer和store創(chuàng)建的時候會默認的發(fā)送初始化的action和用于檢測的action;如果state是undefined,而reducer里又沒有處理,直接返回了,就會在控制臺里報錯
    console.log('customListReducer state = ', state, ' action = ', action);
    switch (action.type) {
        /**
         * 添加customItem
         */
        case 'ADD_CUSTOM_ITEM_ACTION':
            // 返回一個新對象
            let newCustomItem = {
                id: action.id,  // id
                text: action.text,  // 文本內(nèi)容
                completed: false    // 是否完成
            };
            // 復制一份state的副本,永遠不要修改state,而是返回state的一份副本
            let newState = [...state];
            // console.log('newState', newState);
            newState.push(newCustomItem);   // 把新的習慣項添加到副本中
            return newState;    // 返回
        /**
         * 修改習慣項完成狀態(tài)Action
         */
        case 'CHANGE_CUSTOM_ITEM_COMPLETION_STATUS_ACTION':
            // 遍歷state(這里得到的state是customList,是一個數(shù)組),返回一個新數(shù)組
            return state.map((customItem) => {
                // 對每一項再調(diào)用customItemReducer
                return customItemReducer(customItem, action);
            });
        default:
            return state;
    }
};

export default customListReducer;
  • customItemReducer.js
/**
 * 習慣項Reducer
 * Created by BadWaka on 2017/3/15.
 * @param state 這里接收到的state只是單個的一個習慣項
 * @param action
 * @return {*}
 */
const customItemReducer = (state, action) => {
    console.log('customItemReducer state = ', state, ' action = ', action);
    switch (action.type) {
        // 修改習慣項完成狀態(tài)Action
        case 'CHANGE_CUSTOM_ITEM_COMPLETION_STATUS_ACTION':
            // 因為上一層customReducer會傳所有的customItem進來,所以要判斷action里的id和傳入的state的id是不是相同
            if (action.id !== state.id) {
                return state;   // 如果不相同,直接返回原state
            }
            
            // 注意,Redux官方文檔中嚴格指出不要直接修改state,所以下面這一句是錯誤的
            // state.completed = !state.completed; // 將completed置反

            // 需要使用Object.assign()新建副本,或者使用對象展開運算符
            // 這里使用對象展開運算符,大概的意思就是將一個對象的可枚舉屬性拷貝至另一個對象,本例中是state中的可枚舉屬性給一個新對象,這個新對象就是state的副本;之后修改這個新對象的completed屬性,然后將這個新對象作為返回值;
            return {
                ...state,
                completed: !state.completed
            };
        // 別忘了返回一個默認的state
        default:
            return state;
    }
};

export default customItemReducer;
為什么有customListReducer又有customItemReducer

因為設(shè)計的時候想著是充分解耦,能在內(nèi)部處理的,就不要麻煩外部的父組件;

比如點擊習慣項的時候切換習慣完成狀態(tài),它不需要父組件,所以就讓customItemReducer來處理;

而添加習慣項的時候,就需要父組件來處理,讓customListReducer來處理;

然而,理想是美好的,現(xiàn)實是殘酷的

indexReducer.js中合并reducer的時候,需要為每個reducer指定對應(yīng)的數(shù)據(jù),like this:

這時候就犯愁了。。整個數(shù)組可以這樣綁定,單個數(shù)據(jù)咋辦?

于是只能曲線救國,通過customListReducer繼續(xù)向下分發(fā),于是就有了在customListReducer中繼續(xù)調(diào)用customItemReducer進行處理

所以這就是為啥有customListReducer又有customItemReducer的原因

好了,既然actionreducer都有了,那么該有store

store

index.js是React的入口文件,在這個文件里,初始化全局的store

  • index.js
// 引入React
import React from 'react';
import ReactDOM from 'react-dom';

// 引入全局css
import './stylesheets/app.css';

// 引入組件
import App from './components/App';

// 引入Redux
import {createStore} from 'redux';  // 需要使用redux的createStore方法創(chuàng)建store
import {Provider} from 'react-redux';   // 需要使用react-redux封裝好的控件Provider包裹App組件
import indexReducer from './redux/indexReducer';    // 導入合并好的全局reducer

// 創(chuàng)建一個全局store用來保存全局狀態(tài)
const store = createStore(indexReducer);
// 循環(huán)顯示總的state,這個主要是為了方便在控制臺看store中的state
setInterval(() => {
    console.log('total state', store.getState());
}, 3000);

ReactDOM.render(
    // 使用Provider組件包裹App組件,把store作為props傳入
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
);

這時候,運行程序npm start,就應(yīng)該可以在控制臺看到日志了

INIT是redux初始化自動發(fā)的action,PROBE_UNKNOWN_ACTION是測試的action,不用管它們

好了,下面就來觸發(fā)一個action,看一下狀態(tài)變化吧

觸發(fā)action,觀察現(xiàn)象

  • components/custom/Custom.js
/**
 * 習慣頁
 * Created by BadWaka on 2017/3/11.
 */
import React, {Component} from 'react';
// 引入組件
import AppBar from '../common/app-bar/AppBar';
import CustomItem from './custom-item/CustomItem';
// 引入用到的action
import {addCustomItemAction} from '../../redux/custom/actions';
// 引入connect函數(shù)用來生成Redux組件
import {connect} from 'react-redux';

class CustomList extends Component {

    constructor(props) {
        super(props);
        // 這個state是本地state,它存在的意義是為了獲得用戶的輸入文本
        this.state = {
            text: ''
        };
    }

    render() {
        return (
            <section className="custom">
                {/*引入AppBar 自己造的一個React Component 輪子*/}
                <AppBar title="習慣"/>
                <ul>
                    {this.props.customList.map(customItem => // 遍歷customList渲染customItem
                        <CustomItem
                            key={customItem.id}
                            id={customItem.id}
                            completed={customItem.completed}
                            text={customItem.text}/>
                    )}
                </ul>
                <div>
                    <input
                        type="text"
                        value={this.state.text}
                        onChange={(e) => {  // 監(jiān)聽用戶輸入
                            this.setState({
                                text: e.target.value
                            });
                        }}/>
                    <button
                        onClick={() => {
                            console.log('添加 click');
                            // 發(fā)送添加習慣項Action
                            this.props.dispatch(addCustomItemAction(this.state.text));
                            // 把輸入置為空
                            this.setState({
                                text: ''
                            });
                        }}>
                        添加
                    </button>
                </div>
            </section>
        );
    }
}

// 關(guān)聯(lián)習慣列表的props與state,不關(guān)聯(lián)的話全局的state.customList就沒法作為props屬性傳進來
const mapStateToProps = (state) => {
    return {
        customList: state.customList
    };
};

// 這個方法是用來綁定dispatch的,這里直接在組件里調(diào)用dispatch了,所以就沒有用到這個方法
// const mapDispatchToProps = {};

// 使用connect函數(shù)包裹組件,從而獲得store上下文,可以在組件里使用this.props.dispatch訪問到dispatch方法
CustomList = connect(mapStateToProps)(CustomList);

export default CustomList;
  • components/custom/custom-item/CustomItem.js
/**
 * 習慣Item頁
 * Created by BadWaka on 2017/3/11.
 */
import React, {Component} from 'react';
import './custom-item.css';

// 引入另一個CheckBox輪子
import CheckBox from '../../common/check-box/CheckBox';

// 引入用到的action
import {changeCustomItemCompletionStatusAction} from '../../../redux/custom/actions';
// 引入connect函數(shù)用來生成Redux組件
import {connect} from 'react-redux';

class CustomItem extends Component {

    render() {
        return (
            <li className="custom-item"
                style={{
                    textDecoration: this.props.completed ? 'line-through' : 'none'
                }}
                onClick={() => {
                    console.log('習慣項點擊 customItem click id', this.props.id, 'completed', this.props.completed);
                    // 發(fā)送改變習慣項完成狀態(tài)Action
                    this.props.dispatch(changeCustomItemCompletionStatusAction(this.props.id));
                }}>
                <span>{this.props.text} {'' + this.props.completed}</span>
                <CheckBox checked={this.props.completed}/>
            </li>
        );
    }
}

// 不需要綁定state.customList作為props,也沒法綁。。因為不知道具體是哪個;所以讓父組件去綁就好了
// 也不需要dispatch,因為組件內(nèi)部已處理了
// 所以直接調(diào)用connect()方法把CustomItem組件變成Redux組件就好了
CustomItem = connect()(CustomItem);

export default CustomItem;

注釋都寫在代碼里了。。運行一下可以就看到現(xiàn)象了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內(nèi)容