Redux 构造
有想过本人去实现一个 Redux
吗?其实并不难,Redux
次要就是由 store
,reducer
,action
组成的,接下来咱们一步一步来尝试的搭建Redux
。
Redux 的分步实现
reducer
依据下面图外面的介绍咱们晓得 reducer
就是依据传来的 type
,对相干state
进行解决,之后返回一个新的state
。由此咱们失去以下代码:
// reducer.js
const init = {num: 0}
export const reducer = (state = init, action) => {switch(action.type) {
case 'add':
return {
...state,
num: state.num + 1
}
case 'low':
return {
...state,
num: state.num - 1
}
default:
return init
}
}
store
咱们实现下面图里的第一步,就是 store.js
文件。咱们首先须要明确 store
文件次要有三个重要的函数,别离是subscribe
,dispatch
,getState
。接下来间接贴上代码来剖析吧。
// store.js
import {reducer} from './reducer.js'
export const createStore = () => {let currentState = {}
let collect = []
dispatch({})
function getState() {return currentState}
function dispatch(action) {currentState = reducer(currentState, action)
collect.forEach(tempFunc => tempFunc())
}
function subscribe(tempFunc) {if (fn instanceof Function) {collect.push(tempFunc)
}
return
}
return {getState, dispatch, subscribe}
}
咱们能够看到 createStore
函数中除了三个根本函数之外有一行 dispatch({})
这个其实就是为了初始化redux
,如果不触发reducer
外面的初始化的话,如果对相干值进行 加 、 减 操作就会失去一个 NaN
的值。
而后 subscribe
函数次要就是依据观察者模式实现的,当用户在页面订阅 subscribe
函数,接着在进行 dispatch
操作之后就会触发以后页面所有订阅 subscribe
的函数。这么讲很麻烦,上代码吧。
// index.js
import React from 'react'
import {createStore} from '../../store/store'
import {reducer} from '../../store/reducer'
const store = createStore(reducer)
export class Roll extends React.Component {constructor(props) {super(props)
this.state = {num:0}
}
componentWillMount() {store.subscribe(()=>this.setState({num: store.getState().num
}))
}
lowNum() {store.dispatch({ type: 'low'})
console.log('store 外面的值为' + store.getState().num)
}
addNum() {store.dispatch({ type: 'add'})
console.log('store 外面的值为' + store.getState().num)
}
render() {
return (<div style={{ textAlign:'center', paddingTop:'100px'}}>
<button onClick={()=> this.lowNum()}>low</button>
<div style={{display: 'inline', padding:'0 10px'}}>{this.state.num}</div>
<button onClick={()=> this.addNum()}>add</button>
</div>
)
}
}
加上了 subscribe
函数的效果图:
没加 subscribe
函数的效果图:
没加的话理论就是更新了 store
外面的状态,然而 store
的状态未同步到页面来,从而无奈触发页面的更新。
react-redux 的实现
咱们个别是在 react 我的项目里并不会间接去应用 redux,而是利用 react-redux
作为沟通两者的桥梁。
例子
首先咱们看看 react-redux
的简略应用形式。
// Provider 伪代码
ReactDOM.render(<Provider store={store}>
<ChildComponent />
</Provider>
)
//connent 伪代码
ChildComponent = connect(mapStateToProps, mapDispatchToProps)(ChildComponent)
Provider
Provider
等同于一个容器组件,容器外部能够嵌套多层组件,实际上 Provider
不会对外面组件做任何解决,只须要让组件失常显示,它承受一个 store
参数,它会把这个外界传来的 store
参数传入到 context
中,而后让这个组件成为组件树的根节点,那么它的子组件都能够获取到 context
了。
// provider.js
import React from 'react'
import PropTypes from 'prop-types'
export class Provider extends React.Component {
// 申明 Context 对象属性
static childContextTypes = {
store: PropTypes.object,
children: PropTypes.object
}
// 返回 Context 对象中的属性
getChildContext = () => {
return {store: this.props.store}
}
render () {
return (<div>{this.props.children}</div>
)
}
}
Connect
connect
函数实际上接管了一个组件作为参数,最初返回一个新的组件,也就是咱们常说的 HOC
(高阶组件),它除了接管到一个组件外还接管两个参数,一个是mapStateToProps
,还有一个是mapDispatchToProps
,这些是传入该组件的props
, 须要由connect
这个高阶组件原样传回原组件。咱们大略理解流程了能够简略实现一下:
import React from 'react'
import PropTypes from 'prop-types'
export function connect(mapStateToProps, mapDispatchToProps) {
// 1. 传入 state 和 dispatch 对象
return function(WrappedCompment) {
// 2. 接管传入的组件
class Connect extends React.Component {constructor() {super()
this.state = {
// 3. 将所有的 props 整合在一个对象上,不便书写
mapStateAndDispatchProps:{}}
}
static contextTypes = {
// 4. 获取 context 里的 store
store: PropTypes.object
}
componentDidMount() {const { store} = this.context
// 5. 用于更新和合并几个传入对象
this.mergeAndUpdateProps()
store.subscribe(()=> {this.mergeAndUpdateProps()
})
}
mergeAndUpdateProps() {const { store} = this.context
let tempState = mapStateToProps ? mapStateToProps(store.getState(), this.props) : {}
let tempDispatch = mapDispatchToProps ? mapDispatchToProps(store.dispatch, this.props) : {}
this.setState({
mapStateAndDispatchProps : {
...tempState,
...tempDispatch,
...this.props
}
})
}
render() {
// 将所有传入的 props 放入之前的组件中
return <WrappedCompment {...this.state.mapStateAndDispatchProps}/>
}
}
// 返回新组件
return Connect
}
}
实现成果
接入到 Roll 组件测试一下:
// Roll.js
import React from 'react'
import {connect} from '../../store/connect'
const mapStateToProps = state => {
return {num: state.num}
}
const mapDispatchToProps = dispatch => {
return {addNum: () => {dispatch({type: 'add'})
},
lowNum: () => {dispatch({type: 'low'})
}
}
}
class Roll extends React.Component {constructor(props) {super(props)
}
render() {
return (<div style={{ textAlign:'center', paddingTop:'100px'}}>
<button onClick={()=> this.props.lowNum()}>low</button>
<div style={{display: 'inline', padding:'0 10px'}}>{this.props.num}</div>
<button onClick={()=> this.props.addNum()}>add</button>
</div>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Roll)
最终后果:
redux 中间件(middleware)
大家可能都用过 redux
的一些中间件,比方 redux-thunk
,redux-saga
,redux-logger
等等,然而这些中间件是怎么实现的呢?咱们一一道来。
首先为什么须要中间件呢?假如当初有一个场景,咱们须要打印咱们每次 dispatch
的记录,那很简略能想到就是在执行 dispatch
后打印即可:
function dispatchAndPrint(store, dispatch) {dispatch({type: 'add'})
console.log('newState:', store.getState())
}
然而当初又来了一个需要须要持续咱们捕捉 dispatch 时的谬误,那咱们须要怎么写呢:
function dispatchAndCatch(store, dispatch) {
try {dispatch({type: 'add'})
} catch(e) {console.error('dispatch error:', err)
throw e
}
}
那如果当这些需要越来越多,咱们实际上也会写越来越多的 dispatch,实际上咱们能够把这一步 dispatch 提取进去:
let next = store.dispatch
store.dispatch = function dispatchAndPrint(store) {next({type: 'add'})
console.log('newState:', store.getState())
}
store.dispatch = function dispatchAndCatch(store, dispatch) {
try {next({type: 'add'})
} catch(e) {console.error('dispatch error:', err)
throw e
}
}
applyMiddleware
咱们在 redux
中应用中间件的时候,都会用到 applyMiddleware
,applyMiddleware
实际上和下面咱们写的例子的性能是差不多的,你能够了解成 applyMiddleware
先去获取一个 dispatch
,而后在中间件中批改dispatch
,具体 dispatch 会被革新成什么样取决于咱们的中间件。对此咱们能够实现一个简略版的applyMiddleware
函数。
const applyMiddleware = function(store, middleware){
let next = store.dispatch;
store.dispatch = middleware(store)(next);
}
applyMiddleware(dispatchAndPrint)
多个中间件的链式调用
过后实际上咱们应用 applyMiddleware
的时候必定不是说每次只能应用一个中间件,那如果应用多个中间件该怎么实现呢?
咱们能够将前一个中间件返回的 dispatch
,作为下一个中间件的next 函数
传入,对此咱们能够将两个函数进行 柯里化
:
const dispatchAndPrint = store => next => action => {console.log('newState:', store.getState())
return next(action)
}
const dispatchAndCatch = store => next => action => {
try {next(action)
} catch(e) {console.error('dispatch error:', err)
throw e
}
}
编写 applyMiddleware:
function applyMiddleware(store, middlewares) {
// 浅拷贝,避免前面 reverse 影响到原 middleware
middlewares = middlewares.slice()
// 最后面放入的中间件应该在后面执行,此处若不翻转数组,最先放入的函数将会在最里层会导致最初才执行
middlewares.reverse()
let dispatch = store.dispatch
middlewares.map((middleware) => {dispatch = middleware(store)(dispatch)
})
return {...store, dispatch}
}
这边咱们解释一下 applyMiddleware
这个函数,实际上 middlewares
是一个中间件的数组,咱们对 middlewares
数组做反转解决是因为每次咱们的中间件函数只是返回了一个新的 dispatch
函数给下一个中间件,而咱们最终拿到的是最初这个包装 dispatch
的中间件返回的函数,若反转的话则最初这个中间件会先执行而后一直向前推能力执行到第一个中间件。
走进 applyMiddleware 源码
当然咱们看 applyMiddleware
的源码的话并不是像咱们一样间接反转中间件数组,而是上面这种写法:
function applyMiddleware(...middlewares) {return (createStore) => (reducer, preloadedState, enhancer) => {var store = createStore(reducer, preloadedState, enhancer);
var dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {...store, dispatch}
}
}
compose 函数
的实现:
function compose(...funcs) {if (funcs.length === 0) {return arg}
if (funcs.length === 1) {
// 只须要执行一个函数,把函数执行,把其后果返回即可
return funcs[0]
}
// 多个函数执行时,利用 reduce 去递归解决这些函数
return funcs.reduce((a, b) => (...args: any) => a(b(...args)))
咱们能够看到 applyMiddleware
的源码中实际上通过 compose 函数
去实现将上一个中间件的返回值传递下一个中间件作为参数,从而实现中间件串联的成果。
如果中间件程序是 a,b,c
则compose 函数
组合后后果是c(b(a(...args)))
, 执行程序为a->b->c
。
总结
兴许前面你看到 redux-thunk
的源码的时候我可能会觉着这个库为什么这么简略就这么几行代码,然而其实没必要诧异,因为就算是 redux
也不是很简单,然而背地蕴含的 JS 编程思维却值得去学习,比方函数的 柯里化
, 函数式编程
, 装璜器
等等常识。
材料:
8k 字 | Redux/react-redux/redux 中间件设计实现分析
redux 中间件的原理
Redux 入门教程(二):中间件与异步操作
JavaScript 函数柯里化
代码组合(compose)