[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属性的联合
相关推荐
WeiXiao_Hyy23 分钟前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
吃杠碰小鸡40 分钟前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone1 小时前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_09011 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农1 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king2 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳2 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵3 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星3 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_3 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js