• React-redux的实现原理
  • 发布于 2个月前
  • 327 热度
    0 评论
写在前面
本文尝试参照react-redux的源码实现一个简易版的react状态管理功能、看懂本文再去看react-redux和redux的源码应该能事半功倍。

React-redux的使用
react-redux的基本使用可以参照如下代码
说明一下:由于webpack打包调试会存在一系列变量读取问题、不利于源码的阅读(本人水平有限😭)、本文都采用的unpkg的JS版本。具体的文件可以在node_modules中对应包的umd文件夹下寻找。
<div id="app"></div>
<script src="js/react.js" type="text/javascript"></script>
<script src="js/react-dom.js" type="text/javascript"></script>
<script src="js/react-redux.js" type="text/javascript"></script>
<script src="js/redux.js" type="text/javascript"></script>
<script src="js/babel.js"></script>
<script type="text/babel">

const { Provider, connect } = ReactRedux;

const { createStore } = Redux;

const Sub = connect(state => state)((props) => {
    console.log('sub update...', props);
    return (
            <div onClick={() => { props.dispatch({ type: 123 }) }}>
                hello, Sub! {props.age}
            </div>
    )
})
const Sub2 = connect(state => ({name: state.name}))((props) => {
    console.log('sub2 update...', props);
    return (
            <div>hello, Sub2! {props.name}</div>
    )
})
function reducer(state, action) {
    return {
            ...state,
            age: 15 + Math.random()
    }
}
const store = createStore(reducer, { name: '_zhangsan_' });
const Body = (props, ref) => {
    return (
            <Provider store={store}>
                    <Sub />
                    <Sub2 />
            </Provider>
    )
}
ReactDOM.render(<Body />, document.getElementById('app'));
我们要想实现react-redux、其实只要实现Provider、connect、createStore三个方法就可以了。

React-redux的实现
我们如何实现关键的函数、其实想清楚两个问题就好了:
1.如何更新react的一个组件、遗弃Class组件、这里统一都指的函数组件。(Class组件应该也一样、只是API不同)

2.react-redux怎么知道一个组件订阅了哪些变量、例如上述代码中、Sub组件订阅了所有的变量、Sub2只订阅了name、因此更新age、Sub更新而Sub2不更新。


思考并回答一下这两个问题:
1. 更新react的组件其实只有两种方法、第一种是通过useState或者useReducer、第二种是通过父组件更新从而触发子组件更新。react-redux采用的第二种方式、本质上就是一个高阶组件、利用connect函数将我们的组件裹一层父组件、通过控制父组件来操作我们的组件。(第一种方案也能实现、大家可以思考下怎么做)

2.第二个问题没看react-redux源码前我也很好奇、难不成和vue一样用defineProperty或者Proxy去监听state的每个属性。后来我尝试了一下将Sub2组件的name改成了Math.random()、发现其也会更新、可以推断出内部实现应该没这么复杂。
其本质是每次调用connet函数传入的mapToState方法、对比该方法返回的结果、如果有变化则更新、如果没变化则不更新。

解决了以上两个问题、编写代码就比较简单了、我们先来编写connect方法
//堆代码 duidaima.com
function myConnect(mapToState) {
    return function (Component) {
        return function (props) {
            const [, setUpdateObject] = useState({});
            const state = store.getState();
            const effectState = mapToState(state);
            function checkForUpdates() {
                var newState = store.getState();
                if (newState != state) {
                    var newEffectState = mapToState(newState);
                    if (!shallowEqual(effectState, newEffectState)) {
                        setUpdateObject({});
                    }
                }
            }
            useLayoutEffect(() => {
                var unsubscribe = store.subscribe(checkForUpdates);
                return unsubscribe;
            }, [store]);
            return React.createElement(
                Component, 
                Object.assign({}, props, effectState, { dispatch: store.dispatch })
            );
        }
    }
}
简单解释下、myConnet方法返回的是第三行的匿名函数、该函数是用来渲染的我们的组件、函数里面涉及到的store是我们用createStore创建的store对象。通过useLayoutEffect在store中注册一个checkForUpdates方法:首先拿到store中的state对象、调用mapToState获取新的state、然后通过shallowEqual去对比是否有更新、如果有更新、通过setState触发父组件更新、从而更新我们的子组件。

完整的代码
我们还是通过完整的代码来看:
<div id="app"></div>
<script src="js/react.js" type="text/javascript"></script>
<script src="js/react-dom.js" type="text/javascript"></script>
<script src="js/babel.js"></script>
<script type="text/babel">
const { useState, useLayoutEffect } = React;

function myCreateStore(reducer, initState) {
    var state = initState;
    var listeners = [];
    function dispatch(action) {
        state = reducer(state, action);
        for (var i = 0; i < listeners.length; i++) {
                listeners[i]();
        }
        return action
    }
    dispatch({
        type: '@@redux/INIT'
    })
    return {
        dispatch,
        subscribe(cb) {
            var i = listeners.length;
            listeners.push(cb);
            return function () {
                listeners.splice(i, 1);
            }
        },
        getState() {
            return state;
        }
    }
}
function myConnect(mapToState) {
    return function (Component) {
        return function (props) {
            const [, setUpdateObject] = useState({});
            const state = store.getState();
            const effectState = mapToState(state);
            function checkForUpdates() {
                var newState = store.getState();
                if (newState != state) {
                        var newEffectState = mapToState(newState);
                        if (!shallowEqual(effectState, newEffectState)) {
                                setUpdateObject({});
                        }
                }
            }
            useLayoutEffect(() => {
                var unsubscribe = store.subscribe(checkForUpdates);
                return unsubscribe;
            }, [store]);
            return React.createElement(
                Component, 
                Object.assign({}, props, effectState, { dispatch: store.dispatch })
            );
        }
    }
}

const Sub = myConnect(state => state)((props) => {
    console.log('sub update...', props);
    // useLayoutEffect(() => {
    //  props.dispatch({ type: 123 })
    // }, [])
    return (
        <div onClick={() => { props.dispatch({ type: 123 }) }}>
            hello, Sub! {props.age}
        </div>
    )
})
const Sub2 = myConnect(state => ({name: state.name}))((props) => {
    console.log('sub2 update...', props);
    return (
        <div>hello, Sub2! {props.name}</div>
    )
})
function reducer(state, action) {
    return {
        ...state,
        age: 15 + Math.random()
    }
}
const store = myCreateStore(reducer, { name: '_zhangsan_' });
const Body = (props, ref) => {
    return (
        <div>
            <Sub />
            <Sub2 />
        </div>
    )
}
ReactDOM.render(<Body />, document.getElementById('app'));

function shallowEqual(objA, objB) {
    if (is(objA, objB)) return true;

    if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
            return false;
    }

    var keysA = Object.keys(objA);
    var keysB = Object.keys(objB);
    if (keysA.length !== keysB.length) return false;

    for (var i = 0; i < keysA.length; i++) {
            if (!Object.prototype.hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) {
                    return false;
            }
    }

    return true;
}
function is(x, y) {
        if (x === y) {
                return x !== 0 || y !== 0 || 1 / x === 1 / y;
        } else {
                return x !== x && y !== y;
        }
}
这里直接省略了Provider、因为其作用是用来传递store对象。源码是通过React.createContext 创建一个Context、然后通过Provider来读取外界创建的store并挂载在Context中、这样内部就能通过Context来使用store。代码并不复杂、这里就不多解释了、希望大家好好阅读一下。

最后
给大家提两个问题
1. 为什么connect函数中要用useLayoutEffect去注册订阅事件、用useEffect行不行?
2.Sub组件中有段注释代码、如果放开了会出现什么情况、该怎么解决这种情况?

本文仅仅代表自己对react-redux源码的理解、如果还有什么疑问或者建议,可以多多交流,文笔有限,才疏学浅,文中若有不正之处,万望告知。
用户评论