React 生态周边解读

1,139 阅读14分钟

一、 Redux

1. Flux框架

Flux 不是一个具体的框架,而是一套由 FackBook 团队提出的应用架构。这套架构约束的是应用处理数据的模式。

Flux 将每个应用都划分为四部分:View、Store、Action、Dispatcher

  • view 视图层:表示用户界面,可以是任何形式的产物
  • Action 动作:视图层发出的消息,会触发应用状态改变
  • Dispatcher 派发器:负责对 action 进行分发
  • Store 数据层:存储应用状态的仓库,同时具备修改状态的逻辑

image.png

Flux 最核心的原理是严格的单向数据流,Redux 是 Flux 思想的产物,虽然没有完全实现 Flux,但是却保留了单向数据流的特点。

2. Redux

1. 功能

  • 组件获取状态:任何组件都可以以约定的方式从 Store 读取全局状态

  • 组件修改状态:任何组件都可以通过合理的派发 Action 来修改全局状态

image.png

2. 核心元素

  • Store:存储应用程序的全局状态。在 Redux 中,整个应用程序的状态存储在一个单一的对象树中,并且这个状态树只存在于唯一的 Store 中。
  • Actions:当应用程序的状态需要更新时,会发送一个 Action。Action 是一个描述“发生了什么”的普通对象。
  • Reducers:它们是处理状态更新的纯函数。Reducer 接收 state 和一个 action 作为参数,并返回一个新的state。

image.png

3. reducer 管理

reducer 本质上是一个函数,负责响应 action 并修改数据,根据 previousState 参数和 action 行为计算出新的 newState。

reducer(previousState,action)=>{
  if(previousState){
      ... ...
      return newState
  }else{
      ... ...
  }
}
  • 拆分:根据独立的模块拆分出单独的 reducer

  • 合并:创建 Store 时合并 reducer

  • 统一管理 actionType

4. Action Type管理

整个 store 中的 action type 值不能重复,需要达到全局唯一性

  • 命名空间(Namespacing)

为每个模块或功能区分配一个独立的命名空间,以确保它们的 action type 常量不会发生冲突。例如将模块名作为前缀,USER_FETCH_REQUESTED

  • 统一文件管理

将所有模块的 action type 常量定义放在一个统一的文件中,以避免不同文件之间的命名冲突

  • 使用工具库

使用工具库来自动化处理 action type 常量的生成。一些常用的工具库有 redux-actions 和 redux-toolkit,它们简化 Redux 开发的功能,可以自动生成唯一且不会重复的 action type 常量

  • 唯一性检查

可以编写自定义的工具函数或脚本,在构建或开发过程中对 action type 常量进行唯一性检查

5. 三大原则

  • 单一数据源:在 Redux 中,整个应用的状态被存储在一个对象树中,并且这个对象树只存在于唯一的一个存储中。这样的设计不仅使得状态的管理变得更加可预测,而且也便于开发者进行状态追踪和调试。

    • 保证数据一致性
    • 简化数据管理
    • 便于调试
  • 状态是只读的:唯一改变状态的方式是触发一个动作(action),动作是一个用于描述已发生事件的普通对象。这种方式确保了视图或网络回调不能直接修改状态,而是必须通过分发动作的方式,保证了数据流的清晰和一致性。

  • 使用纯函数来执行修改:为了描述动作如何改变状态树,你需要编写 reducers。Reducer 是一种特殊的函数,根据旧的状态和一个动作,返回一个新的状态。关键在于,reducers 必须是纯函数,这意味着它们应该只计算下一个状态,而不改变原始状态。

6. 项目中使用

import { Provider } from 'react-redux'
import store from './store'
const App = (
  <Provider store={store}>
    <Home />
  </Provider> 

利用 Provider 从最外层封装整个应用,向 connect 传递 store,使 Provider 内部的所有组件都可以使用 store。

connect 连接器,连接 React 和 Redux。本质上是一个高阶组件,接收一个组件作为参数。

function connect(mapStateToProps, mapDispatchToProps, mergeProps, options={})(App)

参数说明:

  • mapStateToProps:从 Redux 中获取部分状态作为 props 传递给当前组件

  • mapDispatchToProps:将 actionCreator 与 dispatch 绑定在一起,并将其作为 props 传递给当前组件

  • mergeProps:对接收到的所有 props 进行分类,命名和重组

  • options:配置项,一般包含两方面

    • pure:true 表示 connect 在 shouldComponentUpdate 中使用浅比较
    • withRef:true 表示 connect 保存对装饰组件的 refs 引用,可以通过 getWrappedInstance 获取最终的 DOM 节点
import { connect } from 'react-redux';

function Myapp(props) {
    const {num,add} = props
    return (
        <div>
            <div>我的值:{num}</div>
            <button onClick={()=>{add(1)}}>+1</button>
        </div>
    )
}
// 接收props
const mapStateToProps= (state) => {
    return {
        num: state.getNum.num,
    }
}
// 接收props中的action
const mapDispatchToProps= (dispath) =>{
    return {
        add:(id)=>dispath({type:'ADD'})
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Myapp)

页面通过点击按钮,改变 store 中的一个 state,其完整的数据流动如下所示:

image.png

3. 工作原理

1. createStore

createStore 方法是在使用 Redux 时最先调用的方法,是整个流程的入口。同时也是 Redux 中最核心的 API。

image.png

通过 createStore 方法创建 store 对象

const store = createStore(reducers);

createStore 本身包含四个方法:

  • getState:获取当前 store 中的状态
  • dispatch(action):分发一个 action,并返回这个 action,这是唯一能改变 store 中数据的方式
  • subscribe(listener):注册一个监听者,在 store 发生变化时调用
  • replaceReducer(nextReducer):更新当前 store 里的 reducer,一般只会在开发模式中调用此方法
export function createStore(reducers, initialState, enhancer){} 

createStore 利用前两个参数进行 createStore 的调用。

createStore 利用 enhancer 对 createStore 能力做增强,并返回增强后的 createStore(利用高阶函数的原理)

export function createStore(reducers,initialState,enhancer){
    // 认为没有传默认state值
    if(typeof initialState === 'function' && typeof enhancer === 'undefined'){
        enhancer = initialState;
        initialState = undefined;
    }
    if(typeof enhancer !== 'undefined'){
        if(typeof enhancer !== 'function'){
            throw new Error('错误')
        }
        // 高阶函数原理
        return enhancer(createStore)(reducers,initialState)
    }
}

接收 createStore 作为参数传入 ,对 createStore 的能力做增强,并返回 createStore,然后再将 reducers,initialState 作为参数传递给增强后的 createStore,最终得到 store。

2. dispatch

dispatch action,主要工作即“将 Redux 核心三要素串联起来”。

image.png

通过上锁,避免套娃式的 dispatch

try{
    isDispatching = true;
    currentState = currentReducer(currentState,action);
}finally{
    isDispatching = false;
}

Redux 完整流程如下:

image.png

3. subscribe

在 store 对象创建成功后,通过调用 store.subscribe 注册监听函数。

当 dispatch action 发生时,Redux 会在 reducer 执行完毕后,将 listeners 数组中的监听函数逐个执行。

image.png

  • nextListener:订阅、触发、解除订阅操作的均是 nextListener
  • currentListener:记录当前正在工作的 listeners 数组的引用,将它与可能发生改变的 nextListeners 区分开来,以确保监听函数在执行过程中的稳定性

4. 中间件

const store = createStore(
    reducer,
    initial_state,
    applyMiddleWare(middleWare1,middleWare2,...)
)

applyMiddleWare 的作用就是向 store 中注入中间件(enhancer 包装 createStore)。

中间件是指可以增强 createStore 的工具,在 Redux 中所有的更新都是同步执行的,如果想要异步处理更新流程,则需要借助中间件。

中间件的工作流程图:

image.png

中间件的执行时机:action 分发之后、reducer 执行之前。

中间件的执行前提:利用 applyMiddleWare 对 dispatch 函数进行改写,使其在触发 reducer 之前,会先执行对 redux 中间件的链式调用。

1. Redux Thunk

Redux Thunk 是一个异步 Action 的中间件,Redux Thunk 允许在 Redux action 中返回函数而不仅仅是纯对象,从而可以在 action 中进行异步操作,并在操作完成后分发一个新的 action。

第一步:下载插件npm install redux-thunk

第二步:使用插件

const store = createStore(
    reducer,
    initial_state,
    applyMiddleWare(redux-thunk)
)

自定义一个异步 action,如下所示:

// 定义异步 action
const fetchData = () => {
  return (dispatch) => {
    dispatch({ type: 'FETCH_DATA_START' });
    // 发起异步操作
    fetch('https://api.example.com/data')
      .then((response) => response.json())
      .then((data) => {
        // 异步操作成功后,分发新的 action
        dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data });
      })
      .catch((error) => {
        // 异步操作失败后,分发新的 action
        dispatch({ type: 'FETCH_DATA_ERROR', payload: error });
      });
  };
};

// 在组件中调用异步 action
dispatch(fetchData());

fetchData 是一个异步 action,它返回一个函数,函数中包含异步操作,当异步操作完成后,新的 action(成功或失败)将被分发到 Redux store。

适用场景:Redux Thunk 适用于简单的异步操作,比如发送 Ajax 请求或者执行定时器等

4.2 Redux Saga

Redux Saga 是另一个常用的 Redux 中间件,它使用生成器函数(Generators)的方式来处理异步操作,提供了一种声明式的方法来管理和处理副作用。

function* fetchData() {
  try {
    yield put({ type: 'FETCH_DATA_START' });
    const response = yield call(fetch, 'https://api.example.com/data');
    const data = yield response.json();
    yield put({ type: 'FETCH_DATA_SUCCESS', payload: data });
  } catch (error) {
    yield put({ type: 'FETCH_DATA_ERROR', payload: error });
  }
}

适用场景:Redux Saga 适用于复杂的异步操作场景,如多个连续的异步操作、长轮询、并发请求等

二、 React-router

1. 路由模式

React Router 与 React 的很多特性保持一致,在 React 中,组件就是一个方法,props 作为参数传入方法,当 props 更新时会触发方法的执行,从而重新绘制 View。在React Router 中,同样可以把 Router 看作是一个方法,location 作为参数传入方法,返回的结果同样是一个 View。

当用户在任何路由下刷新页面,浏览器都可以根据当前URL进行资源定位,不会出现白屏问题。

  • hash 模式:改变 URL 中 # 后面的部分,实现组件的切换
// 感知hash变化
window.addEventListener('hashChange',functionn(event){
    ...
},false)
  • history 模式:改变整个 URL,实现组件切换
// 追加记录
history.pushState(data[,title][,url]);
// 修改记录
history.replaceState(data[,title][,url])
// 感知state变化
window.addEventListener('popState',functionn(event){
    ...
},false)

1. 声明式路由

ReactRouter 继承了声明式编程特点,允许使用 JSX 标签书写路由

// 当前页面url为/login时,React会渲染Login这个组件
import { Router,Route,browserHistory} from 'react-router';

const routes = (
    <Router history={browserHistory}>
        <Route path="/login" component={<Login />} />
    </Router>
)

2. 嵌套路由及路径匹配

在许多单页应用中,嵌套路由是最常见的路由模式。

例如页面有的顶栏、侧边栏、列表,点击具体列表卡片跳转时顶栏和侧边栏需要复用

import { Router,Route,IndexRoute,browserHistory} from 'react-router';

const routes = (
    <Router history={browserHistory}>
        <Route path="/" component={<App />}>
            <IndexRoute component={< List/>} />
            <Route path="/list/:listID" component={<Case />} />
        </Route>
    </Router>
)

App 组件具有顶栏和侧边栏的功能,React Router 自动根据当前 url 决定匹配列表页还是详情页

  • url = /:则匹配 List 组件
  • url = /list/1:则匹配 Case 组件

3. 支持多种路由切换模式

  • hashHistory:利用 hashChange 改变 # 后面的 url,浏览器兼容性较好,但是 url 中会增加 #
import { Router,Route,hashHistory} from 'react-router';
  • browserHistory:利用 history.pushState 更新整个 url,需要服务端配置,解决任意路径刷新的问题
import { Router,Route,browserHistory} from 'react-router';

4. 路由跳转

  • BrowserRouter:路由器,根据映射关系匹配新的组件。分为 BrowserRouter 和 HashRouter
  • Route:路由,定义组件与路径的映射关系。包括 Route、Switch 等
  • Link:导航,改变路径。如 Link、NavLink、Redirect

2. react-router-dom

在浏览器宿主下进一步封装 react-router,集成了 history 与 react-router,初始化了<BrowserRouter>,<HashRouter>,<Link>等可以直接使用的组件

3. react-router-redux

绑定 Router 与 Redux store

Redux 作为单一的状态管理工具,管理全局状态,其中路由也是全局状态的一种,所以也应该由 Redux 管理,通过 Redux 的方式改变路由。

React Router Redux 提供了 syncHistoryWithStore 实现 Redux store 与 Router 的绑定,它接收两个参数:history 和 store,返回一个增强的 history 对象。

将增强的 history 对象作为 props 传给 React Router 中的<Router>组件,从而实现观察路由变化改变 store 的能力。

import { browserHistory} from 'react-router';
import { syncHistoryWithStore} from 'react-router-redux';
import { reducers} from 'react-redux';

const store = createStore(reducers);
const history = syncHistoryWithStore(browserHistory,store);

用 Redux 切换路由

首先对 store 进行增强

import { browserHistory} from 'react-router';
import {routerMiddleware} from 'react-router-redux';

const middleware = routerMiddleware(browserHistory);
const store = createStore(
  reducers,
  applyMiddleware(middleware)
);

然后通过 action 切换路由

import {push} from 'react-router-redux';

store.dispatch(push('/home'));

三、 React 知识点延伸

1. 不能在循环或条件语句中使用 Hook

函数组件本身是没有状态的,所以需要引入 Hook 为其增加内部状态。

React 中每一个 Hook 方法都对应一个hook对象,用于存储和相关信息。在当前运行组件中有一个_hooks链表用于保存所有的 hook 对象。

export type Hook = {
  memoizedState: any, // 上次渲染时所用的 state
  baseState: any, // 已处理的 update 计算出的 state
  baseQueue: Update<any, any> | null, // 未处理的 update 队列(一般是上一轮渲染未完成的 update)
  queue: UpdateQueue<any, any> | null, // 当前出发的 update 队列
  next: Hook | null, // 指向下一个 hook,形成链表结构
};

以 useState 为例,初次挂载的时候执行 mountState,更新的时候执行 updateState。

  • mountState:构建链表
  • updateState:按照顺序遍历链表,获取 数据进行页面渲染

hook 的渲染其实是根据“依次遍历”来定位每个 hook 内容,如果前后两次读到的链表在顺序上出现差异,那么渲染的结果自然是不可控的。

假设在if循环中使用 useState。

// eslint-disabled-next-line
if (Math.random() > 0.5) {
  useState('first');
}
useState(100);

如上所示例子中,假设第一次调用时 Math.random()>0.5,则底层的 _hooks 数组结构如下:

_hooks: [
  {
    memoizedState: 'first',
    baseState: 'first',
    baseQueue: null,
    queue: null,
    next: {
      memoizedState: 100,
      baseState: 100,
      baseQueue: null,
      queue: null,
    },
  }
]

假设第二次调用时 Math.random()<0.5,则会将链表中的 'first' 赋值给100,从而造成显示错误。

React Hooks 是为了简化组件逻辑和提高代码可读性而设计的。将 Hook 放在 if/循环/嵌套函数中会破坏它们的封装性和可预测性,使得代码更难维护和理解。同时,这样做也增加了代码的复杂度,可能会导致性能下降和潜在的错误。

因此,在编写 React 函数组件时,一定要遵循 Hook 规则,只在顶层使用 Hooks,并且不要在循环、条件或嵌套函数中调用。

  • 只能在函数最外层调用 Hook 。不要在循环、条件语句或子函数中调用useState、useEffect等。

  • 只能在React函数组件或者自定义 Hook 调用 Hook ,不能在其他JavaScript函数中调用。

2. 定时器hook实现

入门案例:利用 react hook 实现一个计时器

1)简单实现

import React, { useEffect, useState } from "react";

const List = () => {
  const [count,setCount] = useState(0);

  useEffect(()=>{
    const timeRef = setInterval(()=>{
      setCount((count) => count + 1);
    },1000)

    return ()=>{
      clearInterval(timeRef);
    }
  },[]);

  return (
    <div>{count}</div>
  );
};

export default List;

2)封装 hook(支持自定义初始值和时间间隔)

// 使用
const {count} = useInterval(0,1000);
// 封装hook
const useInterval = (initValue: number,delay:number) => {
  const [count, setCount] = useState(initValue);

  useEffect(() => {
    const timer = setInterval(()=>{
        setCount(count => count + 1)
    }, delay);

    return () => {
      clearInterval(timer);
    };
  }, []);

  return {count}
};

3)封装 hook(改变当前组件内状态)

// 使用setCount(count => count + 1)
useInterval(()=> setCount(count => count + 1),1000);
// 封装hook
const useInterval = (callback: ()=> void,delay:number) => {
  useEffect(()=>{
    let timer = setInterval(callback,delay);

    return ()=>{
      clearInterval(timer);
    }

  },[]);
};

// 使用setCount(count + 1)
useInterval(()=> setCount(count + 1),1000);
// 封装 hook
const useInterval = (callback: () => void, delay: number) => {
  useEffect(() => {
    const time = setInterval(callback, delay);

    return () => {
      clearInterval(time);
    };
  });
};

React Hook使用时必须显示指明依赖,不能在条件语句中声明Hook

3. 封装 Button 组件

Button 调用方式如下所示:

<Button 
    classNames='btn1 btn2' 
    onClick={()=>alret(1)} 
    size='middle'
>按钮文案</Button>

封装一个 Button 组件如下:

const Button: React.FC = memo((props:any) => {
    const { classNames, onClick, size } = props;
    const sizeList = ['small','large','middle'];

    const getClassName = ()=>{
        if(size && sizeList.includes(size)){
            return `btn-${size} ${classNames}`;
        }
        return classNames;
    }

    const handleClick = ()=>{
        if(onClick){
            onClick();
        }
    }

    return (
        <div
            className={getClassName()}
            onClick={handleClick}
        >
            {props.children}
        </div>
    );
});

.btn-small{
    width: 32px;
    height:24px;
}

.btn-middle{
    width: 50px;
    height:32px;
}

.btn-large{
    width: 88px;
    height:42px;
}

4. 封装 usePrevious Hook

const usePreValue = (value:any)=>{
    const preValue = useRef(null);
    useEffect(()=>{
        preValue.current = value;
        console.log(preValue.current, 'preValue.current');
    });
    return preValue;
}

组件中使用:

const Case: React.FC = () => {
    const [count, setCount] = useState(0);
    const increment = () => setCount((c) => c + 1);
    const preCount = usePreValue(count);
    console.log(count, 'count', preCount, 'preCount');
    return (
        <div className="container">
            <div>Count: {count}</div>
            <div onClick={increment} >+1</div>
        </div>
    );
};

image.png

5. 封装 useDebounce Hook

image.png

点击按钮3秒后打印,只执行1次,不会重复执行。

const dbouncedFunction = useDbounce((str:string) => {
    console.log('数据保存到数据库:', str);
}, 3000);

return (
    <div onClick={() => dbouncedFunction('hello!')}>save</div>
);