上一篇文章我们手写了一个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包裹整个ReactApp的根组件:

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import { Provider } from 'react-redux'
  4. import store from './store'
  5. import App from './App';
  6. ReactDOM.render(
  7. <React.StrictMode>
  8. <Provider store={store}>
  9. <App />
  10. </Provider>
  11. </React.StrictMode>,
  12. document.getElementById('root')
  13. );

上面代码可以看到我们还给Provider提供了一个参数store,这个参数就是Redux的createStore生成的store,我们需要调一下这个方法,然后将返回的store传进去:

  1. import { createStore } from 'redux';
  2. import reducer from './reducer';
  3. let store = createStore(reducer);
  4. export default store;

上面代码中createStore的参数是一个reducer,所以我们还要写个reducer:

  1. const initState = {
  2. count: 0
  3. };
  4. function reducer(state = initState, action) {
  5. switch (action.type) {
  6. case 'INCREMENT':
  7. return {...state, count: state.count + 1};
  8. case 'DECREMENT':
  9. return {...state, count: state.count - 1};
  10. case 'RESET':
  11. return {...state, count: 0};
  12. default:
  13. return state;
  14. }
  15. }
  16. export default reducer;

这里的reduce会有一个初始state,里面的count0,同时他还能处理三个action,这三个action对应的是UI上的三个按钮,可以对state里面的计数进行加减和重置。到这里其实我们React-Redux的接入和Redux数据的组织其实已经完成了,后面如果要用Redux里面的数据的话,只需要用connectAPI将对应的state和方法连接到组件里面就行了,比如我们的计数器组件需要count这个状态和加一,减一,重置这三个action,我们用connect将它连接进去就是这样:

  1. import React from 'react';
  2. import { connect } from 'react-redux';
  3. import { increment, decrement, reset } from './actions';
  4. function Counter(props) {
  5. const {
  6. count,
  7. incrementHandler,
  8. decrementHandler,
  9. resetHandler
  10. } = props;
  11. return (
  12. <>
  13. <h3>Count: {count}</h3>
  14. <button onClick={incrementHandler}>计数+1</button>
  15. <button onClick={decrementHandler}>计数-1</button>
  16. <button onClick={resetHandler}>重置</button>
  17. </>
  18. );
  19. }
  20. const mapStateToProps = (state) => {
  21. return {
  22. count: state.count
  23. }
  24. }
  25. const mapDispatchToProps = (dispatch) => {
  26. return {
  27. incrementHandler: () => dispatch(increment()),
  28. decrementHandler: () => dispatch(decrement()),
  29. resetHandler: () => dispatch(reset()),
  30. }
  31. };
  32. export default connect(
  33. mapStateToProps,
  34. mapDispatchToProps
  35. )(Counter)

上面代码可以看到connect是一个高阶函数,他的第一阶会接收mapStateToPropsmapDispatchToProps两个参数,这两个参数都是函数。mapStateToProps可以自定义需要将哪些state连接到当前组件,这些自定义的state可以在组件里面通过props拿到。mapDispatchToProps方法会传入dispatch函数,我们可以自定义一些方法,这些方法可以调用dispatchdispatch action,从而触发state的更新,这些自定义的方法也可以通过组件的props拿到,connect的第二阶接收的参数是一个组件,我们可以猜测这个函数的作用就是将前面自定义的state和方法注入到这个组件里面,同时要返回一个新的组件给外部调用,所以connect其实也是一个高阶组件。

到这里我们汇总来看下我们都用到了哪些API,这些API就是我们后面要手写的目标:

Provider: 用来包裹根组件的组件,作用是注入Reduxstore

createStore: Redux用来创建store的核心方法,我们另一篇文章已经手写过了

connect:用来将statedispatch注入给需要的组件,返回一个新组件,他其实是个高阶组件。

所以React-Redux核心其实就两个API,而且两个都是组件,作用还很类似,都是往组件里面注入参数,Provider是往根组件注入storeconnect是往需要的组件注入statedispatch

在手写之前我们先来思考下,为什么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

  1. // 我们使用一个单独的文件来调用createContext
  2. // 因为这个返回值会被Provider和Consumer在不同的地方引用
  3. import React from 'react';
  4. const TestContext = React.createContext();
  5. export default TestContext;

使用Context.Provider包裹根组件

创建好了context,如果我们要传递变量给某些组件,我们需要在他们的根组件上加上TestContext.Provider,然后将变量作为value参数传给TestContext.Provider:

  1. import TestContext from './TestContext';
  2. const setting = {
  3. color: '#d89151'
  4. }
  5. ReactDOM.render(
  6. <TestContext.Provider value={setting}>
  7. <App />
  8. </TestContext.Provider>,
  9. document.getElementById('root')
  10. );

使用Context.Consumer接收参数

上面我们使用Context.Provider将参数传递进去了,这样被Context.Provider包裹的所有子组件都可以拿到这个变量,只是拿这个变量的时候需要使用Context.Consumer包裹,比如我们前面的Counter组件就可以拿到这个颜色了,只需要将它返回的JSXContext.Consumer包裹一下就行:

  1. // 注意要引入同一个Context
  2. import TestContext from './TestContext';
  3. // ... 中间省略n行代码 ...
  4. // 返回的JSX用Context.Consumer包裹起来
  5. // 注意Context.Consumer里面是一个方法,这个方法就可以访问到context参数
  6. // 这里的context也就是前面Provider传进来的setting,我们可以拿到上面的color变量
  7. return (
  8. <TestContext.Consumer>
  9. {context =>
  10. <>
  11. <h3 style={{color:context.color}}>Count: {count}</h3>
  12. <button onClick={incrementHandler}>计数+1</button>&nbsp;&nbsp;
  13. <button onClick={decrementHandler}>计数-1</button>&nbsp;&nbsp;
  14. <button onClick={resetHandler}>重置</button>
  15. </>
  16. }
  17. </TestContext.Consumer>
  18. );

上面代码我们通过context传递了一个全局配置,可以看到我们文字颜色已经变了:

使用useContext接收参数

除了上面的Context.Consumer可以用来接收context参数,新版React还有useContext这个hook可以接收context参数,使用起来更简单,比如上面代码可以这样写:

  1. const context = useContext(TestContext);
  2. return (
  3. <>
  4. <h3 style={{color:context.color}}>Count: {count}</h3>
  5. <button onClick={incrementHandler}>计数+1</button>&nbsp;&nbsp;
  6. <button onClick={decrementHandler}>计数-1</button>&nbsp;&nbsp;
  7. <button onClick={resetHandler}>重置</button>
  8. </>
  9. );

所以我们完全可以用context api来传递redux store,现在我们也可以猜测React-ReduxProvider其实就是包装了Context.Provider,而传递的参数就是redux store,而React-ReduxconnectHOC其实就是包装的Context.Consumer或者useContext。我们可以按照这个思路来自己实现下React-Redux了。

手写Provider

上面说了Provider用了context api,所以我们要先建一个context文件,导出需要用的context

  1. // Context.js
  2. import React from 'react';
  3. const ReactReduxContext = React.createContext();
  4. export default ReactReduxContext;

这个文件很简单,新建一个context再导出就行了,对应的源码看这里

然后将这个context应用到我们的Provider组件里面:

  1. import React from 'react';
  2. import ReactReduxContext from './Context';
  3. function Provider(props) {
  4. const {store, children} = props;
  5. // 这是要传递的context
  6. const contextValue = { store };
  7. // 返回ReactReduxContext包裹的组件,传入contextValue
  8. // 里面的内容就直接是children,我们不动他
  9. return (
  10. <ReactReduxContext.Provider value={contextValue}>
  11. {children}
  12. </ReactReduxContext.Provider>
  13. )
  14. }

Provider的组件代码也不难,直接将传进来的store放到context上,然后直接渲染children就行,对应的源码看这里

手写connect

基本功能

其实connect才是React-Redux中最难的部分,里面功能复杂,考虑的因素很多,想要把它搞明白我们需要一层一层的来看,首先我们实现一个只具有基本功能的connect

  1. import React, { useContext } from 'react';
  2. import ReactReduxContext from './Context';
  3. // 第一层函数接收mapStateToProps和mapDispatchToProps
  4. function connect(mapStateToProps, mapDispatchToProps) {
  5. // 第二层函数是个高阶组件,里面获取context
  6. // 然后执行mapStateToProps和mapDispatchToProps
  7. // 再将这个结果组合用户的参数作为最终参数渲染WrappedComponent
  8. // WrappedComponent就是我们使用connext包裹的自己的组件
  9. return function connectHOC(WrappedComponent) {
  10. function ConnectFunction(props) {
  11. // 复制一份props到wrapperProps
  12. const { ...wrapperProps } = props;
  13. // 获取context的值
  14. const context = useContext(ReactReduxContext);
  15. const { store } = context; // 解构出store
  16. const state = store.getState(); // 拿到state
  17. // 执行mapStateToProps和mapDispatchToProps
  18. const stateProps = mapStateToProps(state);
  19. const dispatchProps = mapDispatchToProps(store.dispatch);
  20. // 组装最终的props
  21. const actualChildProps = Object.assign({}, stateProps, dispatchProps, wrapperProps);
  22. // 渲染WrappedComponent
  23. return <WrappedComponent {...actualChildProps}></WrappedComponent>
  24. }
  25. return ConnectFunction;
  26. }
  27. }
  28. export default connect;

触发更新

用上面的Providerconnect替换官方的react-redux其实已经可以渲染出页面了,但是点击按钮还不会有反应,因为我们虽然通过dispatch改变了store中的state,但是这种改变并没有触发我们组件的更新。之前Redux那篇文章讲过,可以用store.subscribe来监听state的变化并执行回调,我们这里需要注册的回调是检查我们最终给WrappedComponentprops有没有变化,如果有变化就重新渲染ConnectFunction,所以这里我们需要解决两个问题:

  1. 当我们state变化的时候检查最终给到ConnectFunction的参数有没有变化
  2. 如果这个参数有变化,我们需要重新渲染ConnectFunction

检查参数变化

要检查参数的变化,我们需要知道上次渲染的参数和本地渲染的参数,然后拿过来比一下就知道了。为了知道上次渲染的参数,我们可以直接在ConnectFunction里面使用useRef将上次渲染的参数记录下来:

  1. // 记录上次渲染参数
  2. const lastChildProps = useRef();
  3. useLayoutEffect(() => {
  4. lastChildProps.current = actualChildProps;
  5. }, []);

注意lastChildProps.current是在第一次渲染结束后赋值,而且需要使用useLayoutEffect来保证渲染后立即同步执行。

因为我们检测参数变化是需要重新计算actualChildProps,计算的逻辑其实都是一样的,我们将这块计算逻辑抽出来,成为一个单独的方法childPropsSelector:

  1. function childPropsSelector(store, wrapperProps) {
  2. const state = store.getState(); // 拿到state
  3. // 执行mapStateToProps和mapDispatchToProps
  4. const stateProps = mapStateToProps(state);
  5. const dispatchProps = mapDispatchToProps(store.dispatch);
  6. return Object.assign({}, stateProps, dispatchProps, wrapperProps);
  7. }

然后就是注册store的回调,在里面来检测参数是否变了,如果变了就强制更新当前组件,对比两个对象是否相等,React-Redux里面是采用的shallowEqual,也就是浅比较,也就是只对比一层,如果你mapStateToProps返回了好几层结构,比如这样:

  1. {
  2. stateA: {
  3. value: 1
  4. }
  5. }

你去改了stateA.value是不会触发重新渲染的,React-Redux这样设计我想是出于性能考虑,如果是深比较,比如递归去比较,比较浪费性能,而且如果有循环引用还可能造成死循环。采用浅比较就需要用户遵循这种范式,不要传入多层结构,这点在官方文档中也有说明。我们这里直接抄一个它的浅比较:

  1. // shallowEqual.js
  2. function is(x, y) {
  3. if (x === y) {
  4. return x !== 0 || y !== 0 || 1 / x === 1 / y
  5. } else {
  6. return x !== x && y !== y
  7. }
  8. }
  9. export default function shallowEqual(objA, objB) {
  10. if (is(objA, objB)) return true
  11. if (
  12. typeof objA !== 'object' ||
  13. objA === null ||
  14. typeof objB !== 'object' ||
  15. objB === null
  16. ) {
  17. return false
  18. }
  19. const keysA = Object.keys(objA)
  20. const keysB = Object.keys(objB)
  21. if (keysA.length !== keysB.length) return false
  22. for (let i = 0; i < keysA.length; i++) {
  23. if (
  24. !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
  25. !is(objA[keysA[i]], objB[keysA[i]])
  26. ) {
  27. return false
  28. }
  29. }
  30. return true
  31. }

在回调里面检测参数变化:

  1. // 注册回调
  2. store.subscribe(() => {
  3. const newChildProps = childPropsSelector(store, wrapperProps);
  4. // 如果参数变了,记录新的值到lastChildProps上
  5. // 并且强制更新当前组件
  6. if(!shallowEqual(newChildProps, lastChildProps.current)) {
  7. lastChildProps.current = newChildProps;
  8. // 需要一个API来强制更新当前组件
  9. }
  10. });

强制更新

要强制更新当前组件的方法不止一个,如果你是用的Class组件,你可以直接this.setState({}),老版的React-Redux就是这么干的。但是新版React-Redux用hook重写了,那我们可以用React提供的useReducer或者useStatehook,React-Redux源码用了useReducer,为了跟他保持一致,我也使用useReducer:

  1. function storeStateUpdatesReducer(count) {
  2. return count + 1;
  3. }
  4. // ConnectFunction里面
  5. function ConnectFunction(props) {
  6. // ... 前面省略n行代码 ...
  7. // 使用useReducer触发强制更新
  8. const [
  9. ,
  10. forceComponentUpdateDispatch
  11. ] = useReducer(storeStateUpdatesReducer, 0);
  12. // 注册回调
  13. store.subscribe(() => {
  14. const newChildProps = childPropsSelector(store, wrapperProps);
  15. if(!shallowEqual(newChildProps, lastChildProps.current)) {
  16. lastChildProps.current = newChildProps;
  17. forceComponentUpdateDispatch();
  18. }
  19. });
  20. // ... 后面省略n行代码 ...
  21. }

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

  1. Subscription负责处理所有的state变化的回调
  2. 如果当前连接redux的组件是第一个连接redux的组件,也就是说他是连接redux的根组件,他的state回调直接注册到redux store;同时新建一个Subscription实例subscription通过context传递给子级。
  3. 如果当前连接redux的组件不是连接redux的根组件,也就是说他上面有组件已经注册到redux store了,那么他可以拿到上面通过context传下来的subscription,源码里面这个变量叫parentSub,那当前组件的更新回调就注册到parentSub上。同时再新建一个Subscription实例,替代context上的subscription,继续往下传,也就是说他的子组件的回调会注册到当前subscription上。
  4. state变化了,根组件注册到redux store上的回调会执行更新根组件,同时根组件需要手动执行子组件的回调,子组件回调执行会触发子组件更新,然后子组件再执行自己subscription上注册的回调,触发孙子组件更新,孙子组件再调用注册到自己subscription上的回调。。。这样就实现了从根组件开始,一层一层更新子组件的目的,保证了父->子这样的更新顺序。

Subscription

所以我们先新建一个Subscription类:

  1. export default class Subscription {
  2. constructor(store, parentSub) {
  3. this.store = store
  4. this.parentSub = parentSub
  5. this.listeners = []; // 源码listeners是用链表实现的,我这里简单处理,直接数组了
  6. this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
  7. }
  8. // 子组件注册回调到Subscription上
  9. addNestedSub(listener) {
  10. this.listeners.push(listener)
  11. }
  12. // 执行子组件的回调
  13. notifyNestedSubs() {
  14. const length = this.listeners.length;
  15. for(let i = 0; i < length; i++) {
  16. const callback = this.listeners[i];
  17. callback();
  18. }
  19. }
  20. // 回调函数的包装
  21. handleChangeWrapper() {
  22. if (this.onStateChange) {
  23. this.onStateChange()
  24. }
  25. }
  26. // 注册回调的函数
  27. // 如果parentSub有值,就将回调注册到parentSub上
  28. // 如果parentSub没值,那当前组件就是根组件,回调注册到redux store上
  29. trySubscribe() {
  30. this.parentSub
  31. ? this.parentSub.addNestedSub(this.handleChangeWrapper)
  32. : this.store.subscribe(this.handleChangeWrapper)
  33. }
  34. }

Subscription对应的源码看这里

改造Provider

然后在我们前面自己实现的React-Redux里面,我们的根组件始终是Provider,所以Provider需要实例化一个Subscription并放到context上,而且每次state更新的时候需要手动调用子组件回调,代码改造如下:

  1. import React, { useMemo, useEffect } from 'react';
  2. import ReactReduxContext from './Context';
  3. import Subscription from './Subscription';
  4. function Provider(props) {
  5. const {store, children} = props;
  6. // 这是要传递的context
  7. // 里面放入store和subscription实例
  8. const contextValue = useMemo(() => {
  9. const subscription = new Subscription(store)
  10. // 注册回调为通知子组件,这样就可以开始层级通知了
  11. subscription.onStateChange = subscription.notifyNestedSubs
  12. return {
  13. store,
  14. subscription
  15. }
  16. }, [store])
  17. // 拿到之前的state值
  18. const previousState = useMemo(() => store.getState(), [store])
  19. // 每次contextValue或者previousState变化的时候
  20. // 用notifyNestedSubs通知子组件
  21. useEffect(() => {
  22. const { subscription } = contextValue;
  23. subscription.trySubscribe()
  24. if (previousState !== store.getState()) {
  25. subscription.notifyNestedSubs()
  26. }
  27. }, [contextValue, previousState])
  28. // 返回ReactReduxContext包裹的组件,传入contextValue
  29. // 里面的内容就直接是children,我们不动他
  30. return (
  31. <ReactReduxContext.Provider value={contextValue}>
  32. {children}
  33. </ReactReduxContext.Provider>
  34. )
  35. }
  36. export default Provider;

改造connect

有了Subscription类,connect就不能直接注册到store了,而是应该注册到父级subscription上,更新的时候除了更新自己还要通知子组件更新。在渲染包裹的组件时,也不能直接渲染了,而是应该再次使用Context.Provider包裹下,传入修改过的contextValue,这个contextValue里面的subscription应该替换为自己的。改造后代码如下:

  1. import React, { useContext, useRef, useLayoutEffect, useReducer } from 'react';
  2. import ReactReduxContext from './Context';
  3. import shallowEqual from './shallowEqual';
  4. import Subscription from './Subscription';
  5. function storeStateUpdatesReducer(count) {
  6. return count + 1;
  7. }
  8. function connect(
  9. mapStateToProps = () => {},
  10. mapDispatchToProps = () => {}
  11. ) {
  12. function childPropsSelector(store, wrapperProps) {
  13. const state = store.getState(); // 拿到state
  14. // 执行mapStateToProps和mapDispatchToProps
  15. const stateProps = mapStateToProps(state);
  16. const dispatchProps = mapDispatchToProps(store.dispatch);
  17. return Object.assign({}, stateProps, dispatchProps, wrapperProps);
  18. }
  19. return function connectHOC(WrappedComponent) {
  20. function ConnectFunction(props) {
  21. const { ...wrapperProps } = props;
  22. const contextValue = useContext(ReactReduxContext);
  23. const { store, subscription: parentSub } = contextValue; // 解构出store和parentSub
  24. const actualChildProps = childPropsSelector(store, wrapperProps);
  25. const lastChildProps = useRef();
  26. useLayoutEffect(() => {
  27. lastChildProps.current = actualChildProps;
  28. }, [actualChildProps]);
  29. const [
  30. ,
  31. forceComponentUpdateDispatch
  32. ] = useReducer(storeStateUpdatesReducer, 0)
  33. // 新建一个subscription实例
  34. const subscription = new Subscription(store, parentSub);
  35. // state回调抽出来成为一个方法
  36. const checkForUpdates = () => {
  37. const newChildProps = childPropsSelector(store, wrapperProps);
  38. // 如果参数变了,记录新的值到lastChildProps上
  39. // 并且强制更新当前组件
  40. if(!shallowEqual(newChildProps, lastChildProps.current)) {
  41. lastChildProps.current = newChildProps;
  42. // 需要一个API来强制更新当前组件
  43. forceComponentUpdateDispatch();
  44. // 然后通知子级更新
  45. subscription.notifyNestedSubs();
  46. }
  47. };
  48. // 使用subscription注册回调
  49. subscription.onStateChange = checkForUpdates;
  50. subscription.trySubscribe();
  51. // 修改传给子级的context
  52. // 将subscription替换为自己的
  53. const overriddenContextValue = {
  54. ...contextValue,
  55. subscription
  56. }
  57. // 渲染WrappedComponent
  58. // 再次使用ReactReduxContext包裹,传入修改过的context
  59. return (
  60. <ReactReduxContext.Provider value={overriddenContextValue}>
  61. <WrappedComponent {...actualChildProps} />
  62. </ReactReduxContext.Provider>
  63. )
  64. }
  65. return ConnectFunction;
  66. }
  67. }
  68. export default connect;

到这里我们的React-Redux就完成了,跑起来的效果跟官方的效果一样,完整代码已经上传GitHub了:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/react-redux

下面我们再来总结下React-Redux的核心原理。

总结

  1. React-Redux是连接ReactRedux的库,同时使用了ReactRedux的API。
  2. React-Redux主要是使用了Reactcontext api来传递Reduxstore
  3. Provider的作用是接收Redux store并将它放到context上传递下去。
  4. connect的作用是从Redux store中选取需要的属性传递给包裹的组件。
  5. connect会自己判断是否需要更新,判断的依据是需要的state是否已经变化了。
  6. connect在判断是否变化的时候使用的是浅比较,也就是只比较一层,所以在mapStateToPropsmapDispatchToProps中不要反回多层嵌套的对象。
  7. 为了解决父组件和子组件各自独立依赖Redux,破坏了React父级->子级的更新流程,React-Redux使用Subscription类自己管理了一套通知流程。
  8. 只有连接到Redux最顶级的组件才会直接注册到Redux store,其他子组件都会注册到最近父组件的subscription实例上。
  9. 通知的时候从根组件开始依次通知自己的子组件,子组件接收到通知的时候,先更新自己再通知自己的子组件。

参考资料

官方文档:https://react-redux.js.org/

GitHub源码:https://github.com/reduxjs/react-redux/

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

作者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges

作者掘金文章汇总:https://juejin.im/post/5e3ffc85518825494e2772fd

手写一个React-Redux,玩转React的Context API的更多相关文章

  1. 放弃antd table,基于React手写一个虚拟滚动的表格

    缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反 ...

  2. 手写一个Redux,深入理解其原理

    Redux可是一个大名鼎鼎的库,很多地方都在用,我也用了几年了,今天这篇文章就是自己来实现一个Redux,以便于深入理解他的原理.我们还是老套路,从基本的用法入手,然后自己实现一个Redux来替代源码 ...

  3. 手写一个虚拟DOM库,彻底让你理解diff算法

    所谓虚拟DOM就是用js对象来描述真实DOM,它相对于原生DOM更加轻量,因为真正的DOM对象附带有非常多的属性,另外配合虚拟DOM的diff算法,能以最少的操作来更新DOM,除此之外,也能让Vue和 ...

  4. 『练手』手写一个独立Json算法 JsonHelper

    背景: > 一直使用 Newtonsoft.Json.dll 也算挺稳定的. > 但这个框架也挺闹心的: > 1.影响编译失败:https://www.cnblogs.com/zih ...

  5. 教你如何使用Java手写一个基于链表的队列

    在上一篇博客[教你如何使用Java手写一个基于数组的队列]中已经介绍了队列,以及Java语言中对队列的实现,对队列不是很了解的可以我上一篇文章.那么,现在就直接进入主题吧. 这篇博客主要讲解的是如何使 ...

  6. 【spring】-- 手写一个最简单的IOC框架

    1.什么是springIOC IOC就是把每一个bean(实体类)与bean(实体了)之间的关系交给第三方容器进行管理. 如果我们手写一个最最简单的IOC,最终效果是怎样呢? xml配置: <b ...

  7. 只会用就out了,手写一个符合规范的Promise

    Promise是什么 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果.从语法上说,Promise 是一个对象,从它可以获取异步操作的消息.Prom ...

  8. 搞定redis面试--Redis的过期策略?手写一个LRU?

    1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...

  9. 利用SpringBoot+Logback手写一个简单的链路追踪

    目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简 ...

随机推荐

  1. Netty 源码解析: Netty 的 ChannelPipeline

    ChannelPipeline和Inbound.Outbound         我想很多读者应该或多或少都有 Netty 中 pipeline 的概念.前面我们说了,使用 Netty 的时候,我们通 ...

  2. 环境篇:呕心沥血@CDH线上调优

    环境篇:呕心沥血@线上调优 为什么出这篇文章? 近期有很多公司开始引入大数据,由于各方资源有限,并不能合理分配服务器资源,和服务器选型,小叶这里将工作中的总结出来,给新入行的小伙伴带个方向,不敢说一定 ...

  3. laravel表单中文错误提示本地化

    <?php return [ /* |-------------------------------------------------------------------------- | V ...

  4. <VCC笔记> 推断操作符,映射和量词

    推断操作符 在VCC中,==>符号意味着逻辑推理结果,即离散数学中的蕴涵关系.P==>Q等价于((!P)||(Q)).是非常常用的操作符. 量词(quantifier) 关于量词,这里指的 ...

  5. 【转载】有人出天价买他的一个文案标题,今天10min教会你……

    目录 1. 套路 1:新闻社论 2. 套路 2:好友对话 3. 套路 3:实用锦囊 4. 套路 4:惊喜优惠 5. 套路 5:意外故事 本文由 简悦 SimpRead 转码, 原文地址 https:/ ...

  6. [原创][开源] SunnyUI.Net 开发日志:UIBarChart 坐标轴刻度取值算法

    _ 在开发UIBarChart的过程中,需要绘制Y轴的刻度,数据作图时,纵横坐标轴刻度范围及刻度值的取法,很大程度上取决于数据的分布.对某一组数据,我们很容易就能知道如何选取这些值才能使图画得漂亮.但 ...

  7. STL sort的comp函数注意事项

    今天写了个题,结果碰巧re了,我眉头一皱发现事情并不简单. 原来我之前的comp写的都是错的. bool cmp(milkman a,milkman b) { return a.price<=b ...

  8. fastjson对String、JSONObject、JSONArray相互转换

    String——>>>JSONArray String st = "[{name:Tim,age:25,sex:male},{name:Tom,age:28,sex:mal ...

  9. 开启PG的归档模式

    目录 开启PG的归档模式 1.查看pg的数据目录 2.查看pg的归档情况 3.查看归档的模式和位置 4.建立归档目录 5.配置归档参数 6.重启pg 7.查看&&切换归档日志 8.查看 ...

  10. Linux nohup命令详解,终端关闭程序依然可以在执行!

    大家好,我是良许. 在工作中,我们很经常跑一个很重要的程序,有时候这个程序需要跑好几个小时,甚至需要几天,这个时候如果我们退出终端,或者网络不好连接中断,那么程序就会被中止.而这个情况肯定不是我们想看 ...