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
}
}
}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
}
}
}
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 (
counter: { counter }p>
同时修改入口文件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>
, 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>
, 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 (
counter: { counter }p>
plusCounter }>增加button> subCounter }>减少button> asyncPlusCounter }>异步增加button> 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>
, document.getElementById('root'));
再次运行,执行结果和预期一样,到此,整个redux、react-redux、中间件的简单实现已经完成。
代码地址:https://github.com/hlongc/reduxs
由于本人水平有限,如有纰漏或建议,欢迎留言。
如果觉得不错,欢迎关注海致前端公众号。
感谢你的阅读,让我们一起进步。

本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
