Redux學習筆記

姜小白發表於2017-11-02

設計思想

核心概念

  • 所有的狀態存放在Store。元件每次重新渲染,都必須由狀態變化引起。
  • 使用者在 UI 上發出action
  • reducer函式接收action,然後根據當前的state,計算出新的state

動機

  • 隨著 JavaScript 單頁應用開發日趨複雜,JavaScript 需要管理比任何時候都要多的 state (狀態)。通過限制更新發生的時間和方式,Redux 試圖讓 state 的變化變得可預測。

生命週期

Redux 應用中資料的生命週期遵循下面 4 個步驟:

呼叫 store.dispatch(action)。
  • Action 就是一個描述“發生了什麼”的普通物件。比如:
{ type: 'LIKE_ARTICLE', articleId: 42 };
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } };
{ type: 'ADD_TODO', text: 'Read the Redux docs.'};複製程式碼
  • 可以把 action 理解成新聞的摘要。如 “瑪麗喜歡42號文章。” 或者 “任務列表裡新增了'學習 Redux 文件'”。
    你可以在任何地方呼叫 store.dispatch(action),包括元件中、XHR 回撥中、甚至定時器中。
Redux store 呼叫傳入的 reducer 函式。
  • Store 會把兩個引數傳入 reducer: 當前的 state 樹和 action。例如,在這個 todo 應用中,根 reducer 可能接收這樣的資料:
// 當前應用的 state(todos 列表和選中的過濾器)
let previousState = {
    visibleTodoFilter: 'SHOW_ALL',
    todos: [
        {
            text: 'Read the docs.',
            complete: false
        }
    ]
}

// 將要執行的 action(新增一個 todo)
let action = {
    type: 'ADD_TODO',
    text: 'Understand the flow.'
}

// render 返回處理後的應用狀態
let nextState = todoApp(previousState, action);複製程式碼
  • 注意 reducer 是純函式。它僅僅用於計算下一個 state。它應該是完全可預測的:多次傳入相同的輸入必須產生相同的輸出。它不應做有副作用的操作,如 API 呼叫或路由跳轉。這些應該在 dispatch(action) 前發生。
根 reducer 應該把多個子 reducer 輸出合併成一個單一的 state 樹。
  • reducer 的結構完全由你決定。Redux 原生提供combineReducers()輔助函式,來把根 reducer 拆分成多個函式,用於分別處理 state 樹的一個分支。

-下面演示 combineReducers() 如何使用。假如你有兩個 reducer:一個是 todo 列表,另一個是當前選擇的過濾器設定:

function todos(state = [], action) {
    // 省略處理邏輯...
    return nextState;
}

function visibleTodoFilter(state = 'SHOW_ALL', action) {
    // 省略處理邏輯...
    return nextState;
}

let todoApp = combineReducers({
    todos,
    visibleTodoFilter
})複製程式碼
  • 當你觸發 action 後,combineReducers 返回的 todoApp 會負責呼叫兩個 reducer:
let nextTodos = todos(state.todos, action);
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action);複製程式碼
  • 然後會把兩個結果集合併成一個 state 樹:
return {
    todos: nextTodos,
    visibleTodoFilter: nextVisibleTodoFilter
};複製程式碼
  • 雖然 combineReducers() 是一個很方便的輔助工具,你也可以選擇不用;你可以自行實現自己的根 reducer
Redux store 儲存了根 reducer 返回的完整 state 樹。
  • 這個新的樹就是應用的下一個 state!所有訂閱 store.subscribe(listener) 的監聽器都將被呼叫;監聽器裡可以呼叫 store.getState() 獲得當前 state。

  • 現在,可以應用新的 state 來更新 UI。如果你使用了 React Redux 這類的繫結庫,這時就應該呼叫 component.setState(newState) 來更新。

三大原則

單一資料來源

  • 整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中。

State 是隻讀的

  • 惟一改變 state 的方法就是觸發 actionaction 是一個用於描述已發生事件的普通物件。

使用純函式來執行修改

  • 為了描述 action 如何改變 state tree ,你需要編寫 reducers

Redux 核心API

  • Redux的核心是一個store,這個store有Redux提供的createStore(reducers,[initialState])方法生成。從函式簽名看出,想生成store,必須傳入reducers,同時也可以傳入第二個可選引數初始化狀態initialState

createStore

  • 使用方法 const store = createStore(reducer);

  • reducer

    • 在Redux裡,負責響應action並修改資料的角色就是reducerreducer本質上是一個純函式,其函式簽名為reducer(previousState, action) => newStatereducer在處理action時,需傳入一個previousState引數。reducer的職責就是根據previousStateaction來計算出新的newState
    • 使用方法將reducer即下面的todo作為引數傳入createStore(todo)中

      //以下為reducer的格式
      const todo = (state = initialState, action) => {
        switch(action.type) {
            case 'XXX':
                return //具體的業務邏輯;
            case 'XXX':
                return //具體的業務邏輯;   
            default:
                return state;
        }
      }複製程式碼
  • getState()

    • 使用方法getState()
    • 獲取store中的狀態。
  • dispatch(action)

    • 使用方法store.dispatch(action)
    • 分發一個action,並返回這個action,這是唯一能改變store中資料的方式。store.dispatch接受一個Action物件作為引數,將它傳送出去。
  • subscribe(listener)

    • 使用方法store.subscribe(listenter)
    • 註冊一個監聽者,它在store發生變化時被呼叫,一旦State發生了變化,就會自動執行這個函式。通過subscribe繫結了一個監聽函式之後,只要dispatch了一個action,所有監聽函式都會自動執行一遍。
  • replaceReducer(nextReducer)

    • 更新當前store裡的reducer,一般只會在開發者模式中呼叫該方法。
  • createStore的實現

    • 包含getState()dispatch(action)subscribe(listener);本函式近似原始碼,可簡單實現功能與幫助理解createStore的原理

      const createStore = (reducer) => {
        let state; //宣告一個變數承接狀態
        let list = [];//宣告一個陣列用於儲存監聽函式
        const getState = () => {
            return state;//直接返回state;
        }
        const dispatch = (action) =>{
            state = reducer(state, action);//更新狀態,且迴圈list陣列,並執行裡面的事件
            list.forEach((fn) => {
                fn();
            })
        }
        const subscribe = (fn) => {
            list.push(fn);//將函式傳入list中
            return () => {
                list = list.filter(cd => cd != fn)
            }
        }
        return {
            getState,
            subscribe,
            dispatch
        }
      }複製程式碼
  • combineReducers

    • 隨著應用變得複雜,需要對 reducer 函式 進行拆分,拆分後的每一塊獨立負責管理 state 的一部分。combineReducers 輔助函式的作用是,把一個由多個不同 reducer 函式作為 valueobject,合併成一個最終的 reducer 函式,然後就可以對這個 reducer 呼叫 createStore
  • 使用方法

    • 將多個不同的reducer作為物件的屬性傳入combineReducers({})函式中,
const rootReducer = combineReducers({
    reducer1,
    reducer2,
    ...
})複製程式碼
  • 返回值

    • (Function):一個呼叫 reducers 物件裡所有 reducer 的 reducer,並且構造一個與 reducers 物件結構相同的 state 物件。

        let store = createStore(rootReducer)
        //store = {
            reducer1: ... ,
            reducer2: ... ,
            ...
        }
        let {reducer1, reducer2} = store; //取出複製程式碼
  • 模擬實現combineReducers

//研究邏輯看這個
const combineReducers = (reducers) => {
    return (state = {}, action) => {
        let newState = {};
        Object.keys(reducers).forEach((key) =>{
            newState[key] = reducers[key](state[key], action);
        })
        return newState;
    }
}

//簡寫裝逼看這個
const combineReducers = (reducers) => (state = {}, action) => Object.keys(reducers).reduce((newState, key) => {
    newState[key] = reducers[key](state[key], action);
    return newState;
},{})複製程式碼

react的跨級元件通訊(蟲洞)幫助理解(react-redux)

  • 此方法為React中的方法,隨著應用變的越來越複雜,元件巢狀越來越深,有時要從最外層將一個資料一直傳遞到最裡層。
  • 理論上,通過prop一層層傳遞下去當然是沒問題的。不過這也太麻煩啦,要是能在最外層和最裡層之間開一個穿越空間的蟲洞就好了。
  • React的開發者也意識到這個問題,為我們開發出了這個空間穿越通道 —— Context
  • 注意:context一直都在React原始碼中,但在React0.14版本才被記錄官方文件。官方並不太推薦大量使用,雖然它可以減少逐級傳遞,但當元件複雜時,我們並不知道context是從哪傳來的。它就類似於全域性變數。

  • context使用方法

    • 在外層定義一個getChildContext方法,在父層制定childContextTypes

      class Provider extends Component{
        getChildContext() {
            return {store: ...};
        }
        render(){
            return(
                this.props.children
            )
        }
      }
      
      Provider.childContextTypes = {
        store : React.PropTypes.object
      };複製程式碼
    • 在內層設定元件的contextTypes後,即可在元件裡通過this.context.來訪問。

      class child extends Component{
        render(){
            const store = this.context.store;
            return(
                <div>1</div>
            )
        }
      }
      child.contextTypes = {
        store: React.PropTypes.object
      }複製程式碼

解讀react-redux

  • 前面說到Redux的核心只有一個createStore()方法,這樣還不足以讓Redux在我們的react應用中發揮作用,還需要react-redux庫 ———— Redux官方提供的React繫結。
  • react-redux提供了一個元件和一個API幫助Redux和React進行繫結,一個是React元件<Provider />, 一個是connect()。關於它們,我們需要知道的是,<Provider />接受一個store作為props,它是整個Redux應用的頂層元件,而connect()提供了在整個React應用的任意元件中獲取store中資料的功能。

Provider

  • 其實就是建立一個外層包裹住整個Redux應用。

  • <Provider />主要原始碼

export default class Provider extends Component {
    getChildContext() {
        return { store: this.store }
    }

    constructor(props, context) {
        super(props, context)
        this.store = props.store
    }

    render() {
        return Children.only(this.props.children)
    }
}複製程式碼
  • 用法
ReactDom.render(
    <Provider store = {store}>
      <App />
    </Provider>,
    document.getElementById("root")
)複製程式碼

connect

  • 使用方法connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(TodoApp)

  • 上面程式碼看似那麼長,但其實理解起來不太難,前四個引數是選填屬性,根據需求填入即可。connect(...)呼叫後會返回一個函式這個函式可傳一個引數,即你需要繫結的元件。

  • 模擬實現connect函式,只針對前兩個關鍵引數。
const connect = (mapStateToProps, mapDispatchToProps) => {
    return (WrapperComponent) => {
    class Connect extends Component {
        componentDidMount() {
            const store = this.context.store;
            this.unsubscribe = store.subscribe(() => {
                this.forceUpdate();
            })
        }
        componentWillUnmount() {
            this.unsubscribe();
        }
        render (){
            const store = this.context.store;
            const stateProps = mapStateToProps(store.getState());
            const dispatchProps = mapDispatchToProps(store.dispatch);
            const props = Object.assign({}, stateProps, dispatchProps);

            // return <WrapperComponent {...props} />;
            return React.createElement(WrapperComponent, props);
        }
    }
    Connect.contextTypes = {
        store: React.PropTypes.object
    };
        return Connect;
    }
}複製程式碼

mapStateToProps

  • 官方解釋: 如果定義該引數,元件將會監聽 Redux store 的變化。任何時候,只要 Redux store 發生改變,mapStateToProps 函式就會被呼叫。該回撥函式必須返回一個純物件,這個物件會與元件的 props 合併。如果你省略了這個引數,你的元件將不會監聽 Redux store。如果指定了該回撥函式中的第二個引數 ownProps,則該引數的值為傳遞到元件的 props,而且只要元件接收到新的 propsmapStateToProps 也會被呼叫。

  • 使用方法(其實裡面第一個引數就是最早在 <Provider store = {store}>傳入的store,於是可以在子元件上訪問store裡的屬性)

const mapStateToProps = (state, [ownProps]) => {
    return {
        todos : state.todos
    }
}複製程式碼

mapDispatchToProps

  • 官方解釋: 如果傳遞的是一個物件,那麼每個定義在該物件的函式都將被當作 Redux action creator,而且這個物件會與 Redux store 繫結在一起,其中所定義的方法名將作為屬性名,合併到元件的 props 中。如果傳遞的是一個函式,該函式將接收一個 dispatch 函式,然後由你來決定如何返回一個物件,這個物件通過 dispatch 函式與 action creator 以某種方式繫結在一起(提示:你也許會用到 Redux 的輔助函式 bindActionCreators())。如果你省略這個 mapDispatchToProps 引數,預設情況下,dispatch 會注入到你的元件 props 中。如果指定了該回撥函式中第二個引數 ownProps,該引數的值為傳遞到元件的 props,而且只要元件接收到新 propsmapDispatchToProps 也會被呼叫。
  • 使用方法(用於傳遞方法)。其實總而言之,mapStateToProps是用來傳遞屬性狀態的,而mapDispatchToProps是用來傳遞改變的方法的。
const mapDispatchToProps = (dispatch, [ownProps]) => {
    return{
        ... : () => {
            dispatch(...)
        }
    }
}複製程式碼

mergeProps

  • mergeProps(stateProps, dispatchProps, ownProps)可以接受stateProps, dispatchProps, ownProps三個引數。
  • stateProps就是傳給connect的第一個引數mapStateToProps最終返回的props
  • dispatchProps就是傳給connect的第二個引數mapDispatchToProps最終返回的props
  • ownProps則為元件自己的props

options

  • 如果指定這個引數,可以定製 connector 的行為。

    • [pure = true] (Boolean): 如果為 trueconnector 將執行 shouldComponentUpdate 並且淺對比 mergeProps 的結果,避免不必要的更新,前提是當前元件是一個“純”元件,它不依賴於任何的輸入或 state 而只依賴於 props 和 Redux storestate。預設值為 true

    • [withRef = false] (Boolean): 如果為 trueconnector 會儲存一個對被包裝元件例項的引用,該引用通過 getWrappedInstance() 方法獲得。預設值為 false

相關文章