受控组件和非受控组件
受控组件
受控组件主要是由组件内部自己的状态控制,当组件的value改变时,state更新->组件重新render->新的value传给组件。 最经典的:
1import { useState } from "react";
2
3export default function App() {
4 const [username, setUsername] = useState("");
5
6 return (
7 <input
8 value={username}
9 onChange={(e) => setUsername(e.target.value)}
10 />
11 );
12}这里 value={username} 说明:input的值完全由React state完成。
用户输入:onChange触发 setUsername改变input的值
数据流:
1用户输入
2↓
3onChange
4↓
5setState
6↓
7React重新渲染
8↓
9value更新这就是react控制了input 所以叫受控组件
非受控组件
1import { useRef } from "react";
2
3export default function App() {
4 const inputRef = useRef();
5
6 const handleSubmit = () => {
7 console.log(inputRef.current.value);
8 };
9
10 return (
11 <>
12 <input ref={inputRef} />
13 <button onClick={handleSubmit}>提交</button>
14 </>
15 );
16}这里<input ref={inputRef}>
input的值React不管,而是由浏览器DOM自己存
提交时在DOM中拿值,数据不经过React state
这就叫非受控组件
为什么React更推崇受控组件
这是React推崇页面应该由state决定 受控组件的好处:
- 数据统一
- 好管理
- 好校验
- 好联动
- 好做表单库
大表单库为什么喜欢非受控组件
因为受控组件改变会重新render,受控组件每输入一次,render一次,大表单如果这样做性能开销大,应该尽量避免render
为什么value会让input变成受控组件
因为input的值已经固定由React提供, 修改dom后,下一次render值又会被重新修改,所以DOM自己无法决定value。
defaultValue是什么
1<input defaultValue="hello">这是初始值,后续DOM自己改,React不再控制
所以defaultValue一般属于非受控组件
setState是同步,还是异步?
setState在react18之前,分不同情况来表示同步和异步(setState其实不强调异步还是同步,而是state更新是否立即生效)。 像setTimeout、setInterval、js原生事件、promise中,表现为同步。 在React的合成事件中,表现为异步。
而react18之后,setState都会表现为异步,这个异步是批处理。 当有多个setState时,React不会将其立即更新,也不会获取最新的数据,而是通过调度,将其进行统一更新
拿到最新值的方式
最重要的有四种
- 函数式更新
- useEffect
- useRef
- flushSync(很少用)
函数式更新:
错误写法:
setCount(count+1)这个闭包里面的永远是就值 正确写法:setCount(prev=>prev+1),永远是React更新队列中的最新state。 useEffect 每次拿到值后更新 useRef 用useRef保存最新值,在useEffect中获取useRef的值 flushSync 这个可以强制立即更新
1import { flushSync } from "react-dom";
2
3flushSync(() => {
4 setCount(1);
5});
6
7console.log(count);React事件绑定的方式有哪些?区别是什么
主要有:
- JSX内联函数
- bind绑定
- 类组件构造函数绑定
- 箭头函数类属性
- 函数组件
函数组件
1function App() {
2 const handleClick = () => {
3 console.log("click");
4 };
5
6 return <button onClick={handleClick}>按钮</button>;
7}onClick={handleClick}这里应该直接传函数引用,不能加()不然会直接执行,传参数用箭头函数,或用bind<button onClick={handleClick.bind(null, id)}>
bind和箭头函数的区别
bind本质是返回一个新函数,等点击时再执行 箭头函数本质也是创建新函数
类组件中的事件绑定
class 方法默认不会绑定 this
1class App extends React.Component {
2 handleClick() {
3 console.log(this);
4 }
5
6 render() {
7 return <button onClick={this.handleClick}>按钮</button>;
8 }
9}this是undefined,因为class方法默认丢失this 解决方法
- 用bind绑定
- render中bind
- 箭头函数类属性
React事件和原生事件区别
React中像onClick这种事件,不是直接绑定到DOM上。
React有合成事件,React会将事件事件委托
在React17之前,是将事件挂载到document上,React17后,挂到根容器上
React JSX转换为真实DOM过程
JSX会先通过babel这种,讲JSX代码转换为React.createElement()这样的函数,这个函数中包含了类型,内容等信息,执行这个函数后,会生成一个普通的JS对象,即React Element(严格来说,React Element就是Virtual DOM的一种实现形式),它描述了组件的结构,但他只是轻量级的描述对象。 然后React会根据React Element创建Fiber树,Fiber就是一个增强的虚拟DOM节点,它不仅包含元素信息,还包含了任务优先级、并发状态以及指向父节点/兄弟节点的指针。在rener阶段会进基于新Fiber树和旧Fiber树进行diff算法对比。找出需要变更的地方,打上flags标记,更新新的Fiber树。然后进行Commit,这个过程不能被打断,React会根据flags更新真实DOM,执行ref,执行useLayoutEffecct,最后异步执行useEffect。
React 生命周期
一、Class 组件生命周期 挂载阶段(Mounting)
Class 组件生命周期(Mounting)
挂载阶段
| 生命周期 | 做了什么 |
|---|---|
| constructor(props) | 初始化 state、绑定方法。唯一可以直接赋值 this.state 的地方 |
| static getDerivedStateFromProps(props,state) | 根据 props 派生 state。返回对象合并到 state,返回 null 不更新。纯函数,无副作用 |
| render() | 返回 JSX,描述 UI 结构。纯函数,不能调用 setState,不能操作 DOM |
| componentDidMount() | 组件挂载到 DOM 后调用。适合:发请求、订阅事件、操作 DOM、启动定时器 |
更新阶段
触发条件:setState()、forceUpdate()、父组件重新渲染
| 生命周期 | 做了什么 |
|---|---|
| static getDerivedStateFromProps(props, state | 同挂载阶段,每次更新都会调用 |
| shouldComponentUpdate(nextProps, nextState) | 返回 boolean 决定是否继续渲染。性能优化点。forceUpdate() 会跳过此方法 |
| render() | 重新计算 Virtual DOM |
| getSnapshotBeforeUpdate(prevProps, prevState) | DOM 更新前调用,可以获取更新前的 DOM 信息(如滚动位置)。返回值传给 componentDidUpdate 的第三个参数 |
| componentDidUpdate(prevProps, prevState, snapshot) | DOM 更新后调用。适合:根据 props 变化发请求、操作更新后的 DOM。注意要加条件判断避免死循环 |
卸载阶段
| 生命周期 | 做了什么 |
|---|---|
| componentWillUnmount() | 组件从 DOM 移除前调用。清理工作:取消订阅、清除定时器、取消请求、移除事件监听 |
错误处理
| 生命周期 | 做了什么 |
|---|---|
| static getDerivedStateFromError(error) | 子组件抛错时调用,返回新 state 用于渲染降级 UI。render 阶段调用,不允许副作用 |
| componentDidCatch(error, info) | 子组件抛错时调用,可以执行副作用(如上报错误日志)。info 包含 componentStack |
二、已废弃的生命周期(React 17+ 标记 UNSAFE) 废弃方法 原来做什么 为什么废弃 UNSAFE_componentWillMount 挂载前执行,常用于发请求 Concurrent Mode 下可能被多次调用,副作用不安全 UNSAFE_componentWillReceiveProps(nextProps) props 变化时派生 state 容易写出 bug,用 getDerivedStateFromProps 替代 UNSAFE_componentWillUpdate(nextProps, nextState) 更新前读取 DOM 用 getSnapshotBeforeUpdate 替代 三、Hooks 对应的生命周期 函数组件没有生命周期方法,但 Hooks 覆盖了相同的能力:
1function MyComponent({ id }) {
2 // ≈ constructor:初始化 state
3 const [data, setData] = useState(null);
4
5 // ≈ getDerivedStateFromProps
6 // 直接在渲染过程中根据 props 计算,或用 useMemo
7 const derived = useMemo(() => computeFrom(id), [id]);
8
9 // ≈ componentDidMount(空依赖)
10 useEffect(() => {
11 subscribe();
12 return () => unsubscribe(); // ≈ componentWillUnmount
13 }, []);
14
15 // ≈ componentDidUpdate(有依赖)
16 useEffect(() => {
17 fetchData(id);
18 }, [id]);
19
20 // ≈ getSnapshotBeforeUpdate
21 useLayoutEffect(() => {
22 // DOM 更新后、浏览器绘制前同步执行
23 const scrollPos = ref.current.scrollTop;
24 return () => { /* cleanup */ };
25 });
26
27 // ≈ shouldComponentUpdate
28 // 用 React.memo 包裹组件
29 // 或 useMemo 缓存昂贵计算
30
31 // ≈ render
32 return <div>{data}</div>;
33}
34
35// ≈ shouldComponentUpdate
36export default React.memo(MyComponent);
37四、React 18 Strict Mode 的影响 开发模式下,React 18 会故意双重调用以下方法来帮你发现副作用问题:
constructor getDerivedStateFromProps render useState / useReducer 的初始化函数 useEffect / useLayoutEffect(mount → unmount → mount) 生产环境不会双重调用。
父组件调用子组件的方法
方法一:useImperativeHandle+forwardRed
1import { useRef, useImperativeHandle, forwardRef } from 'react';
2
3//优点:子组件控制暴露哪些方法,保持封装性
4
5
6// 定义暴露给父组件的方法类型
7interface ChildHandle {
8 focus: () => void;
9 reset: () => void;
10 getValue: () => string;
11}
12
13// 子组件
14const Child = forwardRef<ChildHandle>((props, ref) => {
15 const inputRef = useRef<HTMLInputElement>(null);
16 const [value, setValue] = useState('');
17
18 // 选择性暴露方法给父组件
19 useImperativeHandle(ref, () => ({
20 focus() {
21 inputRef.current?.focus();
22 },
23 reset() {
24 setValue('');
25 },
26 getValue() {
27 return value;
28 },
29 }));
30
31 return <input ref={inputRef} value={value} onChange={e => setValue(e.target.value)} />;
32});
33
34// 父组件
35const Parent = () => {
36 const childRef = useRef<ChildHandle>(null);
37
38 const handleSubmit = () => {
39 const value = childRef.current?.getValue();
40 console.log(value);
41 childRef.current?.reset();
42 };
43
44 return (
45 <>
46 <Child ref={childRef} />
47 <button onClick={() => childRef.current?.focus()}>聚焦</button>
48 <button onClick={handleSubmit}>提交并重置</button>
49 </>
50 );
51};
52方法二:React19的ref作为prop(不再需要forwardRef)
React19中ref可以直接作为普通props传递
1interface ChildHandle {
2 scrollToTop: () => void;
3}
4
5// 不需要 forwardRef 了
6const Child = ({ ref }: { ref: React.Ref<ChildHandle> }) => {
7 useImperativeHandle(ref, () => ({
8 scrollToTop() {
9 window.scrollTo(0, 0);
10 },
11 }));
12
13 return <div>Child Content</div>;
14};
15
16const Parent = () => {
17 const childRef = useRef<ChildHandle>(null);
18
19 return (
20 <>
21 <Child ref={childRef} />
22 <button onClick={() => childRef.current?.scrollToTop()}>回到顶部</button>
23 </>
24 );
25};
26方式三:通过回调prop注册方法
不用ref,子组件主动把方法注册给父组件
1const Child = ({ onReady }: { onReady: (api: { refresh: () => void }) => void }) => {
2 const [data, setData] = useState([]);
3
4 const refresh = useCallback(async () => {
5 const res = await fetch('/api/data');
6 setData(await res.json());
7 }, []);
8
9 useEffect(() => {
10 onReady({ refresh });
11 }, [onReady, refresh]);
12
13 return <ul>{data.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
14};
15
16const Parent = () => {
17 const childApi = useRef<{ refresh: () => void }>(null);
18
19 return (
20 <>
21 <Child onReady={api => { childApi.current = api; }} />
22 <button onClick={() => childApi.current?.refresh()}>刷新子组件</button>
23 </>
24 );
25};
26