宿迁哪里有做网站开发的中山网站推广服务
2025/12/27 4:04:18 网站建设 项目流程
宿迁哪里有做网站开发的,中山网站推广服务,wordpress头部,商场商城网站建设方案欢迎各位来到今天的深度技术讲座。今天#xff0c;我们将聚焦于一个在React开发者中普遍存在的疑问#xff0c;也是一个理解React核心机制的关键点#xff1a;为什么对 ref.current 的修改不会触发 useEffect 的重新执行#xff1f;我们将从React的渲染机制、状态管理、副作…欢迎各位来到今天的深度技术讲座。今天我们将聚焦于一个在React开发者中普遍存在的疑问也是一个理解React核心机制的关键点为什么对ref.current的修改不会触发useEffect的重新执行我们将从React的渲染机制、状态管理、副作用处理等多个维度进行剖析力求为大家描绘一幅清晰的React内部工作图景。一、 React的渲染哲学何谓“响应式”在我们深入探讨ref.current与useEffect之前我们必须首先理解React应用程序的核心驱动力——渲染机制。React是一个声明式UI库它的基本哲学是你告诉React你的UI“应该”是什么样子然后React会负责将其渲染出来。这个“应该是什么样子”通常是由你的组件的props和state决定的。1.1 触发组件重新渲染的条件在React中一个组件的重新渲染re-render不是随机发生的而是由特定的事件触发的。主要有以下几种情况State变更当组件内部通过useState或useReducer管理的状态发生变化时。这是最常见也是最核心的触发机制。Props变更当父组件重新渲染时如果传递给子组件的props发生了变化即使是引用地址的变化子组件也会重新渲染。Context变更当组件消费的Context对象发生变化时。强制更新虽然不推荐但可以通过forceUpdate(类组件) 或通过改变一个不相关的useState变量来强制组件重新渲染。核心思想React的渲染是响应式的。它只响应那些被它明确标记为“可能影响UI”的数据变化即state和props。1.2 虚拟DOM与调和Reconciliation当一个组件被触发重新渲染时React并不会立即操作真实的DOM。相反它会执行组件函数重新运行组件的函数体生成一个新的React元素树Virtual DOM。比较差异将新的元素树与上一次渲染的元素树进行对比找出两者的最小差异。这个过程称为“调和”Reconciliation。更新真实DOM根据差异React批量地、高效地更新真实的浏览器DOM。这个过程的关键在于React只关心在两次渲染之间那些声明式数据state和props的变化导致了UI的逻辑变化。二、useState与useRef状态与引用的本质区别理解ref.current不触发useEffect的关键在于区分useState和useRef这两个Hook的根本用途和行为模式。它们虽然都能存储数据但在React的响应式世界中扮演着截然不同的角色。2.1useState管理响应式状态useState是React Hook中用于在函数组件中添加本地状态的机制。它返回一个状态值和一个更新该状态的函数。特点响应式通过setState函数更新状态会触发组件的重新渲染。持久性状态值在组件的多次渲染之间保持不变。批处理React会批量处理多个状态更新以优化性能。示例代码import React, { useState } from react; function Counter() { const [count, setCount] useState(0); console.log(Counter component rendered. Current count: ${count}); const increment () { setCount(prevCount prevCount 1); }; return ( div pCount: {count}/p button onClick{increment}Increment Count/button /div ); } // 行为每次点击按钮count 增加组件重新渲染console.log 会再次打印。2.2useRef持有可变引用useRefHook 用于在函数组件中创建一个可变的引用对象。它返回一个普通的JavaScript对象该对象有一个current属性并且这个对象在组件的整个生命周期中保持不变。特点非响应式直接修改ref.current的值不会触发组件的重新渲染。持久性useRef返回的引用对象在组件的多次渲染之间是同一个引用。可变性ref.current可以被直接修改就像普通的JavaScript变量一样。常见用途访问DOM节点最常见的用途是获取DOM元素的引用以便直接操作它们例如焦点管理、动画。存储可变值存储任何不希望在重新渲染之间丢失但其变化又不需要触发UI更新的值例如计时器ID、前一个状态的值。示例代码import React, { useRef, useState } from react; function RefTracker() { const renderCountRef useRef(0); const [_, setDummyState] useState(0); // 用于强制组件重新渲染 // 每次组件渲染时递增 ref.current renderCountRef.current renderCountRef.current 1; console.log(RefTracker component rendered. Ref current value: ${renderCountRef.current}); const forceReRender () { setDummyState(prev prev 1); // 修改一个不相关的state来触发重新渲染 }; return ( div pThis component has rendered {renderCountRef.current} times./p button onClick{forceReRender}Force Re-render/button p **Note:** Clicking this button forces a re-render. Directly changing renderCountRef.current does NOT cause a re-render on its own. /p /div ); } // 行为 // 1. 首次渲染renderCountRef.current 为 1。 // 2. 点击按钮setDummyState 触发重新渲染。 // 3. 重新渲染renderCountRef.current 递增到 2组件再次打印。 // 重点renderCountRef.current 的变化本身不会引起渲染。2.3 核心差异总结特性useStateuseRef用途管理需要触发UI更新的响应式状态数据存储不触发UI更新的可变值或引用DOM元素触发渲染会触发组件重新渲染不会触发组件重新渲染数据访问直接访问状态变量count通过ref.current属性访问稳定性状态值count在每次渲染时都是最新的快照useRef返回的引用对象本身是稳定的ref.current是可变的更新方式使用setState函数直接修改ref.current理解这个表格是至关重要的。useState掌控着React的响应式更新流而useRef则是React提供的一个逃生舱让你可以在不打扰React渲染机制的前提下持有和修改一些数据。三、useEffect副作用的侦听与同步机制现在我们来谈谈useEffect。它是React Hook中处理副作用side effects的机制。副作用是指那些不直接参与组件渲染但又必须在组件生命周期中执行的操作例如数据获取、订阅事件、手动修改DOM等。3.1useEffect的基本工作原理useEffect接收两个参数一个副作用函数这个函数包含了你希望执行的副作用逻辑。一个依赖项数组dependency array这是一个可选参数用于告诉React何时重新运行副作用函数。useEffect(() { // 副作用逻辑 console.log(Effect function executed!); // 可选返回一个清理函数 return () { console.log(Cleanup function executed!); }; }, [/* 依赖项数组 */]);执行时机useEffect中的副作用函数在组件首次渲染后和每次后续渲染后如果依赖项发生变化执行。清理函数如果返回在组件卸载前和每次副作用函数重新执行前运行。3.2 依赖项数组 (deps) 的作用依赖项数组是useEffect的“大脑”它决定了useEffect何时重新运行副作用。React会对依赖项数组中的每一个值进行浅比较。空数组[]表示副作用只在组件首次渲染后执行一次并且在组件卸载时执行清理函数。它告诉React这个副作用不依赖于组件的任何props或state。省略依赖项数组表示副作用在组件每次渲染后都会执行。这通常不是你想要的行为因为它可能导致性能问题或无限循环。包含依赖项当数组中的任何一个依赖项在两次渲染之间发生变化时通过浅比较副作用函数就会重新执行。示例useEffect对状态的响应import React, { useState, useEffect } from react; function EffectCounter() { const [count, setCount] useState(0); const [message, setMessage] useState(); // 依赖于 count useEffect(() { console.log(Effect 1: Count changed to ${count}); setMessage(Count is now: ${count}); return () { console.log(Effect 1 Cleanup: Before count was ${count}); }; }, [count]); // 只有当 count 变化时才重新运行 // 不依赖任何值只在首次渲染时运行 useEffect(() { console.log(Effect 2: This runs only once on initial mount.); return () { console.log(Effect 2 Cleanup: This runs only on unmount.); }; }, []); // 每次渲染都运行 (没有依赖数组) - 慎用 useEffect(() { console.log(Effect 3: This runs on every render. (Avoid this pattern normally)); }); const increment () setCount(prev prev 1); const changeMessage () setMessage(New Message!); // 这个不会触发 Effect 1 return ( div pCount: {count}/p pMessage: {message}/p button onClick{increment}Increment Count/button button onClick{changeMessage}Change Message (No Effect 1 trigger)/button /div ); }关键点useEffect的依赖项数组是React“侦听”变化并决定是否重新执行副作用的唯一机制。它只关心数组中值的身份或值是否在两次渲染之间发生了变化。四、 深度剖析ref.current的修改为何不触发useEffect现在我们已经铺垫了足够的背景知识可以直面核心问题了。答案其实隐藏在前面章节的每一个细节中。核心原因ref.current的修改不触发组件重新渲染而useEffect的依赖项检查只发生在两次渲染之间。让我们一步步来拆解这个过程ref.current的修改是“隐形”的当你直接修改myRef.current newValue;时React对此一无所知。它没有内置的机制来“观察”一个普通JavaScript对象的属性变化。这种修改发生在组件的当前渲染周期内或某个事件处理器内但它本身不会向React发出信号“嘿有数据变了可能需要重新渲染”useEffect的依赖项检查依赖于“渲染快照”useEffect在每次组件重新渲染时都会获取其依赖项数组中变量的“快照”值。然后它会将当前渲染周期的依赖项快照与上一次渲染周期的快照进行浅比较。如果ref.current不在依赖项数组中那么useEffect根本就不会关心ref.current的任何变化。它只会根据其他依赖项state、props等来决定是否运行。如果ref.current被放在依赖项数组中这是一个常见的误解和错误做法。当你将ref.current放入useEffect的依赖项数组时useEffect确实会尝试“侦听”ref.current的变化。但是这个侦听只发生在两次渲染之间。问题在于如果ref.current在一个渲染周期内被修改但这个修改本身没有触发重新渲染那么useEffect在当前这个渲染周期结束后进行下一次依赖项比较时它只会看到ref.current在上次渲染结束时的值和当前渲染结束时的值。如果ref.current在某个事件处理器中被修改然后没有其他状态或props的变化触发重新渲染那么useEffect根本就没有机会重新运行来检查这个变化。它只会保持上次运行时的状态。总结来说ref.current的修改是“本地”且“瞬时”的它不参与React的响应式更新循环。useEffect是这个响应式循环的一部分它的执行条件严格绑定于组件的重新渲染和依赖项的声明式变化。两者处于不同的“侦听”和“触发”机制中。4.1 示例代码直观演示让我们通过一个代码示例来更清晰地理解这一点。import React, { useState, useEffect, useRef } from react; function RefEffectMystery() { const [stateValue, setStateValue] useState(0); const refValue useRef(0); const refObject useRef({ count: 0 }); // 存储一个对象在ref中 console.log(--- Component Rendered ---); console.log(stateValue: ${stateValue}); console.log(refValue.current: ${refValue.current}); console.log(refObject.current.count: ${refObject.current.count}); // Effect 1: 依赖于 stateValue useEffect(() { console.log(useEffect 1 triggered: stateValue changed to ${stateValue}); // 假设这里有一些操作依赖于 stateValue }, [stateValue]); // 只有 stateValue 变化时才触发 // Effect 2: 依赖于 refValue.current useEffect(() { // 这个 effect 只有在组件重新渲染时才会检查 refValue.current 的值 // 并且只有当 refValue.current 的值在两次渲染之间发生变化时才会触发 console.log(useEffect 2 triggered: refValue.current changed to ${refValue.current}); }, [refValue.current]); // 放入 ref.current 作为依赖项 // Effect 3: 依赖于 refObject.current (整个对象引用) // 注意refObject.current 本身是一个稳定的引用不会变 // 但其内部属性 refObject.current.count 会变 useEffect(() { console.log(useEffect 3 triggered: refObject.current changed. Its count is ${refObject.current.count}); }, [refObject.current]); // 放入 ref 对象本身作为依赖项它不会变 // Effect 4: 依赖于 refObject.current.count // 这种写法是错的因为 React 无法追踪深层属性的变化 // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() { console.log(useEffect 4 triggered: refObject.current.count changed to ${refObject.current.count}); }, [refObject.current.count]); // 放入 ref.current 的属性作为依赖项 (不推荐, 容易误导) const handleUpdateRefOnly () { refValue.current 1; refObject.current.count 1; console.log(--- handleUpdateRefOnly clicked ---); console.log(refValue.current AFTER update: ${refValue.current}); console.log(refObject.current.count AFTER update: ${refObject.current.count}); // 此时组件没有重新渲染useEffect 都没有机会重新运行和检查依赖项 }; const handleUpdateStateAndRef () { setStateValue(prev prev 1); // 这个会触发重新渲染 refValue.current 10; refObject.current.count 10; console.log(--- handleUpdateStateAndRef clicked ---); }; return ( div style{{ border: 1px solid #ccc, padding: 15px, margin: 15px }} h3Ref.current useEffect Interaction/h3 pState Value: {stateValue}/p pRef Value (current): {refValue.current}/p pRef Object Count (current): {refObject.current.count}/p button onClick{handleUpdateRefOnly} Update Ref ONLY (No Re-render) /button button onClick{handleUpdateStateAndRef} Update State AND Ref (Triggers Re-render) /button p style{{ marginTop: 20px, fontWeight: bold }} Observe the console output carefully. /p /div ); }实验步骤及预期结果首次渲染RefEffectMystery组件渲染。stateValue为 0refValue.current为 0refObject.current.count为 0。useEffect 1(依赖stateValue) 触发因为stateValue初始为 0。useEffect 2(依赖refValue.current) 触发因为refValue.current初始为 0。useEffect 3(依赖refObject.current) 触发因为refObject.current初始是一个对象。useEffect 4(依赖refObject.current.count) 触发因为refObject.current.count初始为 0。点击 Update Ref ONLY 按钮handleUpdateRefOnly函数执行。refValue.current和refObject.current.count的值会在内存中改变。控制台不会显示组件重新渲染的日志。任何useEffect都不会触发。useEffect 2和useEffect 4尽管依赖于ref.current相关的值但因为没有重新渲染它们根本没有机会去检查这些依赖项是否发生了变化。UI上显示的ref.current值仍然是旧的因为UI没有更新。点击 Update State AND Ref 按钮handleUpdateStateAndRef函数执行。setStateValue会触发组件重新渲染。refValue.current和refObject.current.count的值会再次改变。控制台会显示组件重新渲染的日志。useEffect 1(依赖stateValue) 触发因为stateValue变化了。useEffect 2(依赖refValue.current) 触发因为refValue.current在两次渲染之间确实变化了 (从 0 到 10或从 1 到 11)。useEffect 3(依赖refObject.current)不会触发因为refObject.current引用本身没有改变。它始终是同一个对象。useEffect 4(依赖refObject.current.count) 触发因为refObject.current.count变化了。这个实验清楚地展示了只有当组件重新渲染时useEffect才有机会重新评估其依赖项。如果ref.current的修改没有伴随着一个状态或props的更新来触发重新渲染那么useEffect就会对此一无所知。4.2 为什么将ref.current放入依赖数组通常是误导性的当你将ref.current放入useEffect的依赖数组时你实际上是告诉React“当ref.current的值发生变化时请重新运行此effect。” 这听起来很合理但其背后的机制却不是你直觉认为的那样。ref.current的值只在渲染时被“捕获”在组件函数每次执行时ref.current的当前值会被“捕获”并放入useEffect的闭包和依赖数组中。后续的内部修改是隐形的如果你在一个事件处理器中比如onClick直接修改了ref.current这个修改发生在一个非渲染上下文non-render context中。React不会因此而重新渲染组件。因此useEffect在下一次组件被动重新渲染之前根本不会有机会去比较ref.current的新旧值。浅比较的陷阱如果ref.current存储的是一个对象并且你只是修改了该对象的内部属性例如ref.current.count那么即使ref.current在依赖数组中useEffect也不会因为这个深层属性的改变而触发。因为ref.current引用本身没有改变浅比较会认为它没有变。这便是ref.current的修改不触发useEffect的根本原因。它们在React的响应式模型中扮演着不同的角色有着不同的触发机制。五、 如何正确地响应ref.current的变化既然ref.current的直接修改无法触发useEffect那么当我们确实需要对ref.current的变化做出响应时应该如何处理呢核心原则是将非响应式的数据变化通过某种方式“桥接”到React的响应式系统。5.1 方案一结合useState或useReducer这是最常见和推荐的方法。当ref.current的变化需要影响UI或触发副作用时就应该伴随一个useState的更新。import React, { useState, useEffect, useRef } from react; function RefWithStateSync() { const inputRef useRef(null); const [inputValue, setInputValue] useState(); // 响应式状态 useEffect(() { // 首次渲染后设置焦点 if (inputRef.current) { inputRef.current.focus(); } }, []); // 只运行一次 // 监听 inputValue 的变化模拟某种副作用 useEffect(() { console.log(inputValue changed to: ${inputValue}); // 假设这里我们需要根据 input 的值做一些API调用或数据处理 if (inputValue.length 5) { console.log(Input value is long!); } }, [inputValue]); // 只有当 inputValue 变化时才触发 const handleInputChange () { if (inputRef.current) { // 1. 直接修改 ref.current 的值 // inputRef.current.value New Value from Ref; // 这种修改是可见的但不会触发组件渲染 // 2. 将 ref.current 的值同步到 state setInputValue(inputRef.current.value); // 触发组件重新渲染 } }; return ( div input ref{inputRef} typetext placeholderType something... // onChange{handleInputChange} // 如果直接绑定 onChangestate 会自动更新 // 为了演示 Ref 的情况我们手动从 Ref 读取 / button onClick{handleInputChange}Sync Input Value to State/button pCurrent input value (from state): {inputValue}/p pCurrent input value (from ref.current directly): {inputRef.current?.value}/p p **Note:** Directly typing in the input will update inputRef.current.value immediately, but inputValue (state) and thus useEffect will only update after clicking Sync. /p /div ); }在这个例子中inputRef.current.value可以在用户输入时立即改变。但useEffect只有在setInputValue被调用导致inputValue状态更新并触发组件重新渲染时才会执行。5.2 方案二使用外部事件监听器或MutationObserver当ref.current指向一个DOM元素并且你希望响应这个DOM元素自身的某些变化例如尺寸变化、属性变化、子节点变化时可以使用原生的DOM API。useEffect在这种情况下用于设置和清理这些监听器。import React, { useRef, useEffect, useState } from react; function DOMChangeTracker() { const boxRef useRef(null); const [boxWidth, setBoxWidth] useState(0); useEffect(() { if (!boxRef.current) return; // 使用 ResizeObserver 来监听 DOM 元素的尺寸变化 const resizeObserver new ResizeObserver(entries { for (let entry of entries) { if (entry.target boxRef.current) { // 当尺寸变化时更新 state setBoxWidth(entry.contentRect.width); } } }); resizeObserver.observe(boxRef.current); // 清理函数组件卸载或 effect 重新执行时停止观察 return () { resizeObserver.disconnect(); }; }, []); // 仅在组件挂载和卸载时设置/清理观察者 // 这个 effect 响应 boxWidth 状态的变化 useEffect(() { console.log(Box width changed to: ${boxWidth}px); // 可以在这里根据 boxWidth 执行其他副作用 }, [boxWidth]); return ( div div ref{boxRef} style{{ width: 50%, // 可以尝试在开发者工具中调整这个 div 的宽度 height: 100px, backgroundColor: lightblue, border: 1px solid blue, resize: horizontal, // 允许用户手动调整大小 overflow: auto, margin: 20px 0, }} Drag the bottom-right corner to resize me. /div pCurrent box width (from state): {boxWidth}px/p p **Note:** The ResizeObserver updates boxWidth state, which then triggers the useEffect. /p /div ); }在这个例子中boxRef.current的DOM元素尺寸变化本身不会触发React渲染。但是ResizeObserver会侦听到这些变化并在其回调中调用setBoxWidth。setBoxWidth会更新状态从而触发组件重新渲染进而触发依赖于boxWidth的useEffect。5.3 方案三使用useImperativeHandle(针对父子组件通信)如果你想让父组件能够“命令式地”调用子组件内部ref.current上的方法并且希望这些操作能触发子组件内部的副作用可以使用useImperativeHandle配合forwardRef。这本质上也是一种对ref操作的封装并通过内部状态管理来触发响应。import React, { useRef, useState, useEffect, forwardRef, useImperativeHandle } from react; // 子组件 const ChildComponent forwardRef((props, ref) { const [internalCount, setInternalCount] useState(0); // 暴露给父组件的方法 useImperativeHandle(ref, () ({ increment: () { setInternalCount(prev prev 1); // 修改状态触发自身渲染和副作用 }, decrement: () { setInternalCount(prev prev - 1); }, getValue: () internalCount // 返回当前状态值 })); // 监听 internalCount 的变化 useEffect(() { console.log(ChildComponent: internalCount changed to ${internalCount}); // 可以在这里执行基于 internalCount 的副作用 }, [internalCount]); return ( div style{{ border: 1px dashed green, padding: 10px, margin: 10px }} h4Child Component/h4 pInternal Count: {internalCount}/p /div ); }); // 父组件 function ParentComponent() { const childRef useRef(null); const [parentMessage, setParentMessage] useState(); const handleIncrementChild () { if (childRef.current) { childRef.current.increment(); // 调用子组件暴露的方法 setParentMessage(Child incremented! New value: ${childRef.current.getValue()}); } }; const handleDecrementChild () { if (childRef.current) { childRef.current.decrement(); setParentMessage(Child decremented! New value: ${childRef.current.getValue()}); } }; return ( div style{{ border: 1px solid purple, padding: 15px }} h3Parent Component/h3 ChildComponent ref{childRef} / button onClick{handleIncrementChild}Increment Child Count/button button onClick{handleDecrementChild}Decrement Child Count/button p{parentMessage}/p /div ); }在这个模式中父组件通过childRef.current.increment()调用子组件的方法。子组件内部的increment方法会更新internalCount状态这又会触发子组件的重新渲染和其内部useEffect的执行。这样通过useState间接实现了对ref操作的响应。六、 最佳实践与心智模型为了避免混淆和错误建立一个清晰的React心智模型至关重要State是UI的驱动力任何会影响到UI展示的数据都应该通过useState或useReducer来管理。当这些状态改变时React会重新渲染组件并更新UI。Refs是逃生舱而非数据中心useRef主要用于存储那些在组件生命周期内需要持久存在但其变化又不应触发UI更新的值或者用于直接访问DOM元素。它是一个通往命令式世界的桥梁。useEffect是同步机制useEffect的任务是协调React的声明式UI与外部系统如浏览器API、第三方库、数据请求之间的状态。它只关心在渲染之间其依赖项数组中声明的响应式数据state或props是否发生了变化。避免在useEffect依赖项中直接使用可变的ref.current值特殊情况除外除非你非常清楚你在做什么并且知道ref.current只有在伴随useState更新时才会“被侦听”到否则这很容易导致副作用不按预期执行。如果真的需要响应ref.current的内部变化通常意味着你需要将这个变化提升为state。Refs的身份是稳定的useRef返回的ref对象本身 (myRef) 在整个组件生命周期中是稳定的可以安全地放入useEffect的依赖项数组如果你想在ref对象本身被重新赋值时触发尽管这种情况非常罕见且通常不必要。但ref.current的值是可变的它的变化不会触发组件重新渲染。结语至此我们已经深入探讨了ref.current的修改为何不触发useEffect的核心原因。这并非React的缺陷而是其设计哲学和工作机制的直接体现。React通过useState和useEffect构建了一个强大的响应式系统而useRef则提供了一个必要但非响应式的逃生通道。理解它们各自的职责和交互方式是成为一名高效React开发者的基石。希望今天的讲座能帮助大家更深刻地理解React的内部运作并在日常开发中做出更明智的决策。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询