Redux 家族(一):實(shí)戰(zhàn)和源碼實(shí)現(xiàn)

每一種新語言志鹃,新框架或者工具的出現(xiàn)都是為了解決項(xiàng)目中存在的問題夭问。

動(dòng)機(jī)

隨著 Javascript 單頁應(yīng)用的開發(fā)越來越復(fù)雜,管理不斷變化的 state 變得非常困難曹铃。這些狀態(tài)有很多:UI 狀態(tài)缰趋,服務(wù)器數(shù)據(jù),緩存數(shù)據(jù)等陕见。而組件秘血,頁面之間狀態(tài)共享也是一個(gè)問題。

在 React 中评甜,數(shù)據(jù)在組件中是單向流動(dòng)的灰粮,數(shù)據(jù)從父組件傳遞給子組件是通過 props,由于這個(gè)特征忍坷,兩個(gè)非父子關(guān)系的組件(或叫兄弟組件)之間的通信就比較麻煩粘舟。

Redux 的出現(xiàn)正式是為了解決 state 數(shù)據(jù)的管理問題。

設(shè)計(jì)思想

Redux 將整個(gè)應(yīng)用的狀態(tài)都存儲(chǔ)在一個(gè)地方佩研,就是 Store(容器)柑肴,里面保存著應(yīng)用的 state(狀態(tài)樹),組件可以通過 dispach(派發(fā))action(行為)給 store(狀態(tài)容器)旬薯,而不是直接通知其他組件晰骑。其他組件可以通過訂閱 store 中的 state(狀態(tài))來更新視圖。


三大原則

寫 Redux 需要遵循三個(gè)原則绊序,有制約硕舆,有規(guī)則隶症,狀態(tài)才會(huì)可預(yù)測。因此 Redux 需要遵循以下三個(gè)原則:

單一數(shù)據(jù)源

整個(gè)應(yīng)用的 state 被儲(chǔ)存在一棵 Object tree 中岗宣,并且這個(gè) Object tree 只存在于唯一一個(gè) store 中蚂会;

State 是只讀

State 是只讀的,惟一改變 state 的方法就是dispatch(觸發(fā)) action(動(dòng)作)耗式;

使用純函數(shù)來執(zhí)行修改

通過 disptch action 來改變 state胁住,action 是一個(gè)用于描述已發(fā)生事件的普通對(duì)象,使用純函數(shù)(沒有副作用的函數(shù):函數(shù)的輸出完全取決函數(shù)的參數(shù)刊咳;副作用:獲取網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)彪见,修改 dom 標(biāo)題等都屬于副作用)來執(zhí)行修改,為了描述 action 如何改變 state tree 娱挨,需要編寫 reducer (處理器)余指。

單一數(shù)據(jù)源的設(shè)計(jì)讓組件之間的通信更加方便,同時(shí)也便于狀態(tài)的統(tǒng)一管理跷坝。


基本使用

通過一個(gè) React 計(jì)數(shù)器組件來使用一下酵镜,新建一個(gè)項(xiàng)目,刪除多余的文件柴钻,啟動(dòng):

npx create-react-app redux-family
cd redux-family
npm install redux -S
npm start

編寫一個(gè) Counter 組件淮韭,應(yīng)用 redux :
通過使用createStore來創(chuàng)建一個(gè)srore倉庫,參數(shù)就是reducer初始 state贴届,通過 store.getState()方法獲取狀態(tài)靠粪,store.dispatch來派發(fā)動(dòng)作,store. subscribe訂閱更新毫蚓,store.unsubscribe取消訂閱占键,這些基本就是 redux 的全部了:

// src/components/Counter1.js
import React, { Component } from 'react';
import { createStore} from 'redux';

const INCREMENT = 'ADD';
const DECREMENT = 'MINUS';

const reducer = (state = initState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { number: state.number + 1 };
    case DECREMENT:
      return { number: state.number - 1 };
    default:
      return state;
    }
}

let initState = { number: 0 };

const store = createStore(reducer, initState);

export default class Counter extends Component {
    unsubscribe;
    constructor(props) {
      super(props);
      this.state = { number: 0 };
    }
    componentDidMount() {
      this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().number }));
    }
    componentWillUnmount() {
      this.unsubscribe();
    }
    render() {
      return (
        <div>
          <p>{this.state.number}</p>
          <button onClick={() => store.dispatch({ type: 'ADD' })}>+</button>
          <button onClick={() => store.dispatch({ type: 'MINUS' })}>-</button>
          <button onClick={
            () => {
                setTimeout(() => {
                    store.dispatch({ type: 'ADD' });
                }, 1000);
            }
          }>1秒后加1</button>
        </div>
      )
    }
}
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Counter1 from './components/Counter1';

ReactDOM.render(
  <Counter1 />,
  document.getElementById('root')
);

結(jié)合 React 框架 react-redux

react-redux 用來整合 react 狀態(tài)和數(shù)據(jù)訂閱,使其使用起來更簡便元潘。提供 Provider使所有組件都可以獲取狀態(tài)畔乙,connect聯(lián)接狀態(tài)和組件,獲取state狀態(tài)柬批,dispatch派發(fā)動(dòng)作:

npm install react-redux -S
npm start
// src/components/Counter2.js
import React, { Component } from 'react';
import { connect } from 'react-redux'

class Counter extends Component {
    render() {
      return (
        <div>
          <p>{this.props.number}</p>
          <button onClick={() => this.props.add()}>+</button>
          <button onClick={() => this.props.minus()}>-</button>
          <button onClick={
            () => {
                setTimeout(() => {
                  this.props.add({ type: 'ADD' });
                }, 1000);
            }
          }>1秒后加1</button>
        </div>
      )
    }
}
const mapStateToProps = (state) => {
  return {
    number: state.number
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    add: () => dispatch({type: 'ADD'}),
    minus: () => dispatch({type: 'MINUS'})
  };
}
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore} from 'redux';
import { Provider } from 'react-redux'
import Counter2 from './components/Counter2'

const INCREMENT = 'ADD';
const DECREMENT = 'MINUS';

const reducer = (state = initState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { number: state.number + 1 };
    case DECREMENT:
      return { number: state.number - 1 };
    default:
      return state;
    }
}

let initState = { number: 0 };

const store = createStore(reducer, initState);

ReactDOM.render(
  <Provider store={store}>
    <Counter2 />
  </Provider>,
  document.getElementById('root')
);

不僅僅使用在 React 上

Redux 是一個(gè)狀態(tài)數(shù)據(jù)流管理方案啸澡,不是為了 react 單獨(dú)實(shí)現(xiàn)的,可以使用在任何框架以及原生 Javascript 中氮帐,這更體現(xiàn)了 Redux 的廣闊的視角和先進(jìn)的思想。

下面在原生 Javascript 應(yīng)用中使用如下:
需要修改下 index.html洛姑,增加顯示數(shù)據(jù)的 dom上沐,以及操作按鈕的 dom:

// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
    <div id="counter">
      <p id="number">0</p>
      <button id="add">+</button>
      <button id="minus">-</button>
    </div>
  </body>
</html>

index.js文件中通過訂閱渲染函數(shù)來做數(shù)據(jù)的更新:

// src/index.js
import { createStore} from 'redux';

let counterValue = document.getElementById('number');
let incrementBtn = document.getElementById('add');
let decrementBtn = document.getElementById('minus');

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
let initState = { number: 0 };

const reducer = (state = initState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { number: state.number + 1 };
    case DECREMENT:
      return { number: state.number - 1 };
    default:
      return state;
  }
}

let store = createStore(reducer);

function render() {
  counterValue.innerHTML = store.getState().number;
}

store.subscribe(render);

render();

incrementBtn.addEventListener('click', function () {
  store.dispatch({ type: INCREMENT });
});
decrementBtn.addEventListener('click', function () {
  store.dispatch({ type: DECREMENT });
});

以上就是Redux的基本應(yīng)用,學(xué)會(huì)使用一個(gè)框架很快楞艾,但是不能僅限于使用参咙,還要知道其內(nèi)部的實(shí)現(xiàn)原理龄广,下面就一起來寫一個(gè)具有簡單功能的模仿的 redux

脫離框架的簡單實(shí)現(xiàn)(源碼)

createStore 創(chuàng)建倉庫

從使用中可以看出蕴侧,redux需要返回一個(gè)createStore方法择同,這個(gè)方法返回一個(gè)對(duì)象,對(duì)象上有getState()净宵,dispatch敲才,subscribe方法,getState()返回最新的state择葡,dispatch需要派發(fā)action紧武,subscribe需要訂閱更新函數(shù)并返回一個(gè)取消訂閱的函數(shù)redux內(nèi)部需要先派發(fā)一次INIT動(dòng)作填充初始狀態(tài)敏储。如此分析下來阻星,這樣一個(gè)redux殼就有了,先來寫殼已添,再往里面填充功能:
redux/index.js

import createStore from './createStore.js'

export {
  createStore
}

redux/utils/ActionType.js

/**
 * These are private action types reserved(保留的) by Redux.
 * For any unknown actions, you must return the current state.
 * (所有未知的action都必須返回當(dāng)前的state)
 * If the current state is undefined, you must return the initial state.
 * (如果當(dāng)前state是undefined妥箕,必須返回和一個(gè)初始的state)
 * Do not reference these action types directly in your code.
 * (不要在代碼中直接引入這些action type)
 */

const randomString = () => {
  return Math.random().toString(36).substring(7).split('').join('.')
}

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`
}

export default ActionTypes

redux/createStore.js

import ActionType from './utils/ActionType'

function createStore(reducer, preloadedState) {
  let currentState = preloadedState || {}
  let currentReducer = reducer
  let currentListeners = []

  const getState = () => {
    return currentState
  }
  const dispatch = (action) => {
    
  }

  const subscribe = (listener) => {
    return function unsubscribe() {
    }
  }

  // 內(nèi)部需要先派發(fā)一次動(dòng)作
  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates(填充)
  // the initial state tree.

  dispatch({type: ActionType.INIT})

  return {
    getState,
    dispatch,
    subscribe,
  }
}

export default createStore

這樣的殼子就寫好了,現(xiàn)在填充功能更舞,訂閱函數(shù)需要收集所有的訂閱事件存儲(chǔ)到數(shù)組中矾踱,并返回一個(gè)取消訂閱的函數(shù);
dispatch派發(fā)actionreducer獲取最新的狀態(tài)疏哗,最后通知所有的訂閱組件(這里是更新函數(shù))呛讲;

const subscribe = (listener) => {
    currentListeners.push(listener)
    // 返回取消訂閱的函數(shù)
    return function unsubscribe() {
      const index = currentListeners.indexOf(listener)
      // 取消訂閱就是將這個(gè)訂閱事件從數(shù)組中刪除
      currentListeners.splice(index, 1)
    }
  }
const dispatch = (action) => {
    // reducer:傳入舊的狀態(tài)和action,返回新的state
    currentState = currentReducer(currentState, action)

    const listeners = currentListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      // 執(zhí)行每一個(gè)訂閱函數(shù)
      listener()
    }
    return action
  }

這樣基本的redux功能就實(shí)現(xiàn)完了返奉,將原生 Javascript 應(yīng)用改成自己實(shí)現(xiàn)的redux贝搁,功能依然正常。

bindActionCreator

使用action creator直接派發(fā)動(dòng)作芽偏,不直接調(diào)用 dispatch雷逆,這是一種更便捷的方法。bindActionCreatoraction creator 轉(zhuǎn)換為帶有相同key污尉,但是每個(gè)函數(shù)都包含dispatch調(diào)用膀哲。

將之前直接在 react 中使用 redux 的例子重新改寫一下,使用 bindActionCreator 重寫一下實(shí)現(xiàn):

import React, { Component } from 'react';
import { createStore, bindActionCreators} from 'redux';

const INCREMENT = 'ADD';
const DECREMENT = 'MINUS';

const reducer = (state = initState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { number: state.number + 1 };
    case DECREMENT:
      return { number: state.number - 1 };
    default:
      return state;
    }
}

+ function Add() {
+  return { type: INCREMENT }
+}
+function Minus() {
+  return { type: DECREMENT }
+}

let initState = { number: 0 };
const store = createStore(reducer, initState);
+ const actions = { Add, Minus }
+ const boundActions = bindActionCreators(actions, store.dispatch)


export default class Counter extends Component {
    unsubscribe;
    constructor(props) {
      super(props);
      this.state = { number: 0 };
    }
    componentDidMount() {
      this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().number }));
    }
    componentWillUnmount() {
      this.unsubscribe();
    }
    render() {
      return (
        <div>
          <p>{this.state.number}</p>
+           <button onClick={boundActions.Add}>+</button>
+           <button onClick={boundActions.Minus}>-</button>
          <button onClick={
            () => {
                setTimeout(() => {
+                   boundActions.Add();
                }, 1000);
            }
          }>1秒后加1</button>
        </div>
      )
    }
}

可以看到所有 action 都由 action creator 派發(fā)被碗,不再顯式的調(diào)用store.dispatch某宪,bindActionCreator 內(nèi)部就是返回了 dispatch 函數(shù)的調(diào)用,下面我們來寫一下這個(gè) bindActionCreator 函數(shù)實(shí)現(xiàn):

思路:就是給每一個(gè)action creator 都返回 dispatch 方法調(diào)用锐朴,如果傳入的是函數(shù)兴喂,也就是單個(gè) action creator,直接調(diào)用返回 type,將參數(shù)傳給調(diào)用 dispatch 的函數(shù)衣迷,如果是一個(gè)對(duì)象畏鼓,就給這個(gè)對(duì)象上的每一個(gè) action creator 都加上 dispatch 方法調(diào)用,所以代碼的核心就是bindActionCreator

function bindActionCreator(actionCreator, dispatch) {
  // 這里派發(fā) action壶谒,執(zhí)行 actionCreator 函數(shù)云矫,返回一個(gè) type 作為 dispatch 的參數(shù)調(diào)用
  return function(...args) {
    dispatch(actionCreator.apply(this, args))
  }
}

完整的實(shí)現(xiàn)代碼如下:

/**
 * function Add() {
      return { type: INCREMENT }
   }
   function Minus() {
      return { type: DECREMENT }
   }
 * const actions = { Add, Minus }
 * const boundActions = bindActionCreators(actions, store.dispatch)
 */

function bindActionCreator(actionCreator, dispatch) {
  // 這里派發(fā) action,執(zhí)行 actionCreator 函數(shù)汗菜,返回一個(gè) type 作為 dispatch 的參數(shù)調(diào)用的函數(shù)
  return function(...args) {
    dispatch(actionCreator.apply(this, args))
  }
}

function bindActionCreators(actionCreators, dispatch) {
  // 如果是一個(gè)函數(shù)让禀,表示就是一個(gè) actionCreator,直接返回包裝了調(diào)用dispatch的函數(shù)
  if (actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  // 如果是一個(gè)對(duì)象呵俏,給每個(gè) actionCreator 都包裝返回調(diào)用dispatch的函數(shù)
  if (typeof actionCreators === 'object' && actionCreators !== null) {
    const bindActionCreators = {}
    for(const key in actionCreators) {
      const actionCreator = actionCreators[key]
      bindActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
    return bindActionCreators
  }
}

export default bindActionCreators

combineReducers

通常組件不會(huì)只有一個(gè) reducer堆缘,每個(gè)組件都可以有自己的 reducer 和 state,便于模塊化管理普碎。但是 redux 規(guī)定吼肥,一個(gè)應(yīng)用只能有一個(gè) store,一個(gè) reducer麻车,一個(gè) state缀皱,根據(jù)程序模塊化,分而治之的管理模式动猬,這個(gè)時(shí)候就需要合并多個(gè) reducer啤斗,多個(gè)狀態(tài)為一個(gè) reducer,一個(gè)state赁咙,combineReducers 就是做合并 reducer操作的钮莲,下面先來看看用法,然后再實(shí)現(xiàn)一個(gè)彼水。
這里只貼重要的代碼崔拥,其他細(xì)致的可以到 github 倉庫看實(shí)戰(zhàn)。
src/components/Counter5.js

import React, { Component } from 'react';
import { bindActionCreators} from '../redux';
import * as actionType from '../actionTypes'
// import { bindActionCreators } from '../redux'
import store from '../store'


const actions = {
  Add5: actionType.Add5,
  Minus5: actionType.Minus5
}
const boundActions = bindActionCreators(actions, store.dispatch)

export default class Counter5 extends Component {
  constructor(props) {
    super(props)
    this.state = { number : 5}
  }
  componentDidMount() {
    this.unsubscribe = store.subscribe(() => this.setState({ number: store.getState().Counter5.number }));
  }
  componentWillUnmount() {
    this.unsubscribe();
  }
  render() {
    return (
      <div>
        <p>{this.state.number}</p>
        <button onClick={boundActions.Add5}>+</button>
        <button onClick={boundActions.Minus5}>-</button>
        <button onClick={
          () => {
              setTimeout(() => {
                boundActions.Add5();
              }, 1000);
          }
        }>1秒后加5</button>
      </div>
    )
  }
}

新建 reducers 和 action types
src/reducers/Counter5.js凤覆,src/actionTypes

// src/reducers/Counter5.js
import * as actionType from '../actionTypes'

export let Counter5State = { number: 5 };

const reducer = (state = Counter5State, action) => {
  switch (action.type) {
    case actionType.INCREMENT5:
      return { number: state.number + 5 };
    case actionType.DECREMENT5:
      return { number: state.number - 5 };
    default:
      return state;
    }
}
export default reducer
// src/actionTypes.js
export const INCREMENT5 = 'ADD5';
export const DECREMENT5 = 'MINUS5';

export const INCREMENT6 = 'ADD6';
export const DECREMENT6 = 'MINUS6';

export function Add5() {
  return { type: INCREMENT5 }
}
export function Minus5() {
  return { type: DECREMENT5 }
}

export function Add6() {
  return { type: INCREMENT6 }
}
export function Minus6() {
  return { type: DECREMENT6 }
}

src/store.js

import { createStore } from 'redux'
import reducers from './reducers'
const store = createStore(reducers);
export default store

src/reducers/index.js

import { combineReducers } from 'redux'
import Counter5 from '../reducers/Counter5'
import Counter6 from '../reducers/Counter6'

const reducers = {
  Counter5,
  Counter6
}
const combinedReducers = combineReducers(reducers)
export default combinedReducers

其他的不貼了链瓦,效果就是組件的狀態(tài)互不干擾:


combineReducers就是將多個(gè)組件的 reducer 合并為一個(gè) reducer,內(nèi)部實(shí)現(xiàn)是過濾 reducers 對(duì)象上的函數(shù)屬性盯桦,然后返回一個(gè)combination函數(shù)慈俯,這個(gè)combination函數(shù)內(nèi)部會(huì)遍歷過濾之后的對(duì)象,解析出來每一個(gè) reducer拥峦,拿到對(duì)應(yīng)的狀態(tài)贴膘,最后再根據(jù) reducer 的 key,返回一個(gè)新的狀態(tài)對(duì)象事镣。根據(jù) reducer 解析出來的 state 是最重要的一段步鉴,const reducer = finalReducers[key]揪胃,const nextStateForKey = reducer(previousStateForKey, action)璃哟。這里面有一個(gè)優(yōu)化操作:定義一個(gè) hasChanged的標(biāo)志氛琢,新狀態(tài)和老狀態(tài)不一致就返回新狀態(tài),否則返回老的狀態(tài)随闪。
具體代碼實(shí)現(xiàn)如下阳似,跟源碼的實(shí)現(xiàn)是一致的,代碼有備注解釋:
src/redux/combineReducers

export default function combineReducers (reducers) {
  // 1. 過濾 reducers 對(duì)象上的函數(shù)屬性
  const reducerKeys = Object.keys(reducers) // ["counter1", "counter2"]
  const finalReducers = {}
  for(let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i] // "counter1"
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key] // finalReducers: {counter1: counter1, counter2: counter2}
    }
  }

  // 2. 返回一個(gè)函數(shù) 遍歷finalReducers铐伴,生成state
  const finalReducerKeys = Object.keys(finalReducers) // ['counter1', 'counter2']
  return function combination (state = {}, action) {
    let hasChanged = false // hasChanged是作性能優(yōu)化撮奏,沒有改變就返回原來的state
    const nextState = {}
    for(let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i] // "counter1"
      const reducer = finalReducers[key] // reducer counter1 函數(shù)
      const previousStateForKey = state[key] // 取出reducer對(duì)應(yīng)的state

      // 這步最重要:給reducer傳入老的state和action,返回新的state
      const nextStateForKey = reducer(previousStateForKey, action)
      // 將新的state的key 拼到總的state上,組件通過 state.counter1.number獲取
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey // 前后狀態(tài)不一樣就是改變了
    }
    hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length // reducers的key不一樣也是變了
    return hasChanged ? nextState : state
  }

以上就是 combineReducers 的實(shí)現(xiàn)当宴。

react-redux 實(shí)現(xiàn)

從上面可以 Counter5畜吊,Counter6 組件可以看出來,這兩個(gè)組件其實(shí)有很多的相似代碼户矢,比如訂閱更新玲献,獲取狀態(tài)等,這些都可以抽離出來梯浪,減少冗余代碼捌年,將 store 綁定到 context 上,至上而下的傳遞挂洛,這就是 react-redux 做的事情礼预。

先來看一下 react-redux 的應(yīng)用,實(shí)現(xiàn)一個(gè) Counter7 組件虏劲,對(duì)應(yīng)的 actions 和 reducer 和上面一樣托酸,就不贅述了。Counter7 使用 react-redux 的 connect 返回一個(gè)高階組件

src/components/Counter7.js

import React, { Component } from 'react'
import * as actionType from '../actionTypes'
import { connect } from 'react-redux'


const actions = {
  add: actionType.Add7,
  minus: actionType.Minus7
}

const mapStateToProps = (state) => {
  console.log(state)
  return {
    number: state.Counter7.number
  }
}

const mapDispatchToProps = actions

class Counter7 extends Component {
  render() {
    const { number, add, minus } = this.props
    return (
      <div>
        <p>{number}</p>
        <button onClick={add}>+</button>
        <button onClick={minus}>-</button>
        <button onClick={
          () => {
              setTimeout(() => {
                add()
              }, 1000)
          }
        }>1秒后加5</button>
      </div>
    )
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps 
)(Counter7)

src/index.js 使用 Provider 共享 store:

import React from 'react';
import ReactDOM from 'react-dom';
import Counter7 from './components/Counter7'
import store from './store'
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <Counter7 />
  </Provider>
,
  document.getElementById('root')
)

可以看到 Counter7 使用 connect 連接柒巫,mapStateToProps 將 store 上的state 拼到 props 上励堡,mapDispatchToProps 將派發(fā)函數(shù)拼接到 props
下面來看看 Provider 和 connect 實(shí)現(xiàn)吻育,根據(jù) react-redux 官方的實(shí)現(xiàn)念秧,我們也采用函數(shù)和 hooks 的方式實(shí)現(xiàn)。

Provider 是利用 react 的 context 創(chuàng)建上下文容器布疼,value 可以設(shè)置成共享的數(shù)據(jù)摊趾,provider 提供數(shù)據(jù),consumer 消費(fèi)這些數(shù)據(jù)游两。
const { Provider, Consumer } = React.createContext(defaultValue);
新建一個(gè) react-redux 文件目錄砾层,里面來寫代碼實(shí)現(xiàn):
src/react-redux/index.js

import Provider from './Provider'
import connect from './connect'

export {
  Provider,
  connect
}

src/react-redux/ReactReduxContext.js

import React from 'react'
export const ReactReduxContext = React.createContext(null)
export default ReactReduxContext

src/react-redux/Provider.js

import React from 'react'
import ReactReduxContext from './ReactReduxContext'

export default function Provider (props) {
  return (
    <ReactReduxContext.Provider value={{ store: props.store }}>
      {props.children}
    </ReactReduxContext.Provider>
  )
}

最復(fù)雜的是 connnect 的實(shí)現(xiàn):
connnect 是一個(gè)函數(shù),返回一個(gè)被包裝過后的組件贱案,里面做的事情就是將 state肛炮,dispatch 派發(fā)函數(shù)作為 props 拼到被包裝的組件上,然后在渲染的時(shí)候訂閱更新函數(shù),更新狀態(tài)侨糟。
mapStateToProps 映射 state 到 props 上const stateProps = useMemo(() => mapStateToProps(preState), [preState])

const mapStateToProps = (state) => {
  return {
    number: state.Counter7.number
  }
}

mapDispatchToProps 就是映射 dispatch 函數(shù)到 props碍扔,這里分三種情況,如果參數(shù)是對(duì)象就要做 bindActionCreators秕重,分別包裝返回具有 dispatch能力的函數(shù)對(duì)象不同,像這樣:{add: ?, minus: ?};如果參數(shù)是函數(shù)溶耘,就直接拼mapDispatchToProps(props, dispatch)二拐,否則就直接返回一個(gè)包含 dispatch 的對(duì)象;

const dispatchProps = useMemo(() => {
  let dispatchProps
  if (typeof mapDispatchToProps === 'object') {
    dispatchProps = bindActionCreators(mapDispatchToProps, dispatch)
   } else if (typeof mapDispatchToProps === 'function') {
    dispatchProps = mapDispatchToProps(props, dispatch)
    } else {
    dispatchProps = { dispatch }
    }
    return dispatchProps
}, [dispatch, props])

然后訂閱更新函數(shù)凳兵,更新狀態(tài):

const [, forceUpdate] = useReducer(x => x + 1, 0)
useLayoutEffect(() => subscribe(forceUpdate), [subscribe])

useLayoutEffect 會(huì)在 dom 更新完之后百新,瀏覽器繪制之前執(zhí)行。
最后返回包裝之后的組件:
return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />
完整的 connect 的實(shí)現(xiàn)代碼如下:

import React, { useContext, useMemo, useReducer, useLayoutEffect } from 'react'
import ReactReduxContext from './ReactReduxContext'
import { bindActionCreators } from '../redux'

export default function connect (mapStateToProps, mapDispatchToProps) {
  return function (WrappedComponent) {
    return function (props) {
      const { store } = useContext(ReactReduxContext)
      const { getState, dispatch, subscribe} = store
      const preState = getState()
      // 映射狀態(tài)到 props
      const stateProps = useMemo(() => mapStateToProps(preState), [preState])

      // 映射 dispatch 到 props
      const dispatchProps = useMemo(() => {
        let dispatchProps
        if (typeof mapDispatchToProps === 'object') {
          dispatchProps = bindActionCreators(mapDispatchToProps, dispatch)
        } else if (typeof mapDispatchToProps === 'function') {
          dispatchProps = mapDispatchToProps(props, dispatch)
        } else {
          dispatchProps = { dispatch }
        }
        return dispatchProps
      }, [dispatch, props])

      // 訂閱更新函數(shù)庐扫,更新狀態(tài)
      const [, forceUpdate] = useReducer(x => x + 1, 0)
      useLayoutEffect(() => subscribe(forceUpdate), [subscribe])

      // 返回被包裝的組件
      return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />
    }
  }
}

到目前為止饭望,redux 和 react-redux 的基本核心都實(shí)現(xiàn)了,參照的是 github 上的源碼聚蝶。還剩下一個(gè) 中間件的功能杰妓,這個(gè)準(zhǔn)備在下一章單獨(dú)去寫,分析一下時(shí)常用的幾個(gè) redux 中間件用法和實(shí)現(xiàn)碘勉,以及連接中間件的實(shí)現(xiàn)原理巷挥,到 react 生態(tài)的整合,比如 redux-saga验靡,dva倍宾,umi等。目前項(xiàng)目用到的生態(tài)就是這些胜嗓,所以想單獨(dú)開一篇專門寫這個(gè)(希望不要開天窗)高职,加深理解,溫故知新辞州。

參考:
https://github.com/reduxjs/redux
https://github.com/reduxjs/react-redux

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末怔锌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子变过,更是在濱河造成了極大的恐慌埃元,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件媚狰,死亡現(xiàn)場離奇詭異岛杀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)崭孤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門类嗤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來糊肠,“玉大人,你說我怎么就攤上這事遗锣』豕” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵黄伊,是天一觀的道長泪酱。 經(jīng)常有香客問我派殷,道長还最,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任毡惜,我火速辦了婚禮拓轻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘经伙。我一直安慰自己扶叉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布帕膜。 她就那樣靜靜地躺著枣氧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪垮刹。 梳的紋絲不亂的頭發(fā)上达吞,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音荒典,去河邊找鬼酪劫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛寺董,可吹牛的內(nèi)容都是我干的覆糟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼遮咖,長吁一口氣:“原來是場噩夢啊……” “哼滩字!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起御吞,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤麦箍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后魄藕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體内列,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年背率,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了话瞧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嫩与。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖交排,靈堂內(nèi)的尸體忽然破棺而出划滋,到底是詐尸還是另有隱情,我是刑警寧澤埃篓,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布处坪,位于F島的核電站,受9級(jí)特大地震影響架专,放射性物質(zhì)發(fā)生泄漏同窘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一部脚、第九天 我趴在偏房一處隱蔽的房頂上張望想邦。 院中可真熱鬧,春花似錦委刘、人聲如沸丧没。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呕童。三九已至,卻和暖如春淆珊,著一層夾襖步出監(jiān)牢的瞬間夺饲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國打工套蒂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钞支,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓操刀,卻偏偏與公主長得像烁挟,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子骨坑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

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