作者:周婷是个死胖子
本文来自小伙伴投稿,欢迎大家投稿哦!
写在前面
本文尝试参照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
的实现
我们如何实现关键的函数、其实想清楚两个问题就好了
如何更新
react
的一个组件、遗弃Class
组件、这里统一都指的函数组件
。(Class组件
应该也一样、只是API
不同)
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
方法
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
源码的理解、如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。