讓react用起來更得心應手——(react-redux)

夢想攻城獅發表於2019-03-04

讓react用起來更得心應手系列文章:

  1. 讓react用起來更得心應手——(react基礎簡析)
  2. 讓react用起來更得心應手——(react-router原理簡析)
  3. 讓react用起來更得心應手——(react-redux原理簡析)

Redux應用場景

在沒有redux出來之前,父元件和子元件之間,平行元件之間傳遞和修改狀態,需要將狀態和修改狀態的方法逐級往下傳,元件巢狀過深則很容易出現管理混亂的問題。所以redux就是為了解決狀態管理而誕生的。

redux應用場景

Redux實現

基礎框架

//用來建立Store
function createStore(reducer){}

// 抽離出不同的type呼叫dispatch函式的複用部分
function bindActionCreators(actions,dispatch){}

// 合併reducer
function combineReducers (reducers){}

// 使用中介軟體形成一個新的dispatch覆蓋原來的dispatch,並返回store
function applyMiddleware (...middlewares){}

//合併中介軟體
function compose(...args){}

export  {
  createStore,
  bindActionCreators,
  combineReducers,
  compose,
  applyMiddleware
}
複製程式碼
Redux的設計思想

Redux的核心createStore

  1. 建立一個state變數用來儲存平行元件間需要共享的變數
  2. 提供getState方法給外界獲取state
  3. 提供subscribe方法給外界訂閱回撥在dispatch修改state時執行
  4. 提供dispatch方法給外界呼叫使用者註冊的reucer來修改state並執行訂閱的回撥
function createStore(reducer){
    let state;  //存放狀態
    let listeners = []; //存放訂閱的回撥
    
    function dispath(action){
        state = reducer(state, action); // 呼叫reducer返回新的state
        listeners.forEach(fn=>fn());    // 釋出所有訂閱的回撥
    }
    
    // 派發初始動作,type為reducer中沒有的型別,目的是初始化狀態為使用者設定的狀態
    dispatch({type:`@INIT`}); 
    
    function getState(){
        // 暴露的state屬性不希望別人能改,改了也不會影響原有的狀態
        return JSON.parse(JSON.stringify(state));
    }
    
    function subscribe(fn){
        //訂閱回撥,並返回一個從listeners刪除回撥的函式
        listeners.push(fn); 
        return ()=>{listeners = listeners.filter(l=>l!=fn)};
    }
    }
    return {
        getState,
        dispatch,
        subscribe
    }
}
複製程式碼

為了理解上面的東西,看下面的用例:

let initState = {
  title: { color: "red", text: "kbz" }
};

function reducer(state = initState, action) {
  switch (action.type) {
    case "CHANGE_TITLE_COLOR":
      return { ...state, title: { ...state.title, color: action.color } };
      break;
    case "CHANGE_TITLE_TEXT":
      return { ...state, content: { ...state.title, text: action.text } };
      break;
  }
  return state;
}
let store = createStore(reducer);

let unsubcribe = store.subscribe(function() {
  console.log(store.getState().title.color);
});

setTimeout(() => {
  store.dispatch({ type: "CHANGE_TITLE_COLOR", color: "yellow" });
}, 1000);
setTimeout(() => {
  unsubcribe();
  store.dispatch({ type: "CHANGE_TITLE_COLOR", color: "blue" });
}, 2000);

複製程式碼

bindActionCreators

在建立元件的時候,元件應該是存粹的,使用store.dispatch等其它變數,需要根據情況而區別的引入不同的變數,最好使用state或者props,所以需要將action和dispatch抽離封裝然後賦予到元件的prop上

  1. 抽離type
//action-types.js
export const ADD = `ADD`;
export const MINUS = `MINUS`;
複製程式碼
  1. 抽離action
//actions.js
import * as types from `../action-types`;
let actions = {
  add(count){
    return {type:types.ADD,count}
  },
  minus(count){
     return {type:types.MINUS,count}
  }
}
export default actions
複製程式碼
  1. 批量抽離dispatch
function bindActionCreators(actions,dispatch){
  let obj = {}
  for(let key in actions){ 
    obj[key] = (...args)=>dispatch(actions[key](...args))
  }
  return obj;
}
複製程式碼
  1. 屬性賦值
import React,{Component} from `react`;
import {render} from `react-dom`;
import Counter from `./components/Counter`;
import {bindActionCreators} from `redux`;
import store from `./store`;
inmport action form `./action`

let props= bindActionCreators(actions,store.dispatch)

render(<Counter {...props}></Counter>,window.root);
複製程式碼

combineReducers

為了更好的模組化管理,可以將每個元件的reducer分開來建立,然後再通過combineReducers將所有的reducer合併起來,其原理就是建立一個函式,dispatch的時候將所有reducer都執行一次

function combineReducers (reducers){
  //返回一個總的totalReducer,和所有的reducer一樣接收state和action
  return (state={},action)=>{
    // totalState登記每一個元件的state
    // 遍歷執行所有的reducer,將返回的state重新登記在totalState中
    let obj = {};   
    for(let key in reducers){
      obj[key] = reducers[key](state[key],action)
    }
    return obj;
  }
}
複製程式碼

applyMiddleware

常用的中介軟體

中介軟體原理:在原來的dispatch方法外包裝一層函式,擴充套件其他功能,又能保證原來功能的使用。

// 列印日誌中介軟體
let reduxLogger = (store)=>(dispatch)=>(action)=>{
  console.log(`prev`,store.getState());
  dispatch(action)
  console.log(`next`,store.getState());
}

//
let reduxThunk = (store)=>(dispatch)=>(action)=>{
  // 如果是函式將正真的dispatch傳給使用者,使用者抉擇是否要派發
  if(typeof action === `function`){
    return action(dispatch,store.getState); 
  }
  dispatch(action); // 直接把物件派發即可
}

let reduxPromise = (store)=>(dispatch)=>(action)=>{
  // 判斷當前action是不是一個promise,如果是promise就執行,執行的時候只會管成功的結果
  if( action.then &&typeof(action.then) == `function`){
    return action.then(dispatch);
  }else if(action.payload && action.payload.then){  //action.payload是否為promise
    return action.payload.then(data=>{
      dispatch({...action,payload:data}); 
    },err=>{
      dispatch({...action,payload:err});
      return Promise.reject(err); // 對外丟擲錯誤
    })
  }
  return dispatch(action); 
}
複製程式碼

applyMiddleware基礎原理

let applyMiddleware = (middleware)=> (createStore)=> (reducer)=>{
  let store = createStore(reducer);
  
  // 返回新的dispatchL:(action)=>{xxxxxx}
  let fn = middleware(store);
  let newDispatch = fn(store.dispatch);
  
  //覆蓋原有的dispatch,返回{getState,dispatch:newDispatch,subscribe}
  return {...store,dispatch:newDispatch};   
}
// 典型的柯里化,把多個middleware連起來,後面compose會介紹
export default applyMiddleware(reduxLogger)(createStore)(reducer);
複製程式碼

compose

compose的原理

專案中使用的外掛不止一個,在使用多個外掛的情況下,需要使用一個方法將多個外掛合併成一個。

function add(a,b){
  return a+b;
}
function toUpperCase(str){
  return str.toUpperCase();
}
function len(str){
  return str.length
}

function compose(...args){
  return args.reduce((a,b)=>{(...args)=>a(b(...args))});
}
compose(len,toUpperCase,add)(a,b);  //(a,b) => len(toUpperCase(add(a,b)))
複製程式碼
a b 返回函式
len toUpperCase (…args)=>len(toUpperCase(…args))
(…args)=>{len(toUpperCase(…args)} add (…args)=>len(toUpperCase(add(…args)))

完善applyMiddleware

let reduxLogger = (store)=>(dispatch)=>(action)=>{
  console.log(`prev`,store.getState());
  dispatch(action)
  console.log(`next`,store.getState());
}

let applyMiddleware = (...middlewares)=> (createStore)=> (reducer)=>{
  let store = createStore(reducer);
  let fns = middlewares.map(middleware=>{
    return middleware(store)    //返回的函式接受disopatch用於在原來的基礎上擴充套件
  });
  
  // compose(fn1,fn2)(store.dispatch)
  //fn執行返回一個新的包裝dispatch函式傳給fn1
  let newDispatch = compose(...fns)(store.dispatch);
  
  return {...store,dispatch:newDispatch};   //將合併後的dispatch覆蓋原來的最初的dispatch
}
function compose(...args){
  return args.reduce((a,b)=>((...args)=>a(b(...args))));
}
複製程式碼

redux完整程式碼

function createStore(reducer,fn) {
  let state;
  let listeners = [];
  let dispatch = (action) => {
    state = reducer(state,action);
    listeners.forEach(fn=>fn());
  }
  dispatch({type:`@INIT`});
  
  // createStore(reducer,applyMiddleware(...middlewares))一步到位
  // 在內部使用applyMiddleware(...middlewares)(createStore)(reducer)
  if(typeof fn === `function`){ 
    return fn(createStore)(reducer);
  }
  let getState = ()=> JSON.parse(JSON.stringify(state));
  let subscribe = (fn)=>{
    listeners.push(fn);
    return ()=>{
      listeners = listeners.filter(l=>l!=fn);
    }
  }
  return {getState,subscribe,dispatch}
}
function bindActionCreators(actions,dispatch){
  let obj = {}
  for(let key in actions){ 
    obj[key] = (...args)=>dispatch(actions[key](...args))
  }
  return obj;
}
let combineReducers = (reducers)=>{
  return (state={},action)=>{
    let obj = {}
    for(let key in reducers){
      obj[key] = reducers[key](state[key],action)
    }
    return obj;
  }
}
let applyMiddleware = (...middlewares)=> (createStore)=> (reducer)=>{
  let store = createStore(reducer);
  let fns = middlewares.map(middleware=>{
    return middleware(store)
  });
  let newDispatch = compose(...fns)(store.dispatch);
  return {...store,dispatch:newDispatch};
}
function compose(...args){
  return args.reduce((a,b)=>((...args)=>a(b(...args))));
}
export  {
  createStore,
  bindActionCreators,
  combineReducers,
  compose,
  applyMiddleware
}
複製程式碼

React-redux

Redux和React的關係

Redux是一款狀態管理庫,並且提供了react-redux庫來與React親密配合,這兩者的關係如下圖:

Redux和React

從上面可以看出,React-redux通過Provider和connet將Redux和React聯絡起來:

React-redux框架

import React,{Component} from `react`;
import {bindActionCreators} from `./redux`
let Context = React.createContext();

//將store掛載在contex上,供巢狀元件使用
class Provider extends Component{}

// connect的作用就是獲取store,子元件獲取contex上的store
let connect = (mapStateToProps,mapDispatchToProp)=>{}

export  {
  Provider,
  connect
}
複製程式碼

Provider

React會提供一個createContext的API,呼叫它會生成一個Context,裡面包含了Provider元件和Consume元件,Provider提供一個狀態供跨元件使用,需要使用狀態的元件只要巢狀在Consume中就獲取Provider提供的狀態。讓react用起來更得心應手——(react基礎解析)裡面有介紹,這裡不贅述。

let Context = React.createContext();
class Provider extends Component{
  // 將React-redux中的Provide包裝了react提供的API生成的Context.Provider
  //<Provider store={xxxx}></Provider>,將store掛載在contex上
  render(){
    return  <Context.Provider value={{store:this.props.store}}>
        {this.props.children}   //子元件
      </Context.Provider>
  }
}
複製程式碼

connect

既然有掛載store,就必然有子元件獲取store,connect的作用就是獲取提供好的store

//呼叫方法:connect(mapStateToProps,mapDispatchToProp)(Com)
// connect是一個高階元件,呼叫後的返回一個元件
let connect = (mapStateToProps,mapDispatchToProp)=>(Com) =>{
    return ()=>{ 
      // 高階元件的特點就是把元件中公用的邏輯抽取來,返回一個經過處理的元件
      class Proxy extends Component{ 
        state = mapStateToProps(this.props.store.getState())
        componentWillMount(){
          this.unsub = this.props.store.subscribe(()=>{
            this.setState(mapStateToProps(this.props.store.getState()))
          })
        }
        componentWillUmount(){
          this.unsub()
        }
        
        //mapStateToProps就是將state中的部分或全部狀態對映到需要的元件中作為其props
        //mapDispatchToProp就是將action中已經繫結成dispatch形式的action按需求對映到需要的元件作為其props
        
        render(){
          let b
          if(typeof mapDispatchToProp === `function`){
              b = mapDispatchToProp(this.props.store.dispatch);
          }else{
              // bindActionCreators把直接將所有action的繫結成diapatch(action)形式組成一個物件
              b = bindActionCreators(mapDispatchToProp,this.props.store.dispatch)
          }
          //將所有的state和修改state的方法以props的方式傳入
          return <Com {...this.state} {...b}></Com>
        }
      }
      
      //呼叫Consumer將獲取到的store傳給包裝Com的Proxy
      return <Context.Consumer>
       {({store})=>{
          return <Proxy store={store}></Proxy>
       }}
      </Context.Consumer>
    }
}
複製程式碼

用例:

import React,{Component} from `react`;
import actions from `../store/actions/counter`;
import {connect} from `react-redux`;
class Counter extends Component{
  render(){
    return (<div>
      <button onClick={()=>{
         this.props.add(2);
      }}>+</button>
        {this.props.number}
        <button  onClick={()=>{
          this.props.minus(2);
      }}>-</button>
    </div>)
 }
}
// mapStateToProps使用者自己定義需要的狀態
let mapStateToProps = (state)=>{ 
  return {number:state.counter.number}
}

// action也是使用者自己定義的,可以是函式可以是物件
// 如果傳遞過來的不是方法是物件,會把這個物件自動用bindActionCreators包裝好
export default connect(mapStateToProps,actions)(Counter);
複製程式碼

結語:

個人使用一種框架時總有一種想知道為啥這樣用的強迫症,不然用框架用的不舒服,不要求從原始碼上知道其原理,但是必須得從心理上說服自己。

相關文章