React18 - redux知识点补充

207 阅读4分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第9篇文章,点击查看活动详情

Redux Toolkit的数据不可变性

在React开发中,我们总是会强调数据的不可变性:

  • 无论是类组件中的state,还是redux中管理的state
  • 事实上在整个JavaScript编码过程中,数据的不可变性都是非常重要的

因此我们经常会进行浅拷贝来完成某些操作,但是浅拷贝事实上也是存在问题的

  • 比如过大的对象,进行浅拷贝也会造成性能的浪费
  • 比如浅拷贝后的对象,在深层改变时,依然会对之前的对象产生影响

而Redux Toolkit底层使用了immerjs的一个库来保证数据的不可变性

immer 实现的算法方式是Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享

Persistent Data Structure

自定义connect函数

import { PureComponent } from "react"
import store from '../store'

export function connect(mapStateToProps, mapDispatchToProps) {
	return function(Cpn) {
		return class enhanceCpn extends PureComponent {
			constructor(props) {
				super(props)

				// 调用mapStateToProps方法,返回的就是我们所实际需要使用的store中的数据
				this.state = mapStateToProps(store.getState())
			 }

			 componentDidMount() {
				store.subscribe(() => {
					// 一旦发生更新重新执行mapStateToProps得到最新的state
					this.setState(mapStateToProps(store.getState()))
				})
			 }

			render() {
				let mapState = {}
        let mapDispatch = {}

				if (typeof mapStateToProps === 'function') {
					mapState = mapStateToProps(store.getState())
				}

        if (typeof mapDispatchToProps === 'function') {
          mapDispatch = mapDispatchToProps(store.dispatch)
        }

				return <Cpn { ...this.props } { ...mapState } {...mapDispatch} />
			}
		}
	}
}

虽然这么做依旧模拟实现了connect函数的功能,但是该函数存在一个致命的问题,那就是它需要自己主动引入store

也就是说该文件和store的位置是强耦合的

为此我们还需要模拟实现一些Provider组件

context.js

import { createContext } from 'react'
export const StoreCtx = createContext()

Provider.jsx

import React, { PureComponent } from 'react'
import { StoreCtx } from './context'

export class Provider extends PureComponent {
	render() {
		return (
			<StoreCtx.Provider value={this.props.store}>
				{ this.props.children }
			</StoreCtx.Provider>
		)
	}
}

export default Provider

/src/index.jsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from './utils';
import App from './App';
import store from './store'

const root = ReactDOM.createRoot(document.getElementById('root'))

root.render(
  <React.StrictMode>
    <Provider store={store}>
			<App />
		</Provider>
  </React.StrictMode>
)

使用store中数据的组件

import { PureComponent } from "react"
import { StoreCtx } from './Provider'

export function connect(mapStateToProps, mapDispatchToProps) {
	return function(Cpn) {
		return class enhanceCpn extends PureComponent {
			static contextType = StoreCtx

      // 构造方法有第二个参数,为指定的context
			constructor(props, context) {
        super(props)

        this.unsubscribe = () => {}

        this.state = mapStateToProps(context.getState())
      }

      componentDidMount() {
        this.unsubscribe = this.context.subscribe(() => {
          this.setState(mapStateToProps(this.context.getState()))
        })
      }

      componentWillUnmount() {
        this.unsubscribe()
      }

			render() {
				let mapState = {}
        let mapDispatch = {}

				if (typeof mapStateToProps === 'function') {
					mapState = mapStateToProps(this.context.getState())
				}

        if (typeof mapDispatchToProps === 'function') {
          mapDispatch = mapDispatchToProps(this.context.dispatch)
        }

				return <Cpn { ...this.props } { ...mapState } {...mapDispatch} />
			}
		}
	}
}

中间件

react的中间件本质上就是对dispatch操作进行拦截,在对派发的操作进行二次处理后,再将处理后的结果实际派发给redux

在拦截后,可以进行任何所需要的自定义操作,从而扩展对应的功能

需求: 在dispatch之前,打印一下本次的action对象,dispatch完成之后可以打印一下最新的store state

也就是我们需要将对应的代码插入到redux的某部分,让之后所有的dispatch都可以包含这样的操作

import store from './store'

function log(store) {
	// 该函数本质上就可以看成是一种中间件
	// 实际调用的看上去是dispatch,但是本质上是dispatchAndLog
	// 对原本的dispatch方法扩展了打印日志操作后,在调用原本的dispatch方法
	function dispatchAndLog(action) {
		console.log('正在派发', action)
		next(action)
		console.log('派发结果为', store.getState())
	}

	// 这种行为被称之为monkey patching 猴补丁
	// 也就是篡改现有对象,对整体执行逻辑进行修改
	const next = store.dispatch
	store.dispatch = dispatchAndLog
}

log(store)

所以 redux-thunk 的 核心代码为

function thunk(store) {
	const next = store.dispatch

	function dispatchThunk(action) {
		// 如果传入的是函数,直接调用
    // 并传入修改后的dispatch --- 方便继续派发函数
    // 同时传入store.getState函数 --- 方便调用者获取最新state
    if (typeof action === 'function') {
			action(store.dispatch, store.getState)
		} else {
      // 如果不是函数,是对象形式,直接调用原生派发
			next(action)
		}
	}
	store.dispatch = dispatchThunk
}

thunk(store)

单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并:

// 这里之所以需要多传递一个store
// 是因为redux的applyMiddleWare是直接在createStore的时候传入的参数,其可以直接获取到内部的store
// 而自己封装无法直接获取到对应的store,所以需要显式的传入
function applyMiddleWare(store, ...fns) {
	fns.forEach(fn => fn(store))
}