React的一些相对进阶点(手写简版redux、Next)(16)

273 阅读5分钟

1 手写简版redux

首先需要明白状态管理的思路,我们所管理的状态是怎样的?

  • 数据的生命周期是什么?(js runtime,刷新就没了)
  • 数据的作用范围是什么?(全局共享数据状态)
  • 数据应该存放在哪里?(为了不被GC,我们应该存放在Global上,且使用闭包)。
  • 我修改这个数据后,相关方要能感知到
  • 修改状态,会触发UI更新(比如react的useState,在setState时会触发页面更新)

1.1 初始版(UNSAFE_CHANGE)

UNSAFE_CHANGE: 代表每次修改值时都是不安全的,且这种操作很繁琐

要显示的页面(页面与store在同一文件夹下)

index.jsx

import React, { useEffect, useState } from "react";
import { store } from "./data";

export default function DataIndex() {
  return (
    <div>
      <Child1 />
      <Child2 />
    </div>
  );
}

const Child1 = () => {
  const [count, setCount] = useState(2);

  useEffect(() => {
    // 初始值 同步
    setCount(() => store.getState()?.count);
    store.subcribe(() => {
      setCount(() => store.getState()?.count);
    });
  }, []);

  return <div>Child1 count: {count}</div>;
};

const Child2 = () => {
  return (
    <div>
      Child2
      <button
        onClick={() =>
          store.changeState({ count: store.getState()?.count + 1 })
        }
      >
        add
      </button>
    </div>
  );
};

初始版的redux

data.js

function createStore(initState = {}) {
    let state = initState
    // 我们希望,订阅了这个Store 的函数handler ,在 state 改变的时候,handler 要执行一下
    let listener = []

    function getState() {
        return state
    }
    function subcribe(handler) { // 这里应该是 subscribe ,订阅的意思
        listener.push(handler)
    }
    function changeState(data) {
        state = data
        listener.forEach(h => h())
    }
    return { getState, subcribe, changeState }
}

const initState = {
    count: 0
}

export const store = createStore(initState)

1.1.1 SAFE_CHANGE

基于初始版上,为了让我们每次修改值都是可控的,安全的,我们加上action和reducer

那我们的store就变成

data.js

function createStore(initState = {}) {
    let state = initState
    let listener = []

    function getState() {
        return state
    }
    function subcribe(handler) {
        listener.push(handler)
    }
    function changeState(data) {
        state = data
        listener.forEach(h => h())
    }

    // safe_change
    function changeStateByAction(action) {
        state = reducer(state, action)
        listener.forEach(h => h())
    }

    return { getState, subcribe, changeState, changeStateByAction }
}

const initState = {
    count: 0
}

export const ADD_ACTION = {
    type: 'ADD_ACTION',
    payload: 1
}

const reducer = (state, action) => {
    switch (action.type) {
        case 'ADD_ACTION':
            // ...state是防止state中不止有count属性
            return {...state, count: state.count + action.payload }
        default:
            return state
    }
}

export const store = createStore(initState)

对应的页面上的修改只需要通过传入action,使之变为可控的

<button onClick={() => store.changeStateByAction(ADD_ACTION)}>
    safe_add
</button>

1.2 简版Redux

编写顺序: createStore ==>> action、reducer ==>> combineReducer ==>> connect

1.2.1 无combineReducer、connect版本

当前版本文件夹文件

image.png

看懂初始版的操作后,我们根据Redux的使用写出自己的状态管理

新建一个redux.js文件,照搬初始版的createStore,替换变量名称,更像redux源码写法

export const createStore = (reducer, initState) => {
    const listeners = []
    let state = initState

    function subscribe(handler) {
        listeners.push(handler)
    }

    function dispatch(action) {
        const currentState = reducer(state, action)
        state = currentState
        listeners.forEach(h => h())
    }

    function getState() {
        return state
    }

    return {
        subscribe,
        getState,
        dispatch,
    }
}

新建index.js文件,类似于状态管理的中央处理器(store)

import { ADD_AGE, ADD_COUNT, CHANGE_NAME } from "./action"
import { createStore } from "./redux"

const initState = {
    counter: {
        count: 0
    },
    info: {
        name: 'xxx',
        age: 18
    }
}

const reducer = (state, action) => {
    switch (action.type) {
        case ADD_COUNT.type:
            return {
                ...state,
                counter: {
                    ...state.counter,
                    count: state.counter.count + action.payload
                }
            }
        case ADD_AGE.type:
            return {
                ...state,
                info: {
                    ...state.info,
                    age: state.info.age + action.payload
                }
            }
        case CHANGE_NAME.type:
            return {
                ...state,
                info: {
                    ...state.info,
                    name: action.payload
                }
            }
        default:
            return state
    }
}

export const store = createStore(reducer, initState)

action集中于一个文件处理,便于防止变量写错(多处使用到的变量,建议集中式处理)

export const ADD_COUNT = {
    type: 'ADD_COUNT',
    payload: 2
}
export const ADD_AGE = {
    type: 'ADD_AGE',
    payload: 1
}
export const CHANGE_NAME = {
    type: 'CHANGE_NAME',
    payload: `LuyolG`
}

当前展示的页面文件

import React, { useEffect, useState } from "react";
import { ADD_AGE, ADD_COUNT, CHANGE_NAME } from "./action";
import { store } from "./index";

export default function Test1() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("xxx");
  const [age, setAge] = useState(0);

  useEffect(() => {
    setCount(() => store.getState()?.counter?.count);
    setName(() => store.getState()?.info?.name);
    setAge(() => store.getState()?.info?.age);
    store.subscribe(() => {
      setCount(() => store.getState()?.counter?.count);
      setName(() => store.getState()?.info?.name);
      setAge(() => store.getState()?.info?.age);
    });
  }, []);

  return (
    <div>
      <div>
        <h5>count: {count}</h5>
        <button onClick={() => store.dispatch(ADD_COUNT)}>ADD_COUNT</button>
      </div>
      <div>
        <h5>name: {name}</h5>
        <button onClick={() => store.dispatch(CHANGE_NAME)}>CHANGE_NAME</button>
      </div>
      <div>
        <h5>age: {age}</h5>
        <button onClick={() => store.dispatch(ADD_AGE)}>ADD_AGE</button>
      </div>
    </div>
  );
}

页面的呈现:

image.png

每个按钮都点击一下后:

image.png

1.2.2 combineReducer

让我的reducer函数能够拆分出来独立处理自己的状态,比如counterReducer处理counter数据,infoReducer处理info数据,我们只需写一个combineReducer函数将其reducer作一个整合即可

我们只需修改index.js文件,(后面为了让文件更分工明确,将combineReducer移至redux.js文件中)

import { ADD_AGE, ADD_COUNT, CHANGE_NAME } from "./action"
import { createStore } from "./redux"

const initState = {
    counter: {
        count: 0
    },
    info: {
        name: 'xxx',
        age: 18
    }
}

const counterReducer = (state, action) => {
    switch (action.type) {
        case ADD_COUNT.type:
            return {
                ...state,
                count: state.count + action.payload
            }
        default:
            return state
    }
}
const infoReducer = (state, action) => {
    switch (action.type) {
        case ADD_AGE.type:
            return {
                ...state,
                age: state.age + action.payload
            }
        case CHANGE_NAME.type:
            return {
                ...state,
                name: action.payload
            }
        default:
            return state
    }
}
const combineReducer = (reducers) => {
    const keys = Object.keys(reducers) // counter info
    return function (state, action) {
        const nextState = {}
        keys.forEach((key) => {
            const reducer = reducers[key] // counterReducer infoReducer
            const preV = state[key]
            const nextV = reducer(preV, action)
            nextState[key] = nextV
        })
        return nextState
    }
}

const reducer = combineReducer({
    counter: counterReducer,
    info: infoReducer
})

export const store = createStore(reducer, initState)

1.2.3 connect(完整版)

完整版的文件夹里的文件

image.png

我们目前Test1.jsx页面文件的写法实在是不雅观且还有很多都是重复的写法,基于此问题,我们其实可以将其数据以及修改数据的函数都通过props传递到我们的页面组件中

我们平时使用connect的用法就是:

image.png

不难看出connect函数实则还是返回一个函数,且入参为我们的页面组件component

那我们新建一个connect.js文件编写该函数

import { useContext, useEffect, useState } from "react"
import ReduxContext from "./context"

export default (mapStateToProps, mapDispatchToProps) => (MyComponent) => {
    return function (props) {
        /**
         * context没有响应式处理,我们需要加上对应的subscribe
         */
        const _store = useContext(ReduxContext)
        const [Bool, setBool] = useState(0)
        const forceUpdate = () => {
            setBool(_ => Math.random())
        }

        useEffect(() => {
            _store.subscribe(forceUpdate)
        }, [])

        return <ReduxContext.Consumer>
            {(store) => {
                return <MyComponent
                    {...props}
                    {...mapStateToProps(store.getState())}
                    {...mapDispatchToProps(store.dispatch)}
                />
            }}
        </ReduxContext.Consumer>
    }
}

我们是使用react的createContext创建上下文,便于在所有组件中传递数据,那我们新建个context文件写法就是

import { createContext } from "react";

const ReduxContext = createContext({})

export default ReduxContext

然后在react的入口文件中传递该上下文,我们就能在connect文件中消费该上下文

image.png

我们展示页面的组件Test2.jsx

import { ADD_AGE, ADD_COUNT, CHANGE_NAME } from "./action";
import connect from "./connect";

function Test2({
  counter,
  info,
  handleAddCount,
  handleChangeName,
  handleAddAge,
}) {
  return (
    <div>
      <div>
        <h5>count: {counter?.count}</h5>
        <button onClick={() => handleAddCount()}>ADD_COUNT</button>
      </div>
      <div>
        <h5>name: {info?.name}</h5>
        <button onClick={() => handleChangeName()}>CHANGE_NAME</button>
      </div>
      <div>
        <h5>age: {info?.age}</h5>
        <button onClick={() => handleAddAge()}>ADD_AGE</button>
      </div>
    </div>
  );
}

const mapStateToProps = (state) => ({
  counter: state.counter,
  info: state.info,
});
const mapDispatchToProps = (dispatch) => ({
  handleAddCount: () => dispatch(ADD_COUNT),
  handleChangeName: () => dispatch(CHANGE_NAME),
  handleAddAge: () => dispatch(ADD_AGE),
});
export default connect(mapStateToProps, mapDispatchToProps)(Test2);

至此,简版redux完结!(页面效果同上方一样)

2.router(未开始)

3.Next(page 与 app 的一些异同点)

3.1 路由方面的使用

约定式路由:默认的路由文件名字由index改为page(比如src/pages/news/index.tsx ==>> src/app/news/page.tsx)

且app router,编写组件时最好遵循模块化开发,统一都写成文件夹,然后文件夹下有page文件的写法

image.png

3.2 CSR和SSR

3.2.1 使用CSR

app router(默认组件都是SSR)如果要使用CSR,须在组件文件首行标注"use client"

image.png

且只有CSR才能使用react hooks(比如useEffect、useState)

pages router 则像react的普通写法一样,就是CSR,

3.2.2 使用SSR

app router(默认组件都是SSR)

那么想要获取接口数据要怎么做?我们不用像pages router那样使用getServerSideProps,只需直接将export default的函数改为异步的(需注意是:该函数里无法使用react hooks)

image.png

pages router要使用SSR且获取接口数据,则需要getServerSideProps 函数api

image.png