上一篇文章咱们手写了一个Redux,然而单纯的Redux只是一个状态机,是没有UI出现的,所以个别咱们应用的时候都会配合一个UI库,比方在React中应用Redux就会用到React-Redux
这个库。这个库的作用是将Redux的状态机和React的UI出现绑定在一起,当你dispatch action
扭转state
的时候,会自动更新页面。本文还是从它的根本应用动手来本人写一个React-Redux
,而后替换官网的NPM库,并放弃性能统一。
本文全副代码曾经上传GitHub,大家能够拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/react-redux
根本用法
上面这个简略的例子是一个计数器,跑起来成果如下:
要实现这个性能,首先咱们要在我的项目外面增加react-redux
库,而后用它提供的Provider
包裹整个React
App的根组件:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import store from './store'
import App from './App';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
下面代码能够看到咱们还给Provider
提供了一个参数store
,这个参数就是Redux的createStore
生成的store
,咱们须要调一下这个办法,而后将返回的store
传进去:
import { createStore } from 'redux';
import reducer from './reducer';
let store = createStore(reducer);
export default store;
下面代码中createStore
的参数是一个reducer
,所以咱们还要写个reducer
:
const initState = {
count: 0
};
function reducer(state = initState, action) {
switch (action.type) {
case 'INCREMENT':
return {...state, count: state.count + 1};
case 'DECREMENT':
return {...state, count: state.count - 1};
case 'RESET':
return {...state, count: 0};
default:
return state;
}
}
export default reducer;
这里的reduce
会有一个初始state
,外面的count
是0
,同时他还能解决三个action
,这三个action
对应的是UI上的三个按钮,能够对state
外面的计数进行加减和重置。到这里其实咱们React-Redux
的接入和Redux
数据的组织其实曾经实现了,前面如果要用Redux
外面的数据的话,只须要用connect
API将对应的state
和办法连贯到组件外面就行了,比方咱们的计数器组件须要count
这个状态和加一,减一,重置这三个action
,咱们用connect
将它连贯进去就是这样:
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement, reset } from './actions';
function Counter(props) {
const {
count,
incrementHandler,
decrementHandler,
resetHandler
} = props;
return (
<>
<h3>Count: {count}</h3>
<button onClick={incrementHandler}>计数+1</button>
<button onClick={decrementHandler}>计数-1</button>
<button onClick={resetHandler}>重置</button>
</>
);
}
const mapStateToProps = (state) => {
return {
count: state.count
}
}
const mapDispatchToProps = (dispatch) => {
return {
incrementHandler: () => dispatch(increment()),
decrementHandler: () => dispatch(decrement()),
resetHandler: () => dispatch(reset()),
}
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
下面代码能够看到connect
是一个高阶函数,他的第一阶会接管mapStateToProps
和mapDispatchToProps
两个参数,这两个参数都是函数。mapStateToProps
能够自定义须要将哪些state
连贯到以后组件,这些自定义的state
能够在组件外面通过props
拿到。mapDispatchToProps
办法会传入dispatch
函数,咱们能够自定义一些办法,这些办法能够调用dispatch
去dispatch action
,从而触发state
的更新,这些自定义的办法也能够通过组件的props
拿到,connect
的第二阶接管的参数是一个组件,咱们能够猜想这个函数的作用就是将后面自定义的state
和办法注入到这个组件外面,同时要返回一个新的组件给内部调用,所以connect
其实也是一个高阶组件。
到这里咱们汇总来看下咱们都用到了哪些API,这些API就是咱们前面要手写的指标:
Provider
: 用来包裹根组件的组件,作用是注入Redux
的store
。
createStore
:Redux
用来创立store
的外围办法,[咱们另一篇文章曾经手写过了]()。
connect
:用来将state
和dispatch
注入给须要的组件,返回一个新组件,他其实是个高阶组件。
所以React-Redux
外围其实就两个API,而且两个都是组件,作用还很相似,都是往组件外面注入参数,Provider
是往根组件注入store
,connect
是往须要的组件注入state
和dispatch
。
在手写之前咱们先来思考下,为什么React-Redux
要设计这两个API,如果没有这两个API,只用Redux
能够吗?当然是能够的!其实咱们用Redux
的目标不就是心愿用它将整个利用的状态都保留下来,每次操作只用dispatch action
去更新状态,而后UI就自动更新了吗?那我从根组件开始,每一级都把store
传下去不就行了吗?每个子组件须要读取状态的时候,间接用store.getState()
就行了,更新状态的时候就store.dispatch
,这样其实也能达到目标。然而,如果这样写,子组件如果嵌套层数很多,每一级都须要手动传入store
,比拟俊俏,开发也比拟繁琐,而且如果某个新同学忘了传store
,那前面就是一连串的谬误了。所以最好有个货色可能将store
全局的注入组件树,而不须要一层层作为props
传递,这个货色就是Provider
!而且如果每个组件都独立依赖Redux
会毁坏React
的数据流向,这个咱们前面会讲到。
React的Context API
React其实提供了一个全局注入变量的API,这就是context api。如果我当初有一个需要是要给咱们所有组件传一个文字色彩的配置,咱们的色彩配置在最顶级的组件上,当这个色彩扭转的时候,上面所有组件都要主动利用这个色彩。那咱们能够应用context api注入这个配置:
先应用React.createContext
创立一个context
// 咱们应用一个独自的文件来调用createContext
// 因为这个返回值会被Provider和Consumer在不同的中央援用
import React from 'react';
const TestContext = React.createContext();
export default TestContext;
应用Context.Provider
包裹根组件
创立好了context,如果咱们要传递变量给某些组件,咱们须要在他们的根组件上加上TestContext.Provider
,而后将变量作为value
参数传给TestContext.Provider
:
import TestContext from './TestContext';
const setting = {
color: '#d89151'
}
ReactDOM.render(
<TestContext.Provider value={setting}>
<App />
</TestContext.Provider>,
document.getElementById('root')
);
应用Context.Consumer
接管参数
下面咱们应用Context.Provider
将参数传递进去了,这样被Context.Provider
包裹的所有子组件都能够拿到这个变量,只是拿这个变量的时候须要应用Context.Consumer
包裹,比方咱们后面的Counter
组件就能够拿到这个色彩了,只须要将它返回的JSX
用Context.Consumer
包裹一下就行:
// 留神要引入同一个Context
import TestContext from './TestContext';
// ... 两头省略n行代码 ...
// 返回的JSX用Context.Consumer包裹起来
// 留神Context.Consumer外面是一个办法,这个办法就能够拜访到context参数
// 这里的context也就是后面Provider传进来的setting,咱们能够拿到下面的color变量
return (
<TestContext.Consumer>
{context =>
<>
<h3 style={{color:context.color}}>Count: {count}</h3>
<button onClick={incrementHandler}>计数+1</button>
<button onClick={decrementHandler}>计数-1</button>
<button onClick={resetHandler}>重置</button>
</>
}
</TestContext.Consumer>
);
下面代码咱们通过context
传递了一个全局配置,能够看到咱们文字色彩曾经变了:
应用useContext
接管参数
除了下面的Context.Consumer
能够用来接管context
参数,新版React还有useContext
这个hook能够接管context参数,应用起来更简略,比方下面代码能够这样写:
const context = useContext(TestContext);
return (
<>
<h3 style={{color:context.color}}>Count: {count}</h3>
<button onClick={incrementHandler}>计数+1</button>
<button onClick={decrementHandler}>计数-1</button>
<button onClick={resetHandler}>重置</button>
</>
);
所以咱们齐全能够用context api
来传递redux store
,当初咱们也能够猜想React-Redux
的Provider
其实就是包装了Context.Provider
,而传递的参数就是redux store
,而React-Redux
的connect
HOC其实就是包装的Context.Consumer
或者useContext
。咱们能够依照这个思路来本人实现下React-Redux
了。
手写Provider
下面说了Provider
用了context api
,所以咱们要先建一个context
文件,导出须要用的context
:
// Context.js
import React from 'react';
const ReactReduxContext = React.createContext();
export default ReactReduxContext;
这个文件很简略,新建一个context
再导出就行了,对应的源码看这里。
而后将这个context
利用到咱们的Provider
组件外面:
import React from 'react';
import ReactReduxContext from './Context';
function Provider(props) {
const {store, children} = props;
// 这是要传递的context
const contextValue = { store };
// 返回ReactReduxContext包裹的组件,传入contextValue
// 外面的内容就间接是children,咱们不动他
return (
<ReactReduxContext.Provider value={contextValue}>
{children}
</ReactReduxContext.Provider>
)
}
Provider
的组件代码也不难,间接将传进来的store
放到context
上,而后间接渲染children
就行,对应的源码看这里。
手写connect
基本功能
其实connect
才是React-Redux中最难的局部,外面性能简单,思考的因素很多,想要把它搞明确咱们须要一层一层的来看,首先咱们实现一个只具备基本功能的connect
。
import React, { useContext } from 'react';
import ReactReduxContext from './Context';
// 第一层函数接管mapStateToProps和mapDispatchToProps
function connect(mapStateToProps, mapDispatchToProps) {
// 第二层函数是个高阶组件,外面获取context
// 而后执行mapStateToProps和mapDispatchToProps
// 再将这个后果组合用户的参数作为最终参数渲染WrappedComponent
// WrappedComponent就是咱们应用connext包裹的本人的组件
return function connectHOC(WrappedComponent) {
function ConnectFunction(props) {
// 复制一份props到wrapperProps
const { ...wrapperProps } = props;
// 获取context的值
const context = useContext(ReactReduxContext);
const { store } = context; // 解构出store
const state = store.getState(); // 拿到state
// 执行mapStateToProps和mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
// 组装最终的props
const actualChildProps = Object.assign({}, stateProps, dispatchProps, wrapperProps);
// 渲染WrappedComponent
return <WrappedComponent {...actualChildProps}></WrappedComponent>
}
return ConnectFunction;
}
}
export default connect;
触发更新
用下面的Provider
和connect
替换官网的react-redux
其实曾经能够渲染出页面了,然而点击按钮还不会有反馈,因为咱们尽管通过dispatch
扭转了store
中的state
,然而这种扭转并没有触发咱们组件的更新。之前Redux那篇文章讲过,能够用store.subscribe
来监听state
的变动并执行回调,咱们这里须要注册的回调是查看咱们最终给WrappedComponent
的props
有没有变动,如果有变动就从新渲染ConnectFunction
,所以这里咱们须要解决两个问题:
- 当咱们
state
变动的时候查看最终给到ConnectFunction
的参数有没有变动- 如果这个参数有变动,咱们须要从新渲染
ConnectFunction
查看参数变动
要查看参数的变动,咱们须要晓得上次渲染的参数和本地渲染的参数,而后拿过去比一下就晓得了。为了晓得上次渲染的参数,咱们能够间接在ConnectFunction
外面应用useRef
将上次渲染的参数记录下来:
// 记录上次渲染参数
const lastChildProps = useRef();
useLayoutEffect(() => {
lastChildProps.current = actualChildProps;
}, []);
留神lastChildProps.current
是在第一次渲染完结后赋值,而且须要应用useLayoutEffect
来保障渲染后立刻同步执行。
因为咱们检测参数变动是须要从新计算actualChildProps
,计算的逻辑其实都是一样的,咱们将这块计算逻辑抽出来,成为一个独自的办法childPropsSelector
:
function childPropsSelector(store, wrapperProps) {
const state = store.getState(); // 拿到state
// 执行mapStateToProps和mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
return Object.assign({}, stateProps, dispatchProps, wrapperProps);
}
而后就是注册store
的回调,在外面来检测参数是否变了,如果变了就强制更新以后组件,比照两个对象是否相等,React-Redux
外面是采纳的shallowEqual
,也就是浅比拟,也就是只比照一层,如果你mapStateToProps
返回了好几层构造,比方这样:
{
stateA: {
value: 1
}
}
你去改了stateA.value
是不会触发从新渲染的,React-Redux
这样设计我想是出于性能思考,如果是深比拟,比方递归去比拟,比拟节约性能,而且如果有循环援用还可能造成死循环。采纳浅比拟就须要用户遵循这种范式,不要传入多层构造,这点在官网文档中也有阐明。咱们这里间接抄一个它的浅比拟:
// shallowEqual.js
function is(x, y) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
return x !== x && y !== y
}
}
export default function shallowEqual(objA, objB) {
if (is(objA, objB)) return true
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false
}
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
if (keysA.length !== keysB.length) return false
for (let 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
}
在回调外面检测参数变动:
// 注册回调
store.subscribe(() => {
const newChildProps = childPropsSelector(store, wrapperProps);
// 如果参数变了,记录新的值到lastChildProps上
// 并且强制更新以后组件
if(!shallowEqual(newChildProps, lastChildProps.current)) {
lastChildProps.current = newChildProps;
// 须要一个API来强制更新以后组件
}
});
强制更新
要强制更新以后组件的办法不止一个,如果你是用的Class
组件,你能够间接this.setState({})
,老版的React-Redux
就是这么干的。然而新版React-Redux
用hook重写了,那咱们能够用React提供的useReducer
或者useState
hook,React-Redux
源码用了useReducer
,为了跟他保持一致,我也应用useReducer
:
function storeStateUpdatesReducer(count) {
return count + 1;
}
// ConnectFunction外面
function ConnectFunction(props) {
// ... 后面省略n行代码 ...
// 应用useReducer触发强制更新
const [
,
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, 0);
// 注册回调
store.subscribe(() => {
const newChildProps = childPropsSelector(store, wrapperProps);
if(!shallowEqual(newChildProps, lastChildProps.current)) {
lastChildProps.current = newChildProps;
forceComponentUpdateDispatch();
}
});
// ... 前面省略n行代码 ...
}
connect
这块代码次要对应的是源码中connectAdvanced
这个类,基本原理和构造跟咱们这个都是一样的,只是他写的更灵便,反对用户传入自定义的childPropsSelector
和合并stateProps, dispatchProps, wrapperProps
的办法。有趣味的敌人能够去看看他的源码:https://github.com/reduxjs/react-redux/blob/master/src/components/connectAdvanced.js
到这里其实曾经能够用咱们本人的React-Redux
替换官网的了,计数器的性能也是反对了。然而上面还想讲一下React-Redux
是怎么保障组件的更新程序的,因为源码中很多代码都是在解决这个。
保障组件更新程序
后面咱们的Counter
组件应用connect
连贯了redux store
,如果他上面还有个子组件也连贯到了redux store
,咱们就要思考他们的回调的执行程序的问题了。咱们晓得React是单向数据流的,参数都是由父组件传给子组件的,当初引入了Redux
,即便父组件和子组件都援用了同一个变量count
,然而子组件齐全能够不从父组件拿这个参数,而是间接从Redux
拿,这样就突破了React
原本的数据流向。在父->子
这种单向数据流中,如果他们的一个专用变量变动了,必定是父组件先更新,而后参数传给子组件再更新,然而在Redux
里,数据变成了Redux -> 父,Redux -> 子
,父
与子
齐全能够依据Redux
的数据进行独立更新,而不能齐全保障父级先更新,子级再更新的流程。所以React-Redux
花了不少功夫来手动保障这个更新程序,React-Redux
保障这个更新程序的计划是在redux store
外,再独自创立一个监听者类Subscription
:
Subscription
负责解决所有的state
变动的回调- 如果以后连贯
redux
的组件是第一个连贯redux
的组件,也就是说他是连贯redux
的根组件,他的state
回调间接注册到redux store
;同时新建一个Subscription
实例subscription
通过context
传递给子级。- 如果以后连贯
redux
的组件不是连贯redux
的根组件,也就是说他下面有组件曾经注册到redux store
了,那么他能够拿到下面通过context
传下来的subscription
,源码外面这个变量叫parentSub
,那以后组件的更新回调就注册到parentSub
上。同时再新建一个Subscription
实例,代替context
上的subscription
,持续往下传,也就是说他的子组件的回调会注册到以后subscription
上。- 当
state
变动了,根组件注册到redux store
上的回调会执行更新根组件,同时根组件须要手动执行子组件的回调,子组件回调执行会触发子组件更新,而后子组件再执行本人subscription
上注册的回调,触发孙子组件更新,孙子组件再调用注册到本人subscription
上的回调。。。这样就实现了从根组件开始,一层一层更新子组件的目标,保障了父->子
这样的更新程序。
Subscription
类
所以咱们先新建一个Subscription
类:
export default class Subscription {
constructor(store, parentSub) {
this.store = store
this.parentSub = parentSub
this.listeners = []; // 源码listeners是用链表实现的,我这里简略解决,间接数组了
this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
}
// 子组件注册回调到Subscription上
addNestedSub(listener) {
this.listeners.push(listener)
}
// 执行子组件的回调
notifyNestedSubs() {
const length = this.listeners.length;
for(let i = 0; i < length; i++) {
const callback = this.listeners[i];
callback();
}
}
// 回调函数的包装
handleChangeWrapper() {
if (this.onStateChange) {
this.onStateChange()
}
}
// 注册回调的函数
// 如果parentSub有值,就将回调注册到parentSub上
// 如果parentSub没值,那以后组件就是根组件,回调注册到redux store上
trySubscribe() {
this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)
}
}
Subscription
对应的源码看这里。
革新Provider
而后在咱们后面本人实现的React-Redux
外面,咱们的根组件始终是Provider
,所以Provider
须要实例化一个Subscription
并放到context
上,而且每次state
更新的时候须要手动调用子组件回调,代码革新如下:
import React, { useMemo, useEffect } from 'react';
import ReactReduxContext from './Context';
import Subscription from './Subscription';
function Provider(props) {
const {store, children} = props;
// 这是要传递的context
// 外面放入store和subscription实例
const contextValue = useMemo(() => {
const subscription = new Subscription(store)
// 注册回调为告诉子组件,这样就能够开始层级告诉了
subscription.onStateChange = subscription.notifyNestedSubs
return {
store,
subscription
}
}, [store])
// 拿到之前的state值
const previousState = useMemo(() => store.getState(), [store])
// 每次contextValue或者previousState变动的时候
// 用notifyNestedSubs告诉子组件
useEffect(() => {
const { subscription } = contextValue;
subscription.trySubscribe()
if (previousState !== store.getState()) {
subscription.notifyNestedSubs()
}
}, [contextValue, previousState])
// 返回ReactReduxContext包裹的组件,传入contextValue
// 外面的内容就间接是children,咱们不动他
return (
<ReactReduxContext.Provider value={contextValue}>
{children}
</ReactReduxContext.Provider>
)
}
export default Provider;
革新connect
有了Subscription
类,connect
就不能间接注册到store
了,而是应该注册到父级subscription
上,更新的时候除了更新本人还要告诉子组件更新。在渲染包裹的组件时,也不能间接渲染了,而是应该再次应用Context.Provider
包裹下,传入批改过的contextValue
,这个contextValue
外面的subscription
应该替换为本人的。革新后代码如下:
import React, { useContext, useRef, useLayoutEffect, useReducer } from 'react';
import ReactReduxContext from './Context';
import shallowEqual from './shallowEqual';
import Subscription from './Subscription';
function storeStateUpdatesReducer(count) {
return count + 1;
}
function connect(
mapStateToProps = () => {},
mapDispatchToProps = () => {}
) {
function childPropsSelector(store, wrapperProps) {
const state = store.getState(); // 拿到state
// 执行mapStateToProps和mapDispatchToProps
const stateProps = mapStateToProps(state);
const dispatchProps = mapDispatchToProps(store.dispatch);
return Object.assign({}, stateProps, dispatchProps, wrapperProps);
}
return function connectHOC(WrappedComponent) {
function ConnectFunction(props) {
const { ...wrapperProps } = props;
const contextValue = useContext(ReactReduxContext);
const { store, subscription: parentSub } = contextValue; // 解构出store和parentSub
const actualChildProps = childPropsSelector(store, wrapperProps);
const lastChildProps = useRef();
useLayoutEffect(() => {
lastChildProps.current = actualChildProps;
}, [actualChildProps]);
const [
,
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, 0)
// 新建一个subscription实例
const subscription = new Subscription(store, parentSub);
// state回调抽出来成为一个办法
const checkForUpdates = () => {
const newChildProps = childPropsSelector(store, wrapperProps);
// 如果参数变了,记录新的值到lastChildProps上
// 并且强制更新以后组件
if(!shallowEqual(newChildProps, lastChildProps.current)) {
lastChildProps.current = newChildProps;
// 须要一个API来强制更新以后组件
forceComponentUpdateDispatch();
// 而后告诉子级更新
subscription.notifyNestedSubs();
}
};
// 应用subscription注册回调
subscription.onStateChange = checkForUpdates;
subscription.trySubscribe();
// 批改传给子级的context
// 将subscription替换为本人的
const overriddenContextValue = {
...contextValue,
subscription
}
// 渲染WrappedComponent
// 再次应用ReactReduxContext包裹,传入批改过的context
return (
<ReactReduxContext.Provider value={overriddenContextValue}>
<WrappedComponent {...actualChildProps} />
</ReactReduxContext.Provider>
)
}
return ConnectFunction;
}
}
export default connect;
到这里咱们的React-Redux
就实现了,跑起来的成果跟官网的成果一样,残缺代码曾经上传GitHub了:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/react-redux
上面咱们再来总结下React-Redux
的外围原理。
总结
-
React-Redux
是连贯React
和Redux
的库,同时应用了React
和Redux
的API。 -
React-Redux
次要是应用了React
的context api
来传递Redux
的store
。 -
Provider
的作用是接管Redux store
并将它放到context
上传递上来。 -
connect
的作用是从Redux store
中选取须要的属性传递给包裹的组件。 -
connect
会本人判断是否须要更新,判断的根据是须要的state
是否曾经变动了。 -
connect
在判断是否变动的时候应用的是浅比拟,也就是只比拟一层,所以在mapStateToProps
和mapDispatchToProps
中不要反回多层嵌套的对象。 - 为了解决父组件和子组件各自独立依赖
Redux
,毁坏了React
的父级->子级
的更新流程,React-Redux
应用Subscription
类本人治理了一套告诉流程。 - 只有连贯到
Redux
最顶级的组件才会间接注册到Redux store
,其余子组件都会注册到最近父组件的subscription
实例上。 - 告诉的时候从根组件开始顺次告诉本人的子组件,子组件接管到告诉的时候,先更新本人再告诉本人的子组件。
参考资料
官网文档:https://react-redux.js.org/
GitHub源码:https://github.com/reduxjs/react-redux/
文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和GitHub小星星,你的反对是作者继续创作的能源。
作者博文GitHub我的项目地址: https://github.com/dennis-jiang/Front-End-Knowledges
发表回复