js实现线路流动_52期:实现redux与reactredux
來自胡龍超同學的分享:《實現redux與react-redux》
我們通過react構建我們的項目時,開發過程中總是避免不了組件之間的數據傳遞以及共享數據狀態,由于數據在組件中是單向流動的,數據自上而下從父組件流向子組件(通過props),導致多個非父子組件之間通信就相對麻煩,redux的出現就是為了解決這個痛點,官方對redux是這樣定義:Redux is a predictable state container for JavaScript apps.(Redux是一個可預測的JavaScript應用程序狀態容器),這個狀態容器能幫助我們管理應用中的狀態,但是redux可以和任何框架搭配使用,不局限于react,而react-redux是一個為react配套的工具,讓我們能在react應用中更輕松的使用它。相信大家都已經使用過這兩個庫,那我們就來實現一個最簡單的redux和react-redux庫。
1.redux
redux主要對我們暴露出一個對象,這個對象包含了我們常用的三個方法:dispatch(該方法傳入一個action用于修改狀態)、getState(獲取當前應用的狀態)、subscribe(訂閱函數,在狀態發生改變時會自動執行),現在我們知道redux導出的結果及傳入的參數,那么開始實現它:
先通過以下命令創建一個應用reduxs:
create-react-app reduxs
然后在reduxs中把項目啟動起來,刪除其他無關文件,現在目錄結構如下:
在src目錄下創建一個self目錄,用于保存自己實現的redux及react-redux,并且在self下創建redux.js,在App.js引入當前redux.js。首先在redux中暴露出一個createStore方法,該方法默認傳入一個reducer參數:
export const createStore = (reducer) => {let currenState = {} // 當前狀態
const watcher = [] // watcher用于收集訂閱的函數
function getState() {}
function subscribe(fn) {}
function dispatch(action) {}
return { getState, subscribe, dispatch }
}
我們把這個createStore返回的對象稱為store,因為getState是獲取應用當前的狀態,那么我們只需要在該方法中返回狀態即可:
function getState() {return currenState
}
此時我們能通過store.getState()獲取當前的狀態,然后我們會對應用進行修改此時會調用dispatch并傳入一個action來對state進行修改,在dispatch內部,我們要做的就是修改當前狀態:
function dispatch(action) {currenState = reducer(currenState, action)
return action
}
我們把當前的狀態currentState和修改狀態的行為action傳入到reducer中,reducer會返回給我們一個最新的state,我們再用這個state來更新當前的currentState即可。
但是我們可能會在應用狀態更新的時候進行一些操作,比如更新視圖或者打印日志之類,此時就用到了subscribe,我們這里用到了最簡單的發布訂閱,在初始化的時候進行訂閱,然后狀態觸發時進行發布:
function subscribe(fn) {if (typeof fn === 'function') {
watcher.push(fn)
} else {
console.warn('The argument must be a function')
}
}
function dispatch(action) {
currenState = reducer(currenState, action)
watcher.forEach(fn => fn())
return action
}
我們在用createStore初始化狀態時,然后用store.subscribe()來訂閱一個事件,subscribe進行一個簡單判斷,當前傳入的是否為函數,當我們dispatch以后,讓watcher中的訂閱事件依次執行。
到現在redux代碼基本完成,但是我們在調用createStore時會默認獲取一次當前的狀態,所以我們在return { getState, subscribe, dispatch }之前手動調用一次dispatch,比如dispatch({ type: '@@SELF_REDUX_INIT' }),保證這個type值特殊不被重復就行,真正redux的是使用的{ type: '@@REDUX_INIT' },此時redux.js代碼:
export const createStore = (reducer) => {let currenState = {}
const watcher = []
function getState() {
return currenState
}
function subscribe(fn) {
if (typeof fn === 'function') {
watcher.push(fn)
} else {
console.warn('The argument must be a function')
}
}
function dispatch(action) {
currenState = reducer(currenState, action)
watcher.forEach(fn => fn())
return action
}
dispatch({ type: '@@SELF_REDUX_INIT' })
return { getState, subscribe, dispatch }
}
再在App.js同級目錄下創建一個counter.js,在App.js引入該文件,counter.js中我們需要實現一個簡單的reducer以及可以返回action的函數,暫且稱之為actionCreator:
import { createStore } from './self/redux'const INIT_STATE = 0
export function counter(state = INIT_STATE, action) {
console.log(action)
switch(action.type) {
case 'plus':
return state + 1
case 'subtract':
return state - 1
default:
return INIT_STATE
}
}
export function plusCounter() {
return { type: 'plus' }
}
export function subCounter() {
return { type: 'subtract' }
}
// 創建store
const store = createStore(counter)
const listener = function() {
const state = store.getState()
console.log('counter', state)
}
store.subscribe(listener)
store.dispatch(plusCounter())
store.dispatch(plusCounter())
store.dispatch(subCounter())
store.dispatch(plusCounter())
此時看下運行效果:
2.react-redux
react-redux的出現是為了讓我們能更好的在react中使用redux,而react-redux主要向我們提供了兩個組件Provider、connect, Provider給子組件提供context,而connect的作用主要是把state和dispatch這個行為放到組件的props上面,供組件使用。
2.1 Provider
Porvider的作用主要就就是讓子元素通過this.context拿到父元素傳遞下來的屬性值,而不需要經過props一層一層的往下面傳遞。Provider接受一個store屬性,并且放到context中,可以讓子元素獲取。由于要獲取props,所以用prop-types進行類型校驗,yarn add prop-types安裝一下,在self下面新建react-redux.js,對外暴露出一個Provider組件:
import React from 'react'import PropTypes from 'prop-types'
export class Provider extends React.Component {
// 類型校驗
static childContextTypes = {
store: PropTypes.object
}
// 獲取context時會調用這個方法
getChildContext() {
return { store: this.store }
}
constructor(props, context) {
super(props, context)
this.store = props.store
}
// 這里直接渲染子元素
render() {
return this.props.children
}
}
2.2 connect
connect是一個高階函數,接收一個組件,并返回一個組件,調用時是這樣的:App = connect(mapStateToProps, mapDispatchToProps)(App),第一次傳入參數時,現在只考慮最簡單的只傳入兩個參數的情況,第一個參數是要把state映射到props上的對象集合,第二個參數為派發action這個行為映射到props上,組件內部通過this.props來調用,在第二次傳參時,傳入了一個組件App,然后對這個組件的屬性進行一些修改然后原樣返回。
export const connect = (mapStateToProps = state => state, mapDispatachToProps = {}) => WrapComponent => {return class ConnectComponent extends React.Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
constructor(props, context) {
super(props, context)
this.state = {
props: {}
}
}
render() {
return <WrapComponent { ...this.state.props } />
}
}
}
props中存放要放在組件中的mapStateToProps和mapDispatachToProps,之前在Provider組件中已經在context中放入了store,所以在connect中我們可以取出來使用,并且也進行類型校驗,下一步先把state中需要取出的值放入props,新加一個update方法來更新當前的this.state.props,并且在componentDidMount中調用:
componentDidMount() {this.update()
}
update() {
const { getState } = this.context.store
const stateProps = mapStateToProps(getState())
this.setState({
props: {
...this.state.props,
...stateProps
}
})
}
因為mapStateToProp是一個函數,傳入當前的state然后返回一個對象,所以這一步實現很簡單,把返回的對象也就是stateProps放入到this.state.props中與原來的state.props進行merge。至此實現了把應用中的狀態放到了當前組件的props中。
下一步就是要實現mapDispatchToProps,原來在使用connect組件時就有過這個疑問,為什么調用一下actionCreator就能直接修改狀態,后來知道了react-redux在內部手動幫我們調用了一次dispatch,所以修改了狀態,這也是connect組件的精華,這部分主要修改update方法實現如下:
import React from 'react'import PropTypes from 'prop-types'
// 依次調用dispatch
function bindActionCreator(creator, dispatch) {
return (...args) => dispatch(creator(...args))
}
// 這里返回一個對象,key保持不變,而value是包含了dispatch的函數
function bindActionCreators(creators, dispatch) {
return Object.keys(creators).reduce((sum, cur) => {
sum[cur] = bindActionCreator(creators[cur], dispatch)
return sum
}, {})
}
export const connect = (mapStateToProps = state => state, mapDispatachToProps = {}) => WrapComponent => {
return class ConnectComponent extends React.Component {
// ...省略未改變代碼
update() {
const { getState, dispatch } = this.context.store
const stateProps = mapStateToProps(getState())
// 這里將actionCreator遍歷依次調用dispatch
const dispatchProps = bindActionCreators(mapDispatachToProps, dispatch)
this.setState({
props: {
...this.state.props,
...stateProps,
...dispatchProps
}
})
}
// 省略render...
}
}
至此已經實現了將state和dispatch(不包括異步)放到組件的props中,但是在數據更新以后,并不會刷新視圖,這個時候就用到了redux中的subscribe,我們可以在這里訂閱一下更新事件,每當狀態發生改變,就重新渲染組件,在這里修改componentDidMount方法,react-redux最終代碼:
import React from 'react'import PropTypes from 'prop-types'
function bindActionCreator(creator, dispatch) {
return (...args) => dispatch(creator(...args))
}
function bindActionCreators(creators, dispatch) {
return Object.keys(creators).reduce((sum, cur) => {
sum[cur] = bindActionCreator(creators[cur], dispatch)
return sum
}, {})
}
export const connect = (mapStateToProps = state => state, mapDispatachToProps = {}) => WrapComponent => {
return class ConnectComponent extends React.Component {
static contextTypes = {
store: PropTypes.object.isRequired
}
constructor(props, context) {
super(props, context)
this.state = {
props: {}
}
}
componentDidMount() {
const { store } = this.context
// 此處訂閱更新組件事件
store.subscribe(() => this.update())
this.update()
}
update() {
const { getState, dispatch } = this.context.store
const stateProps = mapStateToProps(getState())
const dispatchProps = bindActionCreators(mapDispatachToProps, dispatch)
this.setState({
props: {
...this.state.props,
...stateProps,
...dispatchProps
}
})
}
render() {
return <WrapComponent { ...this.state.props } />
}
}
}
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
}
}
現在來測試一下功能是否正常,修改我們的App.js:
import React from 'react';import { connect } from './self/react-redux'
import { plusCounter, subCounter } from './counter'
class App extends React.Component {
render() {
const { counter, plusCounter, subCounter } = this.props
return (
<div><p>counter: { counter }p><p><button onClick={ plusCounter }>增加button><button onClick={ subCounter }>減少button>p>div>
)
}
}
App = connect(
state => ({ counter: state }),
{ plusCounter, subCounter }
)(App)
export default App;
同時修改入口文件index.js,引入我們自己寫的redux及react-redux:
import React from 'react';import ReactDOM from 'react-dom';
import { createStore } from './self/redux';
import { Provider } from './self/react-redux';
import App from './App';
import { counter } from './counter';
const store = createStore(counter)
ReactDOM.render(
<Provider store={store}><App />Provider>
, document.getElementById('root'));
運行結果正常:
counter.js中打印三次,第一次是初始化,后面兩次分別為加減。
3. 中間件
之前只實現了同步dispatch的情況,應用中包含了http請求、setTimeout、Promise等異步操作,此時就要使用中間件來實現異步dispatch。
3.1 applyMiddleware,compose
為了實現中間件,我們需要給createStore傳入第二個參數heightener,第二個參數的作用就是讓中間件先執行以后再傳入createStore,達到強化的作用,修改redux.js中createStore:
export const createStore = (reducer, heightener) => {// 如果存在heightener則返回包裝后的結果
if (heightener) {
return heightener(createStore)(reducer)
}
let currenState = {}
const watcher = []
function getState() { /* ... */ }
function subscribe(fn) { /* ... */ }
function dispatch(action) { /* ... */ }
dispatch({ type: '@@SELF_REDUX_INIT' })
return { getState, subscribe, dispatch }
}
然后為了應用我們的中間件,需要實現applyMiddleware函數,也在redux內部實現,這個函數也是兩層返回函數,第一次接收createStore,第二次接收reducer:
// 傳入的可能不止一個中間件export function applyMiddleware(...middlewares) {
return createStore => reducer => {
const store = createStore(reducer)
let { getState, dispatch } = store
// 在使用redux-thunk時,傳入了兩個參數{ dispatch, getState }
const params = {
getState: getState,
dispatch: (...args) => dispatch(...args)
}
// 遍歷每個中間件傳入{ dispatch, getState },依次調用
const middlewareArr = middlewares.map(middleware => middleware(params))
// 把多個中間件合并成一個執行
dispatch = compose(...middlewareArr)(dispatch)
return { ...store, dispatch }
}
}
export function compose(...fns) {
if (fns.length === 0) return arg => arg
if (fns.length === 1) return fns[0]
return fns.reduce((res, cur) => (...args) => res(cur(...args)))
}
3.2 redux-thunk,array-thunk
實現了applyMiddleware和compose函數以后,再實現一下中間件本身,整個流程就結束了,中間件執行也就是,如果滿足某個條件,那么執行一個動作,否則繼續調用下一個中間件,在self下面新建redux-thunk.js,這個中間件可以幫我們實現異步dispatch功能:
const thunk = ({ dispatch, getState }) => next => action => {// 普通的action就是一個對象{ type: 'plus' },如果是function,那么就是一個異步操作
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
export default thunk
此時在counter.js可以加入異步dispatch代碼:
export function asyncPlusCounter() {return dispatch => {
setTimeout(() => {
dispatch(plusCounter())
}, 2000)
}
}
并且修改App.js和入口文件index.js,此處只展示入口文件變動代碼:
import React from 'react';import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from './self/redux';
import { Provider } from './self/react-redux';
import thunk from './self/redux-thunk'
import App from './App';
import { counter } from './counter';
const store = createStore(counter, applyMiddleware(thunk))
ReactDOM.render(
<Provider store={store}><App />Provider>
, document.getElementById('root'));
結果如下,現在同步和異步dispatch都已經實現:
最后再實現一個可以dispatch數組的功能,之前dispatch都是一個對象,比如dispatch({ type: 'plus' }),現在來實現一個dispatch([{ type: 'plus' }, plusCounter(), asyncPlusCounter()]),與thunk一樣,只需要判斷當前傳入的是否為數組,如果是依次執行并return,否則繼續調用下一個中間件,在self目錄下創建array-thunk.js:
const arrayThunk = ({ dispatch, getState }) => next => action => {// 普通的action就是一個對象{ type: 'ADD' },如果是數組,那么依次執行并且返回
if (Array.isArray(action)) {
return action.forEach(act => dispatch(act))
}
return next(action)
}
export default arrayThunk
同樣修改couter.js、App.js和入口文件index.js。
counter.js:
export function plusMultiple() {return [{ type: 'plus' }, plusCounter(), asyncPlusCounter()]
}
App.js:
import React from 'react';import { connect } from './self/react-redux'
import { plusCounter, subCounter, asyncPlusCounter, plusMultiple } from './counter'
class App extends React.Component {
render() {
const { counter, plusCounter, subCounter, asyncPlusCounter, plusMultiple } = this.props
return (
<div><p>counter: { counter }p><p><button onClick={ plusCounter }>增加button><button onClick={ subCounter }>減少button><button onClick={ asyncPlusCounter }>異步增加button><button onClick={ plusMultiple }>數組式增加button>p>div>
)
}
}
App = connect(
state => ({ counter: state }),
{ plusCounter, subCounter, asyncPlusCounter, plusMultiple }
)(App)
export default App;
index.js:
import React from 'react';import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from './self/redux';
import { Provider } from './self/react-redux';
import thunk from './self/redux-thunk';
import arrayThunk from './self/array-thunk';
import App from './App';
import { counter } from './counter';
const store = createStore(counter, applyMiddleware(thunk, arrayThunk))
ReactDOM.render(
<Provider store={store}><App />Provider>
, document.getElementById('root'));
再次運行,執行結果和預期一樣,到此,整個redux、react-redux、中間件的簡單實現已經完成。
代碼地址:https://github.com/hlongc/reduxs
由于本人水平有限,如有紕漏或建議,歡迎留言。
如果覺得不錯,歡迎關注海致前端公眾號。
感謝你的閱讀,讓我們一起進步。? ?
總結
以上是生活随笔為你收集整理的js实现线路流动_52期:实现redux与reactredux的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 成都吉宝沁风御庭楼盘地址在哪里?
- 下一篇: 瓷砖上边刷粘合剂在刷涂料可以吗?