Redux构造

有想过本人去实现一个Redux吗?其实并不难,Redux次要就是由storereduceraction组成的,接下来咱们一步一步来尝试的搭建Redux

Redux的分步实现

reducer

依据下面图外面的介绍咱们晓得reducer就是依据传来的type,对相干state进行解决,之后返回一个新的state。由此咱们失去以下代码:

// reducer.jsconst 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.jsimport { 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.jsimport 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.jsimport 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.jsimport 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.dispatchstore.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中应用中间件的时候,都会用到applyMiddlewareapplyMiddleware实际上和下面咱们写的例子的性能是差不多的,你能够了解成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,ccompose函数组合后后果是c(b(a(...args))),执行程序为a->b->c

总结

兴许前面你看到redux-thunk的源码的时候我可能会觉着这个库为什么这么简略就这么几行代码,然而其实没必要诧异,因为就算是redux也不是很简单,然而背地蕴含的JS编程思维却值得去学习,比方函数的柯里化函数式编程装璜器等等常识。

材料:

8k字 | Redux/react-redux/redux中间件设计实现分析

redux中间件的原理

Redux 入门教程(二):中间件与异步操作

JavaScript函数柯里化

代码组合(compose)