目錄
前言
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-react
和react
元件結合使用, 修改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
前, 我們要了解一下react
的 context
(不瞭解可以檢視文件), 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
中有兩個是我們常用的元件, 分別是connect
和Provider
, connect
用於元件獲取redux
裡面的資料(state
和action
), Provider
用於把store
置於context
, 讓所有的子元素可以獲取到store
, 下面分別實現connect
和provider
實現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-redux
的connect
元件驗證上面實現的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-redux
的connect
, 下面就來在~/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)複製程式碼
大功告成!