前言
主要根据官网教程和视频学习总结,尽量精简,分享出来帮助刚学react的朋友。
如果有错误或补充,欢迎留言交流,会对文章进行更正,最后帮到你了可以点个赞。
1、react特点
- JSX语法
- 单向数据流(MVC)
- 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.过滤后的产品列表
开发步骤:
-
将UI拆解成一个个组件,并做好命名(遵守单一职责原则,一个组件只做一件事);
-
构建静态版本,创建组件,使用props传递数据,但不创建状态,不添加交互;
-
找到程序的所有数据,并分析哪些是状态;
l 随着时间推移保持不变的,不是状态
l 通过props传入的,不是状态
l 可以根据现有状态或props计算得到的,不是状态(遵守DRY原则,不要重复自己)
-
确定在哪个组件中创建状态;
(1) 找到使用状态的每个组件
(2) 在最近的公共父组件中创建状态,如果没有就创建一个仅用于保存状态的公共父组件
-
添加交互,更改状态
4、纯函数和副作用
react组件的编写规范就是纯函数。
纯函数的特征(函数式编程领域):
1. 相同的输入始终返回相同的输出
2. 只管自己的事(不会更改调用之前存在的任何对象或变量)
可以使用严格模式 <React.StrictMode> 来检测非纯组件,原理是双重渲染(所有组件渲染两次)。
副作用的特征:
-
结果是不可控的,不可预期
-
会影响其他组件,并且无法在渲染期间完成
常见的有:发送网络请求、绑定事件处理、手动操作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属性的联合