又有时间了,继续推进React的第二天。自己学习React往往会忽视许多细节,于是找来别人的项目学习学习。今天学习一个小案例,“排序动画”。
案例的核心是“状态管理”

阅读App.jsx文件,可以看到这个可视化的排序页面主要分为4个部分
- export default function App() {
- return (
- <Container>
- <NavBar />
- <Controller />
- <AlgoDisplay />
- <Footer />
- </Container>
- );
- }
分析一下如果自己去实现,需要完成哪些信息处理逻辑。
这里,四个核心模块都是同一级的兄弟元素,因此第一个需要考虑的是:
- 1. 如何条理清晰的进行模块间信息传递?
关于每一个模块:
- 2. NavBar 需要做的处理比较简单,只需要把用户点击的算法对应的名称存入状态
- 3. Controller的生成数组比较容易,控制动画的速度也可以通过调整动画的帧率(每一帧间隔时长)来完成,最后就是动画的暂停和播放,需要状态来切换
- 4. Footer 没有太多的责任,当作一个Label来完成即可
上面的4条,其实正在关键的是第一条,因为随着项目规模的增大,麻烦的不是实现一个模块,而是井井有序的控制每个模块之间的关系。
- import { getScreenWidth } from "./helper";
- import { BubbleSort } from "../sortFunctions/BubbleSort";
- import { SelectionSort } from "../sortFunctions/SelectionSort";
- import { InsertionSort } from "../sortFunctions/InsertionSort";
- import { QuickSort } from "../sortFunctions/QuickSort";
- import { HeapSort } from "../sortFunctions/HeapSort.js";
- import { MergeSort } from "../sortFunctions/MergeSort";
-
- // 演示动画中,数字不同状态时的颜色
- export const comparisionColor = "pink";
- export const swapColor = "yellow";
- export const sortedColor = "springgreen";
- export const pivotColor = "sandybrown";
-
- // 演示动画的帧数
- export let swapTime = 1000; // 3秒内的帧数
- export let compareTime = 500; // 1.5秒内的帧数
-
- // 根据屏幕宽度,设定初始的数组
- export let sortingArray = initArrayForScreenSize();
-
- // 所有算法信息
- export const sortingAlgorithms = [
- { component: BubbleSort, title: "Bubble", name: "BubbleSort" },
- { component: SelectionSort, title: "Selection", name: "SelectionSort" },
- { component: InsertionSort, title: "Insertion", name: "InsertionSort" },
- { component: HeapSort, title: "Heap", name: "HeapSort" },
- { component: MergeSort, title: "Merge", name: "MergeSort" },
- { component: QuickSort, title: "Quick", name: "QuickSort" },
- ];
-
- function initArrayForScreenSize() {
- const screenSize = getScreenWidth();
- if (screenSize < 460) return [4, 3, 2, 1];
- else if (screenSize < 720) return [8, 7, 6, 5, 4, 3, 2, 1];
- return [12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
- }
- // 将输入框中的字符串转换为数组字符串,意思就是当往输入框中输入非数字逗号的时候,输入的字符会被清除掉
- // 比如 输入 1,2,3,4 正常,当后续输入一个a的时候,输入框不会显示a
- export function convertInputToArrayString(string) {
- string = string.replaceAll(/\s/g, "");
- string = string.replaceAll(/\d{4}/g, "");
- string = string.replaceAll(/\s\s/g, " ");
- string = string.replaceAll(/\s,/g, ",");
- string = string.replaceAll(/,,/g, ",");
- string = string.replaceAll(/[^0-9,\s]/g, "");
- return string;
- }
- // 将数组字符串转变为数组
- export function convertArrayStringToArray(string) {
- return string
- .split(",")
- .filter((v) => v !== "")
- .map((v) => +v);
- }
- // 生成随机数组
- export function getRandomArray(length = generateRandomNumberInRange(5, 30)) {
- return Array.from(new Array(length), () => generateRandomNumberInRange());
- }
- // 获得屏幕(当前浏览器页面)宽度
- export function getScreenWidth(){
- return window.innerWidth;
- }
- // 设定延迟的函数
- export function delay(time) {
- return new Promise((resolve) => setTimeout(resolve, time));
- }
- // 生成指定范围内的数字, 范围是(lowerLimit, lowerLimit+upperLimit)
- function generateRandomNumberInRange(lowerLimit = 0, upperLimit = 999) {
- return lowerLimit + Math.floor(Math.random() * upperLimit);
- }
create方法会创建一个状态管理器,每次使用这个管理器,需要传入一个自定义的方法来间接控制状态,当管理器实现完后,每次传入的方法应该是很简单的方法。一般是调用状态对象的属性,属性包含了状态值或者setState方法。
- import create from "zustand";
- import { devtools } from "zustand/middleware";
- import {
- sortingArray,
- compareTime,
- swapTime,
- sortingAlgorithms,
- } from "./config";
- // 创建了一个全局状态管理器,其内包含各个状态以及对应的seState方法
- export const useControls = create(
- devtools((set) => ({
- progress: "reset", // 动画的重置、播放、暂停状态
- speed: 3, // 动画的速度
- compareTime: compareTime, // 动画数字比较一次所花费的时间
- swapTime: swapTime, // 交换动画一次所花费的时间
- doneCount: 0, // 当前执行完成的动画个数(当需要同时演示所有动画的时候)
- // 开始排序,则设定progress为start,以此类推
- startSorting: () => set({ progress: "start" }),
- pauseSorting: () => set({ progress: "pause" }),
- resetSorting: () => set({ progress: "reset", doneCount: 0 }),
- // 标记动画已经完成:
- // 如果当前选择的是最后一个,即(ALL选项)同时演示所有算法
- // --如果已经完成所有算法, 设置完成算法数量以及progress为done
- // --否则,将doenCount加一,表示当前完成了一个算法
- // 否则直接将doneCount加一,因为非ALL选项,都只有一个算法,完成即表示所有完成
- markSortngDone: () =>
- set((state) => {
- if (useData.getState().algorithm === sortingAlgorithms.length) {
- if (state.doneCount === sortingAlgorithms.length - 1)
- return { doneCount: 0, progress: "done" };
- else return { doneCount: state.doneCount + 1 };
- } else return { progress: "done" };
- }),
- setSpeed: (speed) =>
- set(() => {
- return { swapTime: 3000 / speed, compareTime: 1500 / speed, speed };
- }),
- }))
- );
-
- // 设置当前正在执行的算法的状态管理器,包含两个状态:算法的id和算法排序中途的数组
- export const useData = create(
- devtools((set) => ({
- // algorithm 表示算法id,如果实现了6种排序算法,
- // 则每一个算法id依次为0,1,2,3,4,5 另外还有一个id=6 表示同时演示所有算法
- algorithm: 0,
- // sortingArray这个就是排序数组,表示当前时刻,此算法对数组排序的结果
- sortingArray: sortingArray,
- // 设置新的数组,可能是经过上一步的排序,得到的新数组
- setSortingArray: (array) => set({ sortingArray: array }),
- // 设置算法的id
- setAlgorithm: (idx) => set({ algorithm: idx }),
- }))
- );
这里状态管理的代码就阅读完了,同时整个项目的实现也大致可以猜到了,就是借助全局的状态管理器,来完成兄弟元素间的信息传递,所有的模块只需要跟useData和useControls来交流即可。