深入理解redux原理,從零開始實現一個簡單的redux(包括react-redux, redux-thunk)

Promise212發表於2017-11-30

目錄

前言

Redux作為React的狀態管理工具, 在開發大型應用時已不可缺少, 為了更深入的瞭解Redux的整個實現機制, 決定從頭開始, 實現實現一個具有基礎功能的Redux

專案地址 歡迎star/fork

預覽

初始化專案

1.全域性安裝腳手架
npm install -g create-react-app複製程式碼
2.建立專案
create-react-app mini-redux複製程式碼
3.專案目錄
mini-react
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   └── favicon.ico
│   └── index.html
│   └── manifest.json
└── src
    └── App.css
    └── App.js
    └── App.test.js
    └── index.css
    └── index.js
    └── logo.svg
    └── registerServiceWorker.js複製程式碼

實現Redux基礎功能

1.實現Redux

新建~/src/mini-redux/mini-redux.js, redux會對外暴露一個createStore的方法,接受reducer作為引數

export function createStore(reducer) {
  let currentState = {}
  let currentListeners = []

  function getState() {
    return currentState
  }
  function subscribe(listener) {
    currentListeners.push(listener)
  }
  function dispatch(action) {
    currentState = reducer(currentState, action)
    currentListeners.forEach(v => v())
    return action
  }
  dispatch({type: '@REACT_FIRST_ACTION'})  //初始化state
  return { getState, subscribe, dispatch}
}複製程式碼

以上, 我們就已經實現了redux的基礎功能, 下面來呼叫我們實現的mini-redux, 檢驗是否達到預期. 新建~/src/index.redux.js

import { createStore } from './mini-redux/mini-redux'

const ADD = 'ADD'
const REMOVE = 'REMOVE'

// reducer
export function counter(state=0, action) {
  switch (action.type) {
    case ADD:
        return state + 1
    case REMOVE:
        return state - 1
    default:
        return 10
  }
}

export function add() {
  return {type: 'ADD'}
}
export function remove() {
  return {type: 'REMOVE'}
}

const store = createStore(counter)
const init = store.getState()
console.log(`開始數值:${init}`)

function listener(){
  const current = store.getState()
  console.log(`現在數值:${current}`)
}
// 訂閱,每次state修改,都會執行listener
store.subscribe(listener)
// 提交狀態變更的申請
store.dispatch({ type: 'ADD' })
store.dispatch({ type: 'ADD' })
store.dispatch({ type: 'REMOVE' })
store.dispatch({ type: 'REMOVE' })複製程式碼

index.js中引入以上檔案以執行, 檢視控制檯,可以看到如下log資訊

開始數值:10       index.redux.js:27
現在數值:11       index.redux.js:31 
現在數值:12       index.redux.js:31 
現在數值:11       index.redux.js:31 
現在數值:10       index.redux.js:31複製程式碼

至此,我們已經實現了redux的功能, 但是離我們的預期還差的很遠, 因為我們需要結合react來使用

2.結合React使用

下面將mini-reactreact元件結合使用, 修改index.redux.js如下

const ADD = 'ADD'
const REMOVE = 'REMOVE'

// reducer
export function counter(state=0, action) {
  switch (action.type) {
    case ADD:
        return state + 1
    case REMOVE:
        return state - 1
    default:
        return 10
  }
}

export function add() {
  return {type: 'ADD'}
}
export function remove() {
  return {type: 'REMOVE'}
}複製程式碼

index.js檔案初始化redux

import { createStore } from './mini-redux/mini-redux'
import { counter } from './index.redux'

// 初始化redux
const store = createStore(counter)

function render() {
  ReactDOM.render(<App store={store} />, document.getElementById('root'));
}
render()
// 每次修改狀態,從新渲染頁面
store.subscribe(render)複製程式碼

App.js檔案中我們就可以呼叫redux

import {add, remove} from './index.redux'

class App extends Component {
    render() {
        const store = this.props.store
        // 獲取當前值
        const num = store.getState()
        return (
            <div className="App">
                <p>初始值為{num}</p>
                <button onClick={() => store.dispatch(add())}>Add</button>
                <button onClick={() => store.dispatch(remove())}>Remove</button>
            </div>
        );
    }
}

export default App;複製程式碼

如上圖, 我們就可以在React元件中修改mini-redux的狀態了

實現React-Redux

上面我們已經,實現了Redux的功能,並且且可以和React結合使用了, 但是這種與React的連結的方式非常繁瑣,高度耦合, 在日常開發中不會這樣用, 我們會使用 react-redux庫來連線React(如果不瞭解react-redux可以看看這篇部落格), 下面我們就來實現一個簡易的react-redux

1.context

實現react-redux前, 我們要了解一下reactcontext(不瞭解可以檢視文件), react-redux的實現就利用了context機制. 下面通過一個例子,瞭解context的用法.

新建~/src/mini-redux/context.test.js

import React from 'react'
import PropTypes from 'prop-types'
// context是全域性的, 元件裡宣告, 所有子元素可以直接獲取

class Sidebar extends React.Component {
  render(){
    return (
      <div>
        <p>Sidebar</p>
        <Navbar></Navbar>
      </div>
    )
  }
}

class Navbar extends React.Component {
  // 限制型別, 必須
  static contextTypes = {
    user: PropTypes.string
  }
  render() {
    console.log(this.context)
    return (
      <div>{this.context.user} Navbar</div>
    )
  }
}


class Page extends React.Component {
  // 限制型別, 必須
  static childContextTypes = {
    user: PropTypes.string
  }
  constructor(props){
    super(props)
    this.state = {user: 'Jack'}
  }
  getChildContext() {
    return this.state
  }
  render() {
    return (
      <div>
        <p>我是{this.state.user}</p>
        <Sidebar/>
      </div>
    )
  }
}

export default Page複製程式碼

2.react-readux

react-redux中有兩個是我們常用的元件, 分別是connectProvider, connect用於元件獲取redux裡面的資料(stateaction), Provider用於把store置於context, 讓所有的子元素可以獲取到store, 下面分別實現connectprovider

實現Provider

新建~/src/mini-redux/mini-react-redux, 程式碼如下

import React from 'react'
import PropTypes from 'prop-types'


// 把store放到context裡, 所有的子元素可以直接取到store
export class Provider extends React.Component{
  // 限制資料型別
    static childContextTypes = {
    store: PropTypes.object
  }
  getChildContext(){
    return { store:this.store }
  }
  constructor(props, context){
    super(props, context)
    this.store = props.store
  }
  render(){
    // 返回所有子元素
    return this.props.children
  }
}複製程式碼

下面驗證Provider是否能實現預期功能, 修改~/src/index.js檔案如下

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

import { createStore } from './mini-redux/mini-redux'
import { Provider } from './mini-redux/mini-react-redux'
import { counter } from './index.redux'

const store = createStore(counter)

ReactDOM.render(
  (<Provider store={store}><App/></Provider>), 
  document.getElementById('root')
)複製程式碼

最後我們還要修改~/src/App.js檔案中獲取store資料的方式, 改成使用connect獲取, 但是因為還沒有實現connect, 所有我們暫使用原react-reduxconnect元件驗證上面實現的Provider

import React, { Component } from 'react';
import { connect } from 'react-redux'
import {add, remove} from './index.redux'

class App extends Component {
    render() {
        return (
            <div className="App">
                <p>初始值為{this.props.num}</p>
                <button onClick={this.props.add}>Add</button>
                <button onClick={this.props.remove}>Remove</button>
            </div>
        );
    }
}

App = connect(state => ({num: state}), {add, remove})(App)

export default App;複製程式碼

驗證結果, 上面實現的Provider成功對接connect

實現connect

上面我們實現了Provider, 但是connect仍然用的是原版react-reduxconnect, 下面就來在~/src/mini-redux/mini-react-redux.js檔案中新增一個connect方法

import React from 'react'
import PropTypes from 'prop-types'
import {bindActionCreators} from './mini-redux'

// connect 負責連結元件,給到redux裡的資料放到元件的屬性裡
// 1. 負責接受一個元件,把state裡的一些資料放進去,返回一個元件
// 2. 資料變化的時候,能夠通知元件

export const connect = (mapStateToProps = state=>state, mapDispatchToProps = {}) => (WrapComponent) => {
  return class ConnectComponent extends React.Component{
    static contextTypes = {
      store:PropTypes.object
    }
    constructor(props, context){
      super(props, context)
      this.state = {
        props:{}
      }
    }
    componentDidMount(){
      const {store} = this.context
      store.subscribe(()=>this.update())
      this.update()
    }
    update(){
      // 獲取mapStateToProps和mapDispatchToProps 放入this.props裡
      const {store} = this.context
      const stateProps = mapStateToProps(store.getState())
      // 方法不能直接給,因為需要dispatch
      const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch)
      this.setState({
        props:{
          ...this.state.props,
          ...stateProps,
          ...dispatchProps  
        }
      })
    }
    render(){
      return <WrapComponent {...this.state.props}></WrapComponent>
    }
  }
}複製程式碼

在上面程式碼中, 我們還需要在mini-redux.js中新增一個bindActionCreators方法, 用於使用dispatch包裹包裹actionCreator方法, 程式碼如下

......
function bindActionCreator(creator, dispatch){
  return (...args) => dispatch(creator(...args))
}
export function bindActionCreators(creators,dispatch){
  let bound = {}
  Object.keys(creators).forEach(v=>{
    let creator = creators[v]
    bound[v] = bindActionCreator(creator, dispatch)
  })
  return bound
}
......複製程式碼

最後我們將~/src/App.js中的connect換成上面完成的connect, 完成測試

import { connect } from './mini-redux/mini-react-redux'複製程式碼

實現redux中介軟體機制

實現applyMiddleware

在平常使用redux時, 我們會利用各種中介軟體來擴充套件redux功能, 比如使用redux-thunk實現非同步提交action, 現在來給我們的mini-redux新增中介軟體機制

修改~/src/mini-redux/mini-redux.js程式碼如下

export function createStore(reducer, enhancer) {
  if (enhancer) {
    return enhancer(createStore)(reducer)
  }

  let currentState = {}
  let currentListeners = []

  function getState() {
    return currentState
  }
  function subscribe(listener) {
    currentListeners.push(listener)
  }
  function dispatch(action) {
    currentState = reducer(currentState, action)
    currentListeners.forEach(v => v())
    return action
  }
  //初始化state
  dispatch({type: '@REACT_FIRST_ACTION'})
  return { getState, subscribe, dispatch}
}

export function applyMiddleware(middleware) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = store.dispatch

    const midApi = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    dispatch = middleware(midApi)(store.dispatch)

    return {
      ...store,
      dispatch
    }

  } 
}
......複製程式碼

以上我們就給mini-redux新增了中介軟體機制了, 下面我們就來使用中介軟體, 進行驗證. 由於我們開沒有自己的中介軟體, 現在使用redux-thunk來實現一個非同步提交action

修改~/src/index.js

......
import { createStore, applyMiddleware } from './mini-redux/mini-redux'
import thunk from 'redux-thunk'

const store = createStore(counter, applyMiddleware(thunk))
......複製程式碼

修改~/src/index.redux.js, 新增一個非同步方法

export function addAsync() {
    return dispatch => {
    setTimeout(() => {
        dispatch(add());
    }, 2000);
  };
}複製程式碼

最後我們要~/src/App.js中引入這個非同步方法, 修改如下

......
import React, { Component } from 'react';
import { connect } from './mini-redux/mini-react-redux'
import {add, remove, addAsync} from './index.redux'

class App extends Component {
    render() {
        return (
            <div className="App">
                <p>初始值為{this.props.num}</p>
                <button onClick={this.props.add}>Add</button>
                <button onClick={this.props.remove}>Remove</button>
                <button onClick={this.props.addAsync}>AddAsync</button>
            </div>
        );
    }
}

App = connect(state => ({num: state}), {add, remove, addAsync})(App)
export default App;複製程式碼

然後就可以驗證啦

實現redux中介軟體

上面我們使用了redux-thunk中介軟體, 為何不自己寫一個呢

新建~/src/mini-redux/mini-redux-thunk.js

const thunk = ({dispatch, getState}) => next => action => {
  // 如果是函式,執行一下,引數是dispatch和getState
  if (typeof action=='function') {
    return action(dispatch,getState)
  }
  // 預設,什麼都沒幹,
  return next(action)
}
export default thunk複製程式碼

~/src/index.js中的thunk換成上面實現的thunk, 看看程式是否還能正確執行

在上面的基礎上, 我們再實現一個arrThunk中介軟體, 中介軟體提供提交一個action陣列的功能

新建~/src/mini-redux/mini-redux-arrayThunk.js

const arrayThunk = ({dispatch,getState})=>next=>action=>{
  if (Array.isArray(action)) {
    return action.forEach(v=>dispatch(v))
  }
  return next(action)
}
export default arrayThunk複製程式碼

新增多箇中介軟體處理

上面我們實現的中介軟體機制,只允許新增一箇中介軟體, 這不能滿足我們日常開發的需要

修改~/src/mini-redux/mini-redux.js檔案

......
// 接收中介軟體
export function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = store.dispatch

    const midApi = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const middlewareChain = middlewares.map(middleware=>middleware(midApi))
    dispatch = compose(...middlewareChain)(store.dispatch)

    return {
      ...store,
      dispatch
    }

  } 
}
// compose(fn1,fn2,fn3)  ==> fn1(fn2(fn3))
export function compose(...funcs){
  if (funcs.length==0) {
    return arg=>arg
  }
  if (funcs.length==1) {
    return funcs[0]
  }
  return funcs.reduce((ret,item)=> (...args)=>ret(item(...args)))
}
......複製程式碼

最後我們將之前實現的兩個中介軟體thunk,arrThunk同時使用, 看看上面實現的多中介軟體合併是否完成

修改~/src/index.js

...
import arrThunk from './mini-redux/mini-redux-arrThunk'
const store = createStore(counter, applyMiddleware(thunk, arrThunk))
...複製程式碼

~/src/index.redux.js中新增一個addTwice action生成器

...
export function addTwice() {
  return [{type: 'ADD'}, {type: 'ADD'}]
}
...複製程式碼

~/src/App.js中增加一個addTwice的按鈕, 修改相應程式碼

import {add, remove, addAsync, addTwice} from './index.redux'

class App extends Component {
    render() {
        return (
            <div className="App">
                <p>now num is {this.props.num}</p>
                <button onClick={this.props.add}>Add</button>
                <button onClick={this.props.remove}>Remove</button>
                <button onClick={this.props.addAsync}>AddAsync</button>
                <button onClick={this.props.addTwice}>addTwice</button>
            </div>
        );
    }
}

App = connect(state => ({num: state}), {add, remove, addAsync, addTwice})(App)複製程式碼

大功告成!

相關文章