react面试之context的value变化时,内部所有子组件是否变化

文章讲述了React中的ContextAPI如何影响组件更新,以及如何通过memo函数控制组件因父组件state变化而引起的不必要的刷新。作者还讨论了如何通过拆分state和使用useMemo函数来精确控制子组件的更新行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上测试代码

// context
const state = {
    a: 1,
    b: 1,
}
const context = createContext(state);

export default context;
// A组件
const A = () => {
    const { a } = useContext(context);
    return (<div>{a}</div>)
}
export default A;

// B组件
const B = () => {
    const { b } = useContext(context);
    return (<div>{b}</div>)
}
export default B;

// C组件, 不引用context
const C = () => {
    return (<div>cccc</div>)
}
export default C;

// APP.js
function App() {
	const [a, setA] = useState(-1);
	return (
		<div>
			<Context.Provider value={{
				a,
				b
			}}>
	          <A />
	          <B />
	          <C />
	      	</Context.Provider>
	      	<button onClick={() => setA(a+1)}>+++</button>
      	</div>
     );

问,点击按钮时,ABC三个组件中谁会刷新?


答案是三个都会刷新,因为App组件中state改变了,导致所有组件都刷新了。所以并不能知道context有没有作用给B和C组件。

为了解决父组件state更新,而导致子组件更新,我们需要给ABC三个组件的export default加上memo方法,第二个参数为PropsAreEqual,直接让他返回true。

改造后为:

// A组件
// ...
export default memo(A, () => true);

// B组件
// ...
export default memo(B, () => true);

// C组件, 不引用context
// ...
export default memo(C, () => true);

此时,再次点击按钮,谁会触发更新。


答案是AB会触发更新,C不更新。

但是我们只更新了a的值,为什么没有使用a的B组件却也更新了呢?

是因为他们共同使用了一个context,而他的value是一个对象,每次都会重新生成一个新的对象,所以导致B组件以为context更新了,所以他就会触发渲染了。

所以可知,只要context的value发生改变,所有用到useContext(context)的都会更新。

如果我们在C组件里增加一个D组件,让D组件引用该context,会怎么样呢?

// C组件, 不引用context
const C = () => {
    return (<div>
	    cccc
	    <D/>
    </div>)
}
export default memo(C, () => true);
// D组件
const D = () => {
    const { b } = useContext(context);
    return (<div>{b}</div>)
}
export default memo(D, () => true);

点击按钮,C会触发更新么?

答案是不会,ABD都更新了,C没有更新。进一步验证了自己的猜想。

如果不想让BD因为a的改变而改变,该怎么办?

那就把b从state中拆解出来,重新创建一个bContext!

// App组件改为:
function App() {
const [a, setA] = useState(-1);
const [b, setB] = useState(-1);
	return (
		<div>
			<Context.Provider value={{a}}>
			  <BContext.Provider value={{b}}>
			    <A />
			    <B />
			    <C />
			  </BContext.Provider>
			</Context.Provider>
			<button onClick={() => setA(a+1)}>++a</button>
			<button onClick={() => setA(b+1)}>++b</button>
		</div>
	)
}
// B组件和D组件都更新为引用bContext
import bContext from "./context/b"
// ...
const { b } = useContext(bContext);
// ...

此时,你认为点击a按钮,BD会更新么?

答案是会更新,为什么?

因为BContext的value每次都是一个新对象,所以他认为一直在更改,所以如果想要传递对象,最好的办法就是在外层用useMemo重新定义一个。{a}同理。

// App改为
 const aVal = useMemo(() => ({ a }), [a])
 const bVal = useMemo(() => ({ b }), [b])
 
<Context.Provider value={aVal}>
	<BContext.Provider value={bVal}>
	   <A />
	   <B />
	   <C />
	 </BContext.Provider>
</Context.Provider>

此时更新a的值,并不会影响BD组件,只有A组件会更新。同时,更新b的值,也不会触发A组件的更新,只有BD更新。C则不会收到a,b两个值更新的影响。

至此,可以得出结论,context的值更新时,只会影响使用该context的组件,不会影响其他组件(前提是在memo中做好控制。)

### React 面试常见问题及解答 #### 1. 谈谈 React 中的受控组件和非受控组件的区别,并举例说明 在 React 中,**受控组件**是指其React 组件的状态控制的表单元素。用户输入,会触发事件处理函数来更新状态,进而通过状态更新重新渲染组件。例如: ```jsx class NameForm extends React.Component { constructor(props) { super(props); this.state = { value: '' }; } handleChange = (event) => { this.setState({ value: event.target.value }); }; render() { return ( <form> <input type="text" value={this.state.value} onChange={this.handleChange} /> </form> ); } } ``` 而**非受控组件**则使用 `ref` 来获取表单元素的,而不是通过 React 的状态进行管理。例如: ```jsx class NameForm extends React.Component { inputRef = React.createRef(); handleSubmit = () => { console.log(this.inputRef.current.value); }; render() { return ( <form onSubmit={this.handleSubmit}> <input type="text" ref={this.inputRef} /> <button type="submit">Submit</button> </form> ); } } ``` 受控组件更符合 React 的设计理念,适合需要实响应数据变化的场景;而非受控组件适用于简单快速获取表单的情况[^1]。 --- #### 2. 解释 React 中的 Context API 以及它的优缺点 **Context API** 是 React 提供的一种机制,用于在组件树中传递数据,而无需手动将 props 逐层传递。它通过 `React.createContext()` 创建一个上下文对象,包含 `Provider` 和 `Consumer` 组件。例如: ```jsx const ThemeContext = React.createContext('light'); // Provider 使用 value 属性提供 <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> // Consumer 在子组件中消费 <ThemeContext.Consumer> {theme => <Button theme={theme} />} </ThemeContext.Consumer> ``` **优点**: - 减少层级嵌套的 props 传递。 - 适用于全局共享的数据(如主题、语言等)。 **缺点**: - 数据变化,所有依赖该 context 的组件都会重新渲染。 - 可能导致性能问题,尤其是在大型应用中。 - 不易于调试,因为数据流不够直观[^1]。 --- #### 3. React 中如何实现代码分割(Code Splitting) 代码分割是优化加载性能的重要手段,React 支持以下几种方式实现: - **动态导入(Dynamic Import)**:结合 `React.lazy` 和 `Suspense` 实现按需加载。 ```jsx const LazyComponent = React.lazy(() => import('./LazyComponent')); function App() { return ( <React.Suspense fallback="Loading..."> <LazyComponent /> </React.Suspense> ); } ``` - **路由级代码分割**:在 React Router 中按需加载不同页面组件。 ```jsx const Home = React.lazy(() => import('./Home')); const About = React.lazy(() => import('./About')); function App() { return ( <Router> <Switch> <Route path="/" exact component={Home} /> <Route path="/about" component={About} /> </Switch> </Router> ); } ``` - **Webpack 分块配置**:通过 Webpack 的 `SplitChunksPlugin` 对代码进行自动分块[^1]。 --- #### 4. 描述 React 中的错误边界(Error Boundaries)以及如何使用 **错误边界**是一种 React 组件,可以捕获并处理其子组件树中的 JavaScript 错误,防止整个应用崩溃。它通过定义 `static getDerivedStateFromError()` 或 `componentDidCatch()` 生命周期方法实现。 示例: ```jsx class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, info) { console.error('Caught an error:', error, info); } render() { if (this.state.hasError) { return <h1>Something went wrong.</h1>; } return this.props.children; } } ``` 使用包裹可能出错的组件: ```jsx <ErrorBoundary> <MyComponent /> </ErrorBoundary> ``` 需要注意的是,错误边界无法捕获异步错误或事件处理器中的错误[^1]。 --- #### 5. 解释 React 中的 Fiber 架构以及它解决了什么问题 **Fiber 架构**是 React 16 引入的核心重构之一,旨在提高渲染性能和用户体验。传统的 React 渲染过程是同步且不可中断的,可能导致主线程阻塞,影响交互体验。 Fiber 架构的主要特点包括: - **可中断性**:允许 React 将渲染工作拆分为多个小任务,并在必要暂停、恢复或优先处理更高优先级的任务。 - **增量渲染**:逐步渲染组件树,提升复杂应用的响应速度。 - **支持异步模式**:为未来的并发模式(Concurrent Mode)打下基础。 通过这些改进,React 可以更好地应对复杂的 UI 更新需求,避免页面卡顿[^1]。 --- #### 6. React 中如何处理事件 React 的事件系统是合成事件(SyntheticEvent),它对原生浏览器事件进行了封装,确保跨浏览器一致性。事件处理通常通过内联函数或类组件的方法绑定实现。 示例: ```jsx class Button extends React.Component { handleClick = () => { console.log('Button clicked'); }; render() { return <button onClick={this.handleClick}>Click me</button>; } } ``` 对于函数组件,可以直接在 JSX 中使用箭头函数: ```jsx function Button() { const handleClick = () => { console.log('Button clicked'); }; return <button onClick={handleClick}>Click me</button>; } ``` React 的事件处理与原生事件的区别在于: - 所有事件都绑定在根节点上(事件委托)。 - 合成事件对象会被池化,不能异步访问[^1]。 --- #### 7. 如何在 React 中进行性能优化 React 提供了多种性能优化手段: - **使用 `React.memo`**:避免不必要的组件重新渲染,适用于纯组件。 ```jsx const MemoizedComponent = React.memo(MyComponent); ``` - **使用 `useCallback` 和 `useMemo`**:缓存回调函数和计算结果,减少重复创建。 ```jsx const memoizedCallback = useCallback(() => { // do something }, [deps]); ``` - **代码分割**:如前所述,使用 `React.lazy` 和 `Suspense` 按需加载组件。 - **避免不必要的状态更新**:使用不可变数据更新状态,避免直接修改 state。 - **使用 `shouldComponentUpdate`**:在类组件中自定义是否
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值