b

2026-05-14bas

受控组件和非受控组件

受控组件

受控组件主要是由组件内部自己的状态控制,当组件的value改变时,state更新->组件重新render->新的value传给组件。 最经典的:

tsx
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的值 数据流:

code
1用户输入  
23onChange  
45setState  
67React重新渲染  
89value更新

这就是react控制了input 所以叫受控组件

非受控组件

tsx
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是什么
ts
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 这个可以强制立即更新
tsx
1import { flushSync } from "react-dom";
2
3flushSync(() => {
4  setCount(1);
5});
6
7console.log(count);

React事件绑定的方式有哪些?区别是什么

主要有:

  1. JSX内联函数
  2. bind绑定
  3. 类组件构造函数绑定
  4. 箭头函数类属性
  5. 函数组件

函数组件

tsx
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

tsx
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 解决方法

  1. 用bind绑定
  2. render中bind
  3. 箭头函数类属性

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 覆盖了相同的能力:

tsx
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

tsx
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传递

tsx
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,子组件主动把方法注册给父组件

tsx
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
0 个表情
0 条评论·0 条回复
Aa
0/2000

评论加载中...