xiaochengzi6 / Blog

个人博客
GNU Lesser General Public License v2.1
0 stars 0 forks source link

从一个计时器来了解 React-redux 并实现简单版 #31

Open xiaochengzi6 opened 2 years ago

xiaochengzi6 commented 2 years ago

React-Redux将所有组将分为两大类: UI组件和容器组件

一、UI组件满足以下特征:

1.只负责UI的呈现

2.没有状态

3.所有数据都有参数提供

4.不适用Redux的API

UI 组件又称为"纯组件",即它纯函数一样,纯粹由参数决定它的值。

二、容器组件和UI组件完全相反

1.负责管理数据和业务逻辑,不负责 UI 的呈现

2.带有内部状态

3.使用 Redux 的 API

UI组件负责页面的呈现。容器组件负责管理数据和逻辑。

三、Redux负责为UI提供容器组件进行状态的管理。React-Redux 提供connect方法,用于从 UI 组件生成容器组件。

import { connect } from 'react-redux'

const todo = <div> Hello </div>
const VisibleTodo = connect()(todo)

在这里就会生成一个名为VisibleTodo的容器组件,没有往里面传入什么参数所以还没有什么实际作用。

为了让容器组件有容器组件该有的功能需要满足两方面的信息

(1)输入逻辑:外部的数据(即state对象)如何转换为 UI 组件的参数

(2)输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去。

四、这里开始放入计时器的部分代码

class Counter extends Component {
    render() {
        const { value, onIncreaseClick } = this.props
        return (
            <div>
                <span> {value} </span>
                <button onClick={onIncreaseClick}>Increase</button>
            </div>
        )
    }
}

定义了一个Counter的组件 它是一个无状态组件它接收{value, onIncreaseClick}的参数。

//开始定义了一个Reducer
function counter(state={count: 0}, action){
    const count = state.count
    switch(action.type) {
            case: 'increase':
                return{ count: count + 1}
            default: 
                return state
    }
}

connect规定了四个参数常用的是前两个参数

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

(1). 官方定义:[mapStateToProps(state, [ownProps]): stateProps] (Function): 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。如果你省略了这个参数,你的组件将不会监听 Redux store。如果指定了该回调函数中的第二个参数 ownProps,则该参数的值为传递到组件的 props,而且只要组件接收到新的 props,mapStateToProps 也会被调用

理解:可以看到mapStateToProps这个参数必须是个函数,它的作用监听store是否变化,如果变化就是调用这个函数从新计算state的值

function mapstateToProps (state) {
    return{
        value: state.count;
    }
} 

它建立了一个state对象到props的映射关系,1.它接收 state 作为参数,并返回一个对象,这个对象有一个 value 的属性它代表这UI的同名属性,也可以认为它会为UI组件的this.props.value创建一个映射关系{this.props.value === state.count}当数据变动时就会重新调用mapStateToProps函数来重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。

const UI = ({value}) => {
    return (
        <div>
            {value}
        </div>
    )
}
function mapstateToProps (state,ownProps) {
    return{
        value: state.count;
        //这里的 value 就会传入到 UI 中
    }
} 
const Us  = connect(
    mapstateToProps
)(UI)

2.第二个参数将代表容器组件的props对象,使用ownProps作为参数后,如果组件参数变化,也会引起UI组件从新渲染

const mapStateToProps = (state, ownProps) => (
    return {
        active: ownProps.filter === state.visibilityFilter
    }
)

connect方法可以省略mapStateToProps参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。

(2). 官方定义:mapDispatchToProps(dispatch, [ownProps]): dispatchProps(Object or Function): 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作 Redux action creator,对象所定义的方法名将作为属性名;每个方法将返回一个新的函数,函数中dispatch方法会将action creator的返回值作为参数执行。这些属性会被合并到组件的 props 中。

理解: mapDispatchToProps 他可以做一个函数也可以做一个对象,会到dispatchownProps(容器组件的props对象)两个参数。

它定义了哪些用户的操作应该当作 Action,传给 Store。

const mapDispatchToProps = (dispatch,ownProps) => {
    return {
        onIncreaseClick: () => {
            dispatch({
                type: 'ONCHANGE',
                value: 'Incer'
            })
        }
    }
}
//或者
function mapDispatchToProps(dispatch) {
    return {
        onIncreaseClick: () => dispatch(increaseAction)
    }
}

mapDispatchToProps作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。

如果mapDispatchToProps是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。

const mapDispatchToProps = {
    onIncreaseClick: () => {
        type: 'ONCHNGE',
        value: 'Incer'
    }
}
const App = connect(
    mapStateToProps,
    mapDispatchToProps
)(Counter)

问题来了 mapStateToProps如何获得state的呢,mapDispatchToProps是如何传递action的

connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。

一种解决方法是将state对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级将state传下去就很麻烦。

React-Redux 提供Provider组件,可以让容器组件拿到state

官方定义:<Provider store> 使组件层级中的 connect() 方法都能够获得 Redux store。正常情况下,你的根组件应该嵌套在 <Provider> 中才能使用 connect() 方法。

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

这样App所有子组件就可以默认拿到state

完整例子:

import React, { Component } from 'react'
// import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider, connect } from 'react-redux'

class Counter extends Component {
    render() {
        const { value, onIncreaseClick } = this.props
        return (
            <div>
                <span> {value} </span>
                <button onClick={onIncreaseClick}>Increase</button>
            </div>
        )
    }
}

//Reducer
function counter(state = {count: 0}, action) {
    const count = state.count
    switch (action.type) {
        case 'increase':
            return { count: count + 1 }
            default: 
            return state
    }
}

function mapStateToProps(state) {
    return {
        value: state.count
    }
}
function mapDispatchToProps(dispatch) {
    return {
        onIncreaseClick: () => dispatch(increaseAction)
    }
}
const increaseAction = { type: 'increase' }

const App = connect(
    mapStateToProps,
    mapDispatchToProps
)(Counter)

const store = createStore(counter)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

state数据的流向问题?

(默认state)Reducer --1--> (store = creactStore(Reducer) )--2--> \ --3--> (App = connect(...)(Component)) --4-->Component

第一步由 Reducer 构造初始的 state 然后在会通过 第二步 store 保存数据通过 getState() 来获取 state 通过\ 传向 APP

总结一下

在之前的react-redux 的使用模式下你不需要特殊的处理 redux可以直接写在组件中 但在复杂的项目中为了保持项目中组件的纯度 你常常需要创建 store 文件夹用来管理状态

-store
├── actionCreators.js       
├── constants.js
├── index.js
└── reduxcer.js

不用多说这就非常麻烦了,在 constants.js 中编写静态变量 在 actionCreators.js中创建 action Function (生成函数)使用 reduxcer.js用于判断不同的 action 对数据造成的问题

当然到这一步还没有结束,在没有使用 Hook 的情况下每一次编写的组件要明确区分是 容器组件 还是UI组件 这两者最大的不同就是接收数据的问题,前者需要操作数据,后者只是简单的使用传入的数据就行

// 省略 imort
// 容器组件 
function App (){
    return(
        <div></div>
    )
}

const mapDispatchProps = (dispatch) => {
    change1: (data) => {
        dispatch(actionFunction(data))
    }
}

const mapStateProps = (state) => {
    value: state.value1
}

App = connect(mapStateProps, mapDispatchProps)(React.memo(App))
export default App

redux 是以 Reducer 为主的 状态管理工具 (initState, action) => neweState 参数 InitState 是默认的值,那么通过多个 Reducer 组成的

function (state={}, action){
    return {
        value1: (state.value1, action) => newState,
        value2: (state.value2, action) => newState,
        value3: (state.value3, action) => newState,
    }
}

由多个 Reducer 组成的函数通过 const store = createStore(Reducer) 这样的方式生成的 Store ,而 Store 可以理解为数据状态的管理中心,可以使用 dispatch({type: ''})来去修改数据,使用 subscribe 去订阅,由于每次都需要编写 带有type 属性的对象去修改数据,那么通常使用 actionCreate()函数来去返回一个对象 { type: 'VALUE', ...} 方便调用更改数据 每一次都需要编写 type 那么将常用的字段提取出来放入一个单独的文件中,那么这就是 redux 简单的使用。

手写一个简单的 React-redux

react-redux 在 redux 外层包裹了一层 react 组件使其能够方便的 react 使用 这里主要看一下核心 API

首先是在全局的根目录下导入 store

// ...
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

简单的看一下做了什么事情 由于和原生组件并不一样这里就简写为了了解其原理:

1、为了能够在全局中使用 store 数据这里使用到了 [context ](https://github.com/xiaochengzi6/Blog/issues/12)

const ReactReduxContext = React.createContext()

2、通过 <ReactReduxContext.provider value={}/> 组件去传递数据并订阅 context

3、之后开始编写 Provider 组件

import {ReactReduxContext} from './ReactReduxContext'
export default function Provider (props){
    const {store, children} = props

    const contextValue = { store }

    return (
        <ReactReduxContext.Provider value={contextValue}>
            {children}
        </ReactReduxContext.Provider>
    )
}

provider 组件做的内容就是转发一下 store 以及将子组件放入合适的位置

4、react-redux 将组件分为两大类 容器组件和UI组件 容器组件需要高阶组件 connect 去包裹 如此才能将 store 数据传入 组件的 props 中,将用户的动作通过 dispatch 发出

import {ReactReduxContext} from './ReactReduxContext'

export default function connect(mapStateToProps, mapDispatchToProps) {
  return function Componenct(WrappendComponent) {
    return function ConnectFunction(props) {
      const {...wrapperProps} = props
      // 获取 store 数据
      const {store} = useContext(ReactReduxContext)
      const states = store.getState()

      const stateProps = mapStateToProps(state)
      const dispatchProps = mapDispatchToProps(store.dispatch)

      const propsValues = Object.assign({}, stateProps, dispatchProps, wrapperProps)     
      return <WrappendComponent {...propsValues} />
    }
  }
}

5、这里虽然是通过 dispatch 进行更新数据 虽然数据更新但其组件并不没有随之发生改变 所以这里需要去监听数据的变动以跟新组件

1、检查当数据 state 发生变化的时候这里要去检查传给组件的 state (参数)是否一致【为什么是参数? 因为state 会和参数 props、 stateProps 以及dispatchProps 合并在一起 但最终要检查的是组合在一起的 props】

2、当参数发生改变就要重新渲染组件

将 connect 数据的获取抽离出成一个函数

function childPropsSelector (state, wrapperProps){
    const states = store.getState()

    const stateProps = mapStateToProps(state)
    const dispatchProps = mapDispatchToProps(store.dispatch)

    return Object.assign({}, stateProps, dispatchProps, wrapperProps)
}

6、在检查参数的时候 需要获得上次的渲染参数 然后和这次的渲染参数进行对比, redux 采用的是浅比较,如果使用 immer 库对数据进行包裹这样的比较会比较好,而且也不会发生深比较的循环引用问题

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
}

修改 connect 函数的内容

import {ReactReduxContext} from './ReactReduxContext'
import {shallowEqual} from './shallowEqual'

export default function connect(mapStateToProps, mapDispatchToProps) {
  return function Componenct(WrappendComponent) {
    function childPropsSelector(state, wrapperProps) {
      const states = store.getState()

      const stateProps = mapStateToProps(state)
      const dispatchProps = mapDispatchToProps(store.dispatch)

      return Object.assign({}, stateProps, dispatchProps, wrapperProps)
    }

    return function ConnectFunction(props) {
      const { ...wrapperProps } = props
      // 获取 store 数据
      const { store } = useContext(ReactReduxContext)

      const propsValues = childPropsSelector(store, wrapperProps)
      const lastChildProps = useRef()

      // 保存上一次的 参数
      useEffect(() => [(lastChildProps.current = propsValues)], [])

      // 监听 store 是否发生变化
      store.subscribe(()=>{
        const newChildProps = childPropsSelector(store, wrapperProps)
        if(!shallowEqual(newChildProps, lastChildProps)){
          lastChildProps.current = newChildProps
        }
      })

      return <WrappendComponent {...propsValues} />
    }
  }
}

7、数据发生改变并且也监听到了 这个时候就要去强制更新 使用 useRedux 去派发 dispatch 从而让组件强制更新

function storeStateUpdatesReducer(count){
    return count + 1
}

export default function connect(mapStateToProps, mapDispatchToProps) {
  //...
    const [, forceComponentUpdataDiaptch] = useReducer(storeStateUpdatesReducer, 0)
    store.subscribe(()=>{
        const newChildProps = childPropsSelector(store, wrapperProps)
        if(!shallowEqual(newChildProps, lastchildProps)){
            lastChildProps.current = newChildProps
            // 这里去强制组件更新
            forceComponentUpdataDiaptch()
        }
    })
    // 。。。
}

8、当然这里还涉及到了更新先后的问题,父组件通过 connect 获取到 redux 中的 store 进行 dispatch 改变数据,子组件也是从 redux 取出 store 从而更新 数据的更新都是派发 dispatch 每一次派发 dispatch 都会保持上一次的数据快照,如果是上面的类型显示是两次单独的数据更新【分别从 redux 中取出 store 数据】显然没有这样的先后关系 可能会引发问题,所以应该保持上一次的 store 在上次的 store 改变之后去更新组件

export class Subscription{
    constructor(store, parentSub){
        this.store = store
        this.parentSub = patentSub
        this.handleChangeWrapper =  this.handleChangeWrapper.bind(this)
    }

    // 添加监听器
    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()
        }
    }

    // 包装回调函数--- 目的就是为了 在 this.store 下去调用函数 
    handleChangeWrapper(){
        if(this.onStateChange){
            this.onStateChange()
        }
    }

    trySubScribe(){
        this.parentSub ? this.parentSub.addNestedSub(this.handleChangeWrapper) :  this.store.subscribe(this.handleChangeWrapper)
    }
}

9、通过 Subscription 来去维护这样的关系 在整个项目中 只有 容器组件 或者说 被 connect()包裹的组件才有机会去使用 diapatch 更新数据 每一次的更新数据后都会改变 store 然后组件更新开始于从父级层层往下直到目前的容器组件 而每一次的更新【派发 dispatch】又都会从上一次的 store 中取到最新值,确保更新顺序

import Subscription from './Subscription';

export default function Provider(props){
    const {store, childer} = props

    const contextValue = useMemo(()=>{
        const subscript = new Subscript(store)

        // 回调事件--可以理解为数据发生改变时候要触发的事件也就是 store.subscript(listerne) 中的监听事件 listerne
        subscript.onStateChange = subscript.notifyNestedSubs

        return {
            store,
            subscript
        }
    }, [store])

    const previousState = useMemo(()=> store.getState(0) ), [store])

    useEffect(()=>{
        const {subscription} = contextValue
        // 这里去往 subscript 中添加 onStateChange 函数
        subscript.trySubscribe()

        if(previousState !== store.getState()){
            // 这里发现 store 并不是之前的数据了 就会去调用之前存储的监听事件
            subscription.notifyNestedSubs()
        }
    }, [contextValue, previousState])

    return (
        <ReactReduxContext.Provider value={contextValue}>
            {children}
        </ReactReduxContext.Provider>
    )
}

之后再修改一下 connect 就可以了

import React, { useContext, useRef, useLayoutEffect, useReducer } from 'react';
import ReactReduxContext from './Context';
import shallowEqual from './shallowEqual';
import Subscription from './Subscription

export default function connect(mapStateToProps, mapDispatchToProps){
    return function connectHoc(WrappredComponent){

        function childPropsSelector(store, wrapperProps){
            //...
        }

        return function connectFunctioon(props){
            const {...wrappedn} = props
            const contextValue = useContext(ReactReduxContext)

            const { store, subscription: parentSub } = contextValue

            const actualChildProps = childPropsSelector(store, wrapperProps)

            // 保存上一次的 props
            const lastChildProps = useRef()
            useLayoutEffect(()=>{
                lastChildProps.currnt = actualChildProps
            }, [actualChildProps])

            // 创建一个 reducer 用来强制更新组件
            const [,forceComponentUpdateDispatch] = useReducer(storeStateUpdatesReducer, 0)

            // 创建 subscription 实例用于确保组件更新的顺序
            const subscription = new Subscript(store, parentSub)

            const checkForUpdates = () =>{
                const newChildProps = childPropsSelector(store, wrapperProps)

                if(!shallowEqual(newChildProps, lastChildProps)){
                    lastChildProps.current = newChildProps
                    // 强制更新
                    forceComponentUpdateDispatch()

                    // 通知 调用其子级为其添加的监听事件
                    subscription.notifyNestedSubs();
                }

            }

            // 注册这次的监听事件
            subscript.onStateChange = checkForUpdates
            // 将其放入上一次的 store 中
            subscription.trySubscribe()
        }
    }
}

参考文章

[写的非常不错的 文章主要是去学习他的--手写一个React-Redux,玩转React的Context API][https://juejin.cn/post/6847902222756347911#comment]