实现简版的redux、react-redux

289 阅读5分钟

redux实现目标

redux是一个状态管理容器,提供一个全局对象,这个对象只能通过派发action去修改。创建一个js文件,这个文件提供三个api:

1.createStore,提供三个api: dispatch、getState、subscribe
2.combineReducers,将多个reducer整合成一个reducer
3.applyMiddleware,接收并处理多个中间件

createStore

function createStore(reducers, enhancer) {
    // 处理中间件
    if (enhancer) {
        return enhancer(createStore)(reducers)
    }
    // 唯一的store,数据的源头,包含多个state,有多少个reducer就有多少个state
    let store = {};
    // 初始化一下store的值
    store = reducers(store, {});
    
    // 存放订阅的回调
    const queques = [];
    function dispatch(action) {        
        // 这里会将reducers中所有reducer全跑一遍,只要是有这个type的,全都会执行
        store = reducers(store, action);
        // 将队列中所有注册的回调,全跑一遍
        queques.forEach(queque => queque());
    }
    // 只返回了当前store,快照
    function getState() {
        return store
    }
    // 只是将回调存了起来,并不会执行
    function subscribe(cb) {
        queques.push(cb)
    }
    return {
        dispatch,
        getState,
        subscribe
    }
}

combineReducers

/**
 * 将所有reducers整合整一个reducer
 * 返回一个接受state、action的函数,调用该函数会依次调用reducer,改变各自的state
 * 返回值是对象,以reducer名字为key,各自reducer返回值为值
 * @param {*} reducers
 */
function combineReducers(reducers) {
    let keys = Object.keys(reducers);
    // 看到这里应该理解为什么reducer名字的由来了
    return (state = {}, action) => keys.reduce((a, b) => {
        // a, b 对应:{}、'curReducer',{curReducer: {}}, 'userReducer',打印一下就知道
        a[b] = reducers[b](state[b], action);
        return a
    }, {})
}

applyMiddleware

先写两个常用的中间件:

function logger() {
    return dispatch => action => {
        console.log('logger -> action.type:', action.type);
        return dispatch(action)
    }
}
// 初始的dispatch是不支持接收函数的,只接收对象
function thunk() {
    return dispatch => action => {
        // 这里处理异步action
        if (typeof action === 'function') {
            // 先执行异步,再将dispatch作为参数传过去
            action(dispatch)
        } else {
            return dispatch(action)
        }
        
    }
}

中间件是个三层嵌套的函数:store => next => action,最后返回执行上一个中间件,next就是上一个中间件。最后一个next就是最开始传入的store.dispatch。

// 页面触发dispatch的方法
fnHandle(type) {
    if (type === 'delay') {
        // 异步action
        store.dispatch((dispatch) => {
            setTimeout(() => {
                // 这里的dispatch就是上边中间件传过来的
                dispatch({type: 'add'})
            }, 1000)
        })
    } else {
        store.dispatch({type})
    }
}

compose

这个是理解中间件的基础,先举个例子,写一个函数,依次执行依次三个函数:

function fn1(arg) {
    console.log('fn1', arg);
    return arg
}
function fn2(arg) {
    console.log('fn2', arg);
    return arg
}
function fn3(arg) {
    console.log('fn3', arg);
    return arg
}

这就用到了compose,组合函数:

function compose(...fns) {
    // 这个最终会返回一个函数,每层函数的参数都是上一层函数的返回值
    return fns.reduce((a, b) => (arg) => {
        return a(b(arg))
    })
}
compose(fn1, fn2, fn3)('00')

执行结果:

fn3 00
fn2 00
fn1 00
"00"
// 如果需要执行更多的方法,只需添加到参数中就行了。中间件原理就是这样

这里的顺序是 fn3 -> fn2 -> fn1

// 返回值和createStore一样
function applyMiddleware(...middleWares) {
    return createStore => (reducers) => {
        let store = createStore(reducers);
        let dispatch;
        // 这里传入的middleWareOptions,暂时用不到,这里可能是提供给中间件更多的api吧
        const middleWareOptions = { store: store.getState(), dispatch: store.dispatch}
        let _middleWares = middleWares.map(middleWare => middleWare(middleWareOptions));
        
        // 源码的写法:dispatch = compose(...chain)(store.dispatch)
        let _compose = compose(..._middleWares);
        dispatch = _compose(store.dispatch);
        return {
            ...store,
            dispatch
        }
    }
}

剖析中间件过程

1.let _middleWares = middleWares.map(middleWare => middleWare(middleWareOptions));
2.let _compose = compose(..._middleWares);
3.dispatch = _compose(store.dispatch);

写三个中间件,分析一下以上三步代码:

const A = () => {
    return next => action => {
        // do someting...
        return next(action)
    }
}
const B = () => {
    return next => action => {
        // do someting...
        return next(action)
    }
}
const C = () => {
    return next => action => {
        // do someting...
        return next(action)
    }
}

第一步,传入store之类的配置项,依次执行中间件,此时_middleWares里的fn,是这样的:

const fnA = next => action => {
    // do someting...
    return next(action)
}
const fnB = next => action => {
    // do someting...
    return next(action)
}
const fnC = next => action => {
    // do someting...
    return next(action)
}

第二步,compose(...chain),将传入的函数的组合成一个函数:

(arg) => a(b(arg))

第三步,传入store.dispatch,是这样的:

执行 fnA(fnB(fnC(store.dispatch)))

这个顺序肯定是由内向外的,分析一下这个步骤:

// 执行最里层的fnC:
fnA(fnB((store.dispatch) => action => {
    // do someting...
    return store.dispatch(action)
}))

// 那么fnB是这样:
const fnB = action => {
    // do someting...
    return fnC(action)
}

// 执行fnB这一层:
const fnA = action => {
    // do someting...
    return fnB(action)
}

// 最后执行最外层的fnA,得到最终的dispatch:
(action) => {
    // do someting...
    return fnA(action)
}

当执行dispatch(action), 还是先执行fnA函数体,再执行fnB,最后的那个next才是原始的store.distach。这里广泛用到了科里化,保存了之前的参数引用。

梳理下以上的工作流程:

- 1.ui调用dispatch派发一个action
- 2.reducer根据action.type,修改相应的state并返回给store,下一步更新ui
- 3.组件在挂载后,在subscribe中订阅了forceUpdate,完成了以上两步就将store中的订阅回调执行一遍

以上就是redux的实现流程了,但是存在两个问题:

- 1.数据的来源,需要通过store.getState(),太麻烦
- 2.更新ui试图,还需要手动去触发

react-redux实现目标

// 入口文件:
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
...
// 子组件
const mapStateToProps = state => (
    {
        curReducer: state.curReducer,
        userReducer: state.userReducer
    }
)
// 源码中可以是对象,也可是函数,这里使用对象
const mapDispatchToProps = {add, minus, delay}

export default connect(mapStateToProps, mapDispatchToProps)(ReactReduxComp)

Provider

看上去就是实现一个类似React.createCentext的Provide标签,注入store,将子组件包裹进去。作用就是所有的组件都变成了它的子组件,后续就可以直接获取这个上下文。

class Provider extends React.Component {
    render() {
        return (
            <ReactContext.Provider value={this.props.store}>
                {this.props.children}
            </ReactContext.Provider>
        )
    }
}

当然了,首先要在首部声明一个上下文:

const ReactContext = React.createContext();

connect

connect作用就是将redux的数据和react组件联系到一起。

1.将组件需要的store的值合并props中
2.将派发的action方法,包装一层dispatch,也合并到props中
3.connect内部的副作用注册更新props方法,不需要再手动去触发页面更新了
const connect = (mapStateToProps, mapDispatchToProps) => Comp => props => {
    // 获取当前上下文中的context,即Provider的参数store
    let store = useContext(ReactContext);
    const {dispatch, getState} = store;
    // 获取所有state和dispatch
    let getProps = () => {
        let states = mapStateToProps(getState());
        // 将dispatch绑定到各个action上
        let dispatchs = bindActionCreators(mapDispatchToProps, dispatch);
        return {
            ...states,
            ...dispatchs
        }
    }
    
    const [multipleProps, setMultipleProps] = useState(getProps());
    useEffect(() => {
        store.subscribe(() => {
            // 当store里的值变化了,重新计算props,props改变后触发组件render
            setMultipleProps({
                ...multipleProps,
                ...getProps()
            })
        })
    }, [])
    return <Comp {...props} {...multipleProps} />
}
const bindActionCreators = (actions, dispatch) => {
    let creators = {};
    Object.keys(actions).map(key => {
        creators[key] = (arg) => dispatch(actions[key](arg))
    })
    return creators
}

以上方法就是把

const add = () => ({type: 'add'})

变成了

add: (arg) => dispatch(add(arg))

相比较来看,个人觉得react-redux实现要比redux简单一些,主要是redux的中间件有点绕。