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版本
当前版本文件夹文件
看懂初始版的操作后,我们根据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>
);
}
页面的呈现:
每个按钮都点击一下后:
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(完整版)
完整版的文件夹里的文件
我们目前Test1.jsx页面文件的写法实在是不雅观且还有很多都是重复的写法,基于此问题,我们其实可以将其数据以及修改数据的函数都通过props传递到我们的页面组件中
我们平时使用connect的用法就是:
不难看出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文件中消费该上下文
我们展示页面的组件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文件的写法
3.2 CSR和SSR
3.2.1 使用CSR
app router
(默认组件都是SSR)如果要使用CSR,须在组件文件首行标注"use client"
且只有CSR才能使用react hooks(比如useEffect、useState)
pages router
则像react的普通写法一样,就是CSR,
3.2.2 使用SSR
app router
(默认组件都是SSR)
那么想要获取接口数据要怎么做?我们不用像pages router那样使用getServerSideProps,只需直接将export default的函数改为异步的(需注意是:该函数里无法使用react hooks)
pages router
要使用SSR且获取接口数据,则需要getServerSideProps 函数api