[react] 基础详细总结

前言

主要根据官网教程和视频学习总结,尽量精简,分享出来帮助刚学react的朋友。

如果有错误或补充,欢迎留言交流,会对文章进行更正,最后帮到你了可以点个赞。

1、react特点

  1. JSX语法
  2. 单向数据流(MVC)
  3. Hook函数

2、JSX语法

JSX:JavaScript语法扩展或JavaScript XML

允许在js中直接写html,扩展了js能力,依赖babel转译,babel会把jsx转译成React.createElement()函数调用,结果会创建一个对象,所以jsx本质就是对象

jsx比html更严格:组件只有一个根元素、必须关闭单标签

Fragments(也叫空标签),用于包装或分组:<></>、

通过 {} 嵌入 js 表达式

jsx 复制代码
通过attrs={}可以给属性(或prop)传递任意类型的值。
{{}}表示传递一个对象。style属性就要求必须传递对象。
直接在标签内使用{},只有number、string、array类型的值会被渲染出来,其他类型要么渲染为空,要么报错,一般返回null表示不渲染任何内容。
数组会把每一项展开分别渲染,这很常用:{ [<div>1</div>, <div>2</div>] }

给元素设置样式

jsx 复制代码
style={{ color: 'red', fontSize: '18px' }}
className="box active"

控制元素显示隐藏

jsx 复制代码
style={{ display: flag ? 'unset' : 'none' }}
{flag ? <div>显示</div> : null}
{flag && <div>显示</div>}
    
注意不要将数字放在 && 的左侧,因为如果左侧为0,那整个表达式就返回0:
count && <p>New messages</p> 改成 count > 0 && <p>New messages</p>

列表数据循环渲染(先 filter 后 map

jsx 复制代码
<ul>
    {data
        .filter((item) => item.flag)
        .map((item, index) => (
            <li key={item.id}>
                <em>{index + 1}</em>
                <span>{item.title}</span>
            </li>
        ))}
</ul>

{Array(5)
  .fill(null)
  .map((_, index) => (
    <button key={index}>{index + 1}</button>
  ))}

插入空格

jsx 复制代码
{flag && ' '}
{flag && <> <div>...</div></>}
{flag && <div> ...</div>}

3、开发步骤

上图所有数据(加粗的表示状态):1.原始产品列表 2.搜索值 3.复选值 4.过滤后的产品列表

开发步骤:

  1. 将UI拆解成一个个组件,并做好命名(遵守单一职责原则,一个组件只做一件事);

  2. 构建静态版本,创建组件,使用props传递数据,但不创建状态,不添加交互;

  3. 找到程序的所有数据,并分析哪些是状态;

    l 随着时间推移保持不变的,不是状态

    l 通过props传入的,不是状态

    l 可以根据现有状态或props计算得到的,不是状态(遵守DRY原则,不要重复自己)

  4. 确定在哪个组件中创建状态;

    (1) 找到使用状态的每个组件

    (2) 在最近的公共父组件中创建状态,如果没有就创建一个仅用于保存状态的公共父组件

  5. 添加交互,更改状态

4、纯函数和副作用

react组件的编写规范就是纯函数。

纯函数的特征(函数式编程领域):

1. 相同的输入始终返回相同的输出

2. 只管自己的事(不会更改调用之前存在的任何对象或变量)

可以使用严格模式 <React.StrictMode> 来检测非纯组件,原理是双重渲染(所有组件渲染两次)。


副作用的特征:

  1. 结果是不可控的,不可预期

  2. 会影响其他组件,并且无法在渲染期间完成

常见的有:发送网络请求、绑定事件处理、手动操作DOM、定时器、订阅

事件处理程序不需要是纯粹的,因为它用于执行副作用,如果没有适合副作用的事件,使用useEffect。

5、事件

约定:事件名称以on开头,事件处理程序以handle开头(除非传递prop)

jsx 复制代码
<Test onCustomEvent={handleCustomEvent} />
function Test({ onCustomEvent })
onClick={onCustomEvent} // 只有这种情况例外

如果有多个相同事件,事件处理程序的名称带上影响状态,比如:handleNameChange

使用箭头函数给事件处理程序传参:(e) => handleEvent(e, ...)

事件传播:从事件发生的位置开始,沿着树向上传播,相同事件将被触发,也叫事件"冒泡"(react中除了onScroll外,其他事件都会传播)。

停止传播:e.stopPropagation()

阻止默认行为:e.preventDefault()

事件捕获:在事件名称末尾添加Capture,例如onClickCapture(捕获可以用于代码分析,比如记录点击次数)

事件执行顺序口诀:先捕获后触发再冒泡

react中的事件处理程序所传入的事件对象,是一个合成事件对象

访问原生事件对象:e.nativeEvent

6、props

react组件函数接受单个参数,一个props对象,即props是组件的唯一参数。

props是不可变的,更新渲染只是传递新prop,丢弃旧prop

给prop设置默认址:function Avatar({ size = 100 }) {

什么情况使用默认值?没传prop或传了但值为undefined时(null不行)

将所有props转发给子组件:<Avatar {...props} />

谨慎使用扩展语法,这种情况优先考虑用children将子组件传递进来。

props.children表示组件的嵌套内容,可以理解为一个"洞",由其父组件任意填充。(vue插槽)

传递多个prop,可以一个个传,也可以将相关的分组到一个对象中传递,分组会更清晰,但代码会多

父传子数据,通过props,子想改变父数据,也需要父传方法,子调用

7、状态和渲染

只要状态发生改变,那使用它的组件及其子组件都会重新渲染,注意防止无限循环。

状态是独立的:渲染同一个组件两次,每个组件实例都有自己的状态。

状态是私有的:状态对于对于声明它的组件是完全私有的,其他组件无法查看、更改。

如果状态有关联,那将它们合并为一个(对象或数组)状态会更好维护。比如说一个包含很多字段的表单,使用一个状态来保存一个对象比每个字段都有一个状态更好。

状态提升 :要从多个子组件收集数据 ,或者让两个子组件相互通信,请在其父组件中声明共享状态。父组件可以通过 props 将该状态传递回子组件。这使得子组件之间以及与其父组件保持同步(组件重构时,将状态提升到父组件是常用手段)。

7.1、状态队列

调用set函数会触发重新渲染。react会再次调用组件函数,使用当时的状态快照 返回jsx,然后更新屏幕。状态不像变量那样在函数执行完成后消失,状态存在于内存当中,并且只在下次渲染时变更

状态更新的时机:等到事件处理程序中的所有代码都执行完毕后。这样好处是减少重新渲染的次数。

如果想在下次渲染前多次更新同一状态 ,需要使用更新器函数(updater function),根据队列中的上一个状态来计算下一个状态(简单理解就是先读取最新状态再更新)。语法即set函数传回调:setXxxYyy((xy) => xy + 1),命名约定:状态名的词首字母拼接。另外,更新器函数必须是纯函数(不要在其内部设置状态或执行副作用)。

所有set函数会按执行顺序添加到状态队列中,在下次渲染期间,遍历处理队列,得到最终的更新状态。

jsx 复制代码
const [number, setNumber] = useState(0);
setNumber(number + 5); // n = 0 + 5
setNumber(n => n + 1); // n = 5 + 1
setNumber(42); // n = 42

=> 最终状态:42

7.2、使用flushSync同步更新DOM

由于状态更新是排队的,所以当执行set函数更新状态后,DOM并不会马上同步更新。

要想在状态更新后,立即获取最新的DOM,使用flushSync包裹set函数,在其内部代码执行完成后,会立即同步更新DOM(类似vue的nextTick)。

jsx 复制代码
import { flushSync } from 'react-dom';
flushSync(() => {
  setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();

8、为什么绑定key

渲染列表时,react会存储每个列表项的信息,更新列表时,react需要知道发生了什么变化,所以添加唯一的key,以便react可以跟踪每个列表项。key变了,组件将销毁重建。

使用数组索引作为key是不建议的,除非列表永远不会被移动(重新排序)、插入、删除。

本地生成的数据,使用递增计数器或uuid_v4(crypto.randomUUID())做key。

type Key = number | string;

不要动态生成key,例如key={Math.random()},这将导致key永远不匹配,每次都要重新创建所有组件和DOM,很慢。

9、更新对象或数组状态

更新对象状态 ,需要创建一个新对象替换原对象,而不是直接修改其属性(这叫突变mutation)

使用扩展语法复制原对象的所有属性再覆盖修改:setPosition({ ...position, x: 100 })

或创建一个空对象,通过赋值直接修改它(这叫局部突变local mutation),最后作为状态提交:

jsx 复制代码
const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);

局部突变是可以的,没有问题,但突变不行。

更新嵌套对象的方法

(在代码执行时,不存在"嵌套"对象,它们都是独立的,只是有"指向"关系)

一、 嵌套使用扩展语法(因为其是浅拷贝,只复制一层)

jsx 复制代码
const objCopy = {
    ...obj,
    c: {
        ...obj.c,
        d: '更新',
    },
};

// 技巧:在对象中使用[]定义动态属性名
setPerson({
    ...person,
    [e.target.name]: e.target.value,
});

二、 将状态变得扁平,改成id映射,而不是使用树形结构,这样更容易更新嵌套对象

jsx 复制代码
{
    0: {
        id: 0,
        title: '(Root)',
        childIds: [1, 42, 46],
    },
    1: {
        id: 1,
        title: 'Earth',
        childIds: [2, 10, 19, 26, 34],
    },
    ...
}

三、 Immer


更新数组状态:数组只是另一种对象,所以更新方法和对象一样,也是替换。

添加:push/unshift => [...arr, {}]/[{}, ...arr]

删除:pop/shift => filter

修改:arr[i] => map

插入:splice => [...arr.slice(0, insertIndex), {}, ...arr.slice(insertIndex)]

如果要做的操作没法通过上面方法实现,比如翻转或排序数组,那就先复制整个数组,再进行更改。

注意,如果是更新对象数组,使用map找到要改的对象,然后整个替换掉。

9.1、为什么更新是替换

9.2、Immer

方便更新嵌套状态,减少复制带来的重复代码,并且更新对象或数组状态时不用考虑对原对象或数组的影响,想怎么改怎么改,因为在底层immer会监听draft代理对象的修改,并生成一个全新副本。

jsx 复制代码
npm i use-immer
import { useImmer } from 'use-immer'

const [obj, updateObj] = useImmer({
    a: 1,
    b: {
        c: 2,
    },
});

function handleClick() {
    updateObj((draft) => {
        draft.a += 1;
        draft.b.c += 1;
    });
}

10、表单

受控组件:使用react状态控制值

非受控组件:使用ref获取DOM,再由DOM处理值

  • 指定默认值:defaultValue/defaultChecked

尽量使用受控组件,除非不得已才用非受控(比如文件上传)。

10.1、受控组件

jsx 复制代码
type TFormState = {
    value: string;
    multipleValue: string[];
    checkboxValue: boolean;
    multipleCheckboxValue: string[];
};

// 表单数据
const [formState, setFormState] = useState<TFormState>({
    value: '默认值',
    multipleValue: ['grapefruit'],
    checkboxValue: false,
    multipleCheckboxValue: [],
});

type TOptions = { value: string; label: string; checked: boolean }[];

// 选项
const [options, setOptions] = useState<TOptions>([
    { value: 'grapefruit', label: 'Grapefruit', checked: false },
    { value: 'lime', label: 'Lime', checked: false },
    { value: 'coconut', label: 'Coconut', checked: false },
    { value: 'mango', label: 'Mango', checked: false },
]);

/** 获取表单字段值 */
const getFieldValue = (target: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) => {
    if (target.type === 'checkbox') {
        return (target as HTMLInputElement).checked;
    }

    if (target.type === 'select-multiple') {
        return Array.from((target as HTMLSelectElement).options)
            .filter((option) => option.selected)
            .map((option) => option.value);
    }

    return target.value;
};

/** 处理表单字段变化,支持多种输入类型 */
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
    const target = e.target;
    const fieldValue = getFieldValue(target);
    // 使用函数更新状态,避免多个字段同时更新时,覆盖其他字段的值
    setFormState((prevState) => ({ ...prevState, [target.name]: fieldValue }));
};

/** 渲染下拉选项 */
const renderSelectOptions = options.map((item) => {
    return (
        <option key={item.value} value={item.value}>
            {item.label}
        </option>
    );
});

/** 处理多选复选框变化 */
const handleMultipleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>, index: number) => {
    const newOptions = options.map((item, i) => {
        if (i === index) {
            return { ...item, checked: e.target.checked };
        }
        return item;
    });

    setOptions(newOptions);

    setFormState((prevState) => ({
        ...prevState,
        multipleCheckboxValue: newOptions.filter((item) => item.checked).map((item) => item.value),
    }));
};

/** 渲染多选复选框 */
const renderMultipleCheckbox = options.map((item, index) => {
    return (
        <label key={item.value}>
            <input
                type="checkbox"
                value={item.value}
                checked={item.checked}
                onChange={(e) => handleMultipleCheckboxChange(e, index)}
            />
            {item.label}
        </label>
    );
});

return (
    <div style={{ padding: '20px' }}>
        <label>
            输入框:
            <input type="text" name="value" value={formState.value} onChange={handleChange} placeholder="请输入" />
        </label>

        <label>
            文本域:
            <textarea name="value" value={formState.value} onChange={handleChange} placeholder="请输入" />
        </label>

        <label>
            下拉框:
            <select name="value" value={formState.value} onChange={handleChange}>
                <option value="">=== 请选择 ===</option>
                {renderSelectOptions}
            </select>
        </label>

        <button onClick={() => setFormState({ ...formState, value: '' })}>Clear</button>

        <label>
            多选下拉框:
            <select multiple={true} name="multipleValue" value={formState.multipleValue} onChange={handleChange}>
                {renderSelectOptions}
            </select>
        </label>

        <label>
            单选复选框:
            <input type="checkbox" name="checkboxValue" checked={formState.checkboxValue} onChange={handleChange} />
        </label>

        <label>
            多选复选框:
            {renderMultipleCheckbox}
        </label>
    </div>
);

10.2、非受控组件

jsx 复制代码
const fileRef = useRef<HTMLInputElement>(null);

const handleClick = () => {
    if (fileRef.current && fileRef.current.files) {
        const file = fileRef.current.files[0];
        console.log(file.name);
    }
};

return (
    <div>
        <label>
            上传文件:
            <input type="file" ref={fileRef} />
        </label>

        <button onClick={handleClick}>Submit</button>
    </div>
);

11、Hook

hook是一种特殊函数,可以"接入"react的状态和功能。

react提供了一些内置hook,也可以自定义hook。

11.1、从类组件转为函数组件的原因

1. hook复用状态逻辑更容易,不用改变组件层次结构;

2. 生命周期导致组件越复杂越难维护,因为相互关联的代码会被拆分开,有的代码还要在不同生命周期重复执行,最后生命周期会包含一堆不相关的代码,容易造成错误;

3. js 更适合函数式编程而不是面向对象编程,因为js 中的类并不好用(代码冗长、烦人的this )。

11.2、hook使用规则

hook使用规则:hook 只能在< 函数组件> 或< 自定义hook> 的顶层无条件调用

如果想在条件、循环或嵌套函数中使用,必须提取出一个新组件。

为什么有这样的规则?因为react依赖调用顺序来区分状态,这样能保证在每次渲染时hook的调用顺序是相同的。

11.3、useState和useEffect

使用useState声明状态:const [something, setSomething] = useState(null)

something是状态名,setSomething是更新状态的函数,null是状态的初始值。

有关更新器函数(updater function)查看【状态和渲染】那节。

为什么react使用数组解构的方式声明状态,看着有点奇怪?

数组解构方便给状态和更新函数命名,而对象解构更麻烦:{a: c, b: d}


使用useEffect处理副作用(类似类组件中的生命周期方法),会在组件每次 渲染完成后执行,执行时DOM 已经更新。可以返回一个清理函数,每次会先执行清理函数,再执行副作用(清理是很有必要的,防止内存泄露)。

jsx 复制代码
useEffect(() => {
    // 执行副作用
    return () => {
        // 执行清理函数
    };
});

两次渲染执行顺序:第一次执行副作用 => 第一次执行清理 => 第二次执行副作用

观察可知:清理函数在第一次执行副作用前不会执行。

jsx 复制代码
useEffect(() => {}, [依赖状态1, 依赖状态2, ...]);

为了避免每次渲染都执行浪费性能,一般都会设置依赖项,只在依赖状态发生改变时,才执行副作用(类似vue的computed),空数组表示副作用只执行一次。

11.4、自定义hook

自定义hook与函数的区别:自定义hook能调用其他hook,而普通函数不能。

自定义hook主要用于拆分业务逻辑,重用状态逻辑,约定以use开头。

每次调用自定义hook,都具有完全独立的状态,即使是在同个组件中调用两次。

自定义hook不需要特定签名,接收参数、返回值都没有限制,可以传递状态,非常灵活。

jsx 复制代码
function useFormInput(initialValue: string) {
    const [value, setValue] = useState(initialValue);
    function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
        setValue(e.target.value);
    }
    return {
        value,
        onChange: handleChange,
    };
}

function useDocumentTitle(title: string) {
    useEffect(() => {
        document.title = title;
    });
}

function useWindowWidth() {
    const [width, setWidth] = useState(window.innerWidth);
    useEffect(() => {
        const handleResize = () => setWidth(window.innerWidth);
        window.addEventListener('resize', handleResize);
        return () => {
            window.removeEventListener('resize', handleResize);
        };
    });
    return width;
}

12、使用TS

TS 添加到现有项目中

jsx 复制代码
npm i @types/react @types/react-dom
// tsconfig.json
"lib": ["dom"], // lib必须包含dom
"jsx": "preserve", // 一般preserve

声明 props 的类型www.typescriptlang.org/docs/handbo...

jsx 复制代码
function MyButton({ title }: { title: string })
interface MyButtonProps {
  title: string;
  disabled: boolean;
}
function MyButton({ title, disabled }: MyButtonProps)

声明 useState 的类型(默认是初始状态的类型):

jsx 复制代码
useState<boolean>(false)
type Status = "idle" | "loading" | "success" | "error";
useState<Status>("idle")
type RequestState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success', data: any }
  | { status: 'error', error: Error };
useState<RequestState>({ status: 'idle' })

声明 DOM 事件的类型

jsx 复制代码
<input value={value} onChange={handleChange} />
function handleChange(event: React.ChangeEvent<HTMLInputElement>)

如果不知道事件用什么类型,可以悬停在(onChange)事件上查看,或在《完整列表》中查找,或使用React.SyntheticEvent(合成事件),它是所有事件的基本类型。

声明组件内容的类型

jsx 复制代码
children: React.ReactNode; // 内容是什么都行
children: React.ReactElement; // 内容只能是jsx元素

声明样式的类型

jsx 复制代码
style: React.CSSProperties; // css属性的联合
相关推荐
我码玄黄22 分钟前
THREE.js:网页上的3D世界构建者
开发语言·javascript·3d
罔闻_spider23 分钟前
爬虫----webpack
前端·爬虫·webpack
吱吱鼠叔24 分钟前
MATLAB数据文件读写:1.格式化读写文件
前端·数据库·matlab
爱喝水的小鼠41 分钟前
Vue3(一) Vite创建Vue3工程,选项式API与组合式API;setup的使用;Vue中的响应式ref,reactive
前端·javascript·vue.js
小晗同学42 分钟前
Vue 实现高级穿梭框 Transfer 封装
javascript·vue.js·elementui
WeiShuai1 小时前
vue-cli3使用DllPlugin优化webpack打包性能
前端·javascript
forwardMyLife1 小时前
element-plus的面包屑组件el-breadcrumb
javascript·vue.js·ecmascript
ice___Cpu1 小时前
Linux 基本使用和 web 程序部署 ( 8000 字 Linux 入门 )
linux·运维·前端
JYbill1 小时前
nestjs使用ESM模块化
前端
加油吧x青年1 小时前
Web端开启直播技术方案分享
前端·webrtc·直播