缘起余生 發表於 2023-5-8 09:46:00

理解 React 中的 useEffect、useMemo 与 useCallback

<p><img src="https://img2023.cnblogs.com/blog/1501373/202305/1501373-20230508094607697-173292978.jpg" alt="" loading="lazy"></p>
<h2 id="useeffect">useEffect</h2>
<p>先理解 useEffect 有助于学习 useMemo 和 useCallback。因为 useMemo 和 useCallback 的实现实际上都是基于 useEffect 的。</p>
<p>useEffect 是 React 中的一个很重要的 Hook,用于执行副作用操作。什么是副作用?简单来说,就是那些会改变函数外部变量或有外部可观察影响的操作。useEffect 允许你在函数组件中执行副作用操作。它会在组件每次渲染后执行副作用函数。如果指定了 deps 数组,则只有当 deps 中的某个值变化时才会重新运行副作用函数。</p>
<p>常见的副作用操作有:</p>
<ul>
<li>订阅数据:订阅某个数据源,当数据变化时更新组件 state。</li>
<li>手动更改 DOM: 通过访问 DOM 节点或使用第三方 DOM 库来改变 DOM 结构。</li>
<li>日志记录:在控制台打印日志信息。</li>
<li>计时器:通过设置 Interval 或 Timeout 来执行定时操作。</li>
<li>事件监听:为 DOM 节点添加或移除事件监听器。</li>
</ul>
<h3 id="useeffect-的两个参数">useEffect 的两个参数</h3>
<p>useEffect 根据依赖项确定是否重新执行。它接收两个参数:</p>
<ol>
<li>effect 函数:执行副作用操作的函数。</li>
<li>deps 数组 (可选):effect 函数的依赖项数组。如果指定了 deps,那么只有当 deps 中的某个值发生变化时,effect 才会被重新执行。</li>
</ol>
<pre><code class="language-jsx">useEffect (() =&gt; {
// 副作用操作
}, )// 如果指定了 deps, 则只有 deps 的值变化时才重新执行
</code></pre>
<p>举个例子:</p>
<pre><code class="language-jsx">useEffect (() =&gt; {
document.title = `你点击了 ${count} 次`
}, )// 仅当 count 变化时重新设置 document.title
</code></pre>
<p>这里的 effect 函数是设置 <code>document.title</code>,deps 是 <code></code>。这意味着只有当 count 值变化时,effect 才会重新执行,否则它会被跳过。</p>
<p>如果你传入一个空数组 <code>[]</code> 作为第二个参数,那么 effect 将只在第一次渲染时执行一次:</p>
<pre><code class="language-jsx">useEffect (() =&gt; {
document.title = `Hello!`
}, [])
</code></pre>
<p>这里的 effect 函数仅在组件初始渲染时执行一次,因为 <code>[]</code> 意味着该 effect 没有任何依赖项。</p>
<p>如果你不指定第二个参数,那么 effect 将在每次渲染后执行:</p>
<pre><code class="language-jsx">useEffect (() =&gt; {
document.title = `你点击了 ${count} 次`
})
</code></pre>
<p>这里的 effect 在每次渲染后都会执行,因为我们没有指定 deps。</p>
<p>总结一下,useEffect 的两个参数:</p>
<ul>
<li>effect 函数:执行副作用操作的函数。</li>
<li>deps 数组 (可选):effect 函数的依赖项数组。
<ul>
<li>如果指定了 deps,那么只有 deps 中的值变化时,effect 才会重新执行。</li>
<li>如果传入 <code>[]</code> 作为 deps,则 effect 只会在第一次渲染时执行。</li>
<li>如果不指定 deps,则 effect 将在每次渲染后执行。</li>
</ul>
</li>
</ul>
<p>理解 effect 函数和 deps 数组是使用 useEffect 的关键。effect 负责执行具体的副作用操作,而 deps 控制 effect 的执行时机。</p>
<h3 id="会执行两次的-useeffect">会执行两次的 useEffect</h3>
<p>在开发环境下,useEffect 第二个参数设置为空数组时,组件渲染时会执行两次。这是因为 React 在开发模式下会执行额外的检查,以检测 Trumpkin 警告并给出更好的错误信息。当你指定 <code>[]</code> 作为 deps 时,这意味着 effect 没有任何依赖项,所以它应该只在组件挂载时执行一次。但是,在第一次渲染时,React 无法确定 deps 是否会在将来的渲染中发生变化。所以它会在初始渲染时执行一次 effect,然后在 “调用阶段” 再执行一次,以确保如果 deps 发生变化,effect 也会再次执行。如果在 “调用阶段” 重新渲染时 deps 仍然为 <code>[]</code>,那么 React 会更新内部状态,记录该 effect 确实没有依赖项,并在将来的渲染中跳过 “调用阶段” 重新执行的步骤。</p>
<p>这就是在开发环境下 effect 会执行两次的原因。这种行为只在开发模式下发生,在生产模式下 effect 只会执行一次。目的是为了提高开发体验,给出更清晰的错误提示。如果 effect 的 deps 发生变化但没有再次执行,React 可以明确地给出警告。而在生产模式下,这样的检查是不必要的,所以 effect 只会执行一次以减少性能开销。</p>
<p>总结一下:当你在开发环境下使用 useEffect 并指定 <code>[]</code> 作为依赖项时,effect 函数会在初始渲染时执行两次。这是因为 React 会在 “调用阶段” 再次执行 effect,以检查依赖项是否发生变化,给出更清晰的警告信息。如果 deps 仍然为 <code>[]</code>,那么 React 会更新状态并在将来跳过 “调用阶段” 的重新执行。这种行为只在开发模式下发生,生产模式下 effect 只会执行一次。</p>
<details>
<summary> 什么是 Trumpkin 警告?</summary>
<p>Trumpkin 警告是 useEffect Hook 的一种错误警告。它会在开发环境下出现,用来表示 effect 函数中使用的某个状态或 props 在依赖项 deps 中遗漏。</p>
<p>比如:</p>
<pre><code class="language-jsx">function Counter () {
const = useState (0);

useEffect (() =&gt; {
    document.title = `You clicked ${count} times`;
});// 没有指定 deps
}
</code></pre>
<p>这里,effect 函数使用了 count state,但我们没有将它添加到 deps 中。所以 React 会在开发环境下给出 Trumpkin 警告: React Hook useEffect has a missing dependency: 'count'. Either include it or remove the dependency array.</p>
<p>这是为了提示我们 count 状态发生变化时,effect 函数并不会重新执行,这很可能是个 bug。要修复这个警告,我们有两种选择:</p>
<ol>
<li>添加 count 到 deps:</li>
</ol>
<pre><code class="language-jsx">useEffect (() =&gt; {
document.title = `You clicked ${count} times`;
}, );
</code></pre>
<ol start="2">
<li>如果 effect 不依赖任何值,传入空数组 <code>[]</code>:</li>
</ol>
<pre><code class="language-jsx">useEffect (() =&gt; {
document.title = `You clicked ${count} times`;
}, []);
</code></pre>
<p>为什么说此时可能是个 bug?当你不指定 useEffect 的第二个参数 (deps) 时,effect 回调函数会在每次渲染后执行。但是,这并不意味着 effect 中使用的所有状态和 props 都会在 effect 重新执行时更新。 effect 执行时所使用的变量会被创建出一个闭包,它会捕获 effect 创建时那一刻变量的快照。所以,如果 effect 使用了某个状态,但没将其添加到依赖项 deps 中,当那个状态更新时,effect 中仍然会使用旧的值。 这很可能导致 bug。</p>
<pre><code class="language-jsx">function Counter () {
const = useState (0);

useEffect (() =&gt; {
    document.title = `You clicked ${count} times`;// 使用了 count 但没有指定为依赖
});
}
</code></pre>
<p>这里,effect 中使用了 count 状态,但是我们没有将它添加到 deps 中。在第一次渲染时,count 为 0,所以 document.title 会被设置为 "You clicked 0 times"。如果我们随后将 count 更新为 1, 你可能会期望 document.title 也变为 "You clicked 1 times"。但是,当 effect 被执行时,它会捕获 count 的 “旧值” 0。所以 document.title 实际上仍然会是 "You clicked 0 times"。 count 的更新并没有触发 effect 的重新执行。</p>
<p>这就是 Trumpkin 警告出现的原因,React 会检测到 effect 中使用了某个状态,但没有在依赖项 deps 中指定它,这很有可能导致 bug。 所以 Trumpkin 警告的目的是在开发环境下检测这样的错误,并给出清晰的提示以修复它们。</p>
</details>
<details>
<summary> 了解 React 中的 “调用阶段”</summary>
<p>React 在初次渲染后会再次执行 useEffect Hook 的调用,以校验是否有依赖项被遗漏从而产生 Trumpkin 警告。在上例中,React 在第一次渲染时会执行一次 effect,然后在 “调用阶段” 再次执行 effect。这时,它会检测到 count 状态被使用但未在 deps 中指定,所以会产生 Trumpkin 警告。如果 deps 指定为 <code>[]</code>,在 “调用阶段” 的重新执行中它会检测到 deps 没有变化,所以会更新内部状态并在将来的渲染中跳过这个额外步骤(调用阶段)。</p>
</details>
<h3 id="useeffect-的实现">useEffect 的实现</h3>
<p>effect 函数会创建一个闭包,捕获函数内部使用的所有状态和 props 的值。这是因为 Javascript 中的函数会隐式创建闭包。当 effect 第一次执行时,它会读取函数内使用的所有状态和 props,并将其值保存到闭包中。</p>
<p>举个例子:</p>
<pre><code class="language-jsx">function Counter () {
const = useState (0);

useEffect (() =&gt; {
    const foo = count;// 读取 count 并存入闭包
    document.title = `You clicked ${foo} times`;
});
}
</code></pre>
<p>这里,foo 变量是定义在 effect 函数内部的。当 effect 第一次执行时,它会读取 count 的当前值 0,并将其保存到 foo 中。foo 变量及其所捕获的 0 值都被保存在 effect 的闭包中。即使后续我们将 count 更新为 1,当 effect 重新执行时,它仍然会在闭包中找到 foo 变量,其值为 0。所以 document.title 不会更新。除非我们指定 <code></code> 作为 effect 的依赖项:</p>
<pre><code class="language-jsx">useEffect (() =&gt; {
const foo = count;
document.title = `You clicked ${foo} times`;
}, );
</code></pre>
<p>现在,每当 count 更新时,effect 会重新执行。它会再次读取 count 的最新值,并将其保存到闭包的 foo 中:</p>
<ul>
<li>第一次执行:count 为 0,foo 被设置为 0</li>
<li>count 更新为 1:effect 重新执行,读取 count 为 1,将其保存到 foo 中,覆盖之前的值</li>
<li>以此类推...</li>
</ul>
<p>要实现这个效果,有两个关键点:</p>
<ol>
<li>Javascript 函数会隐式创建闭包,用来存储函数内定义的变量和其值。</li>
<li>effect 会在第一次执行时读取所有使用的状态和 props 的值,并将其保存到闭包中。除非 deps 发生变化,否则 effect 在重新执行时会使用闭包中的 “旧值”。</li>
</ol>
<p>这就是 effect 如何通过闭包捕获变量值的实现机制。理解这一点,以及如何通过依赖项 deps 避免使用 “旧值” 导致的 bug,是使用 useEffect 的关键。</p>
<p>在 useEffect 中指定了 deps 依赖项时,它会在 deps 中的任何值变化时重新运行 effect 函数。这时,它会重新读取最新的值,而不是使用闭包中的 “旧值”。这是通过在 effect 函数内部重新声明状态和 props 的值来实现的。每当 effect 重新运行时,它会捕获那一刻的最新值,然后替换闭包中的 “旧值”。</p>
<p>举个例子:</p>
<pre><code class="language-jsx">function Counter () {
const = useState (0);

useEffect (() =&gt; {
    const foo = count;// 重新读取 count 的最新值
    document.title = `You clicked ${foo} times`;
}, );// 指定 count 作为依赖项
}
</code></pre>
<p>在第一次执行时,foo 被设置为 count 的初始值 0。当我们更新 count 为 1 时,effect 会重新运行,因为我们指定了 <code></code> 作为依赖项。这时,effect 会再次读取 count,现在其值为 1。它会将 1 赋值给 foo,覆盖闭包中的 “旧值” 0。所以每当 effect 重新运行时,它都会重新读取状态和 props 的最新值,并更新闭包中的值。这确保了在 effect 函数中,我们总是使用的是最新的,而不是旧的闭包值。在 React 源码中,这是通过在 effect 重新运行时调用 create 子函数来实现的:</p>
<pre><code class="language-js">function useEffect (create, deps) {
//...
function recompute () {
    const newValue = create ();// 重新运行子函数,读取最新值
    storedValue.current = newValue;// 更新闭包中的值
}

if (depsChanged) recompute ();   // 如果 deps 变化,重新计算
}
</code></pre>
<p>每当依赖项 deps 变化时,React 会调用 recompute 函数来重新运行 create 子函数。create 会读取最新的状态和 props 值,并将新值保存到存储变量 storedValue 中,覆盖之前的值。所以,通过在 effect 重新运行时重新读取值并更新存储变量,React 确保你总是在 effect 函数中使用最新的 props 和状态,而不是闭包捕获的 “旧值”。这就是 useEffect 在指定了 deps 依赖项时如何避免使用闭包中的 “旧值” 的实现机制。</p>
<p>每当 deps 变化,它会重新运行 effect 并读取最新的值,更新存储在闭包中的值。当你不指定 useEffect 的依赖项 deps 时,effect 函数会在每次渲染后运行。这时,effect 在重新运行时会继续使用闭包中的 “旧值”,而不是读取最新的状态和 props 值。这是因为没有指定依赖关系,所以 React 认为 effect 不依赖于任何值的变化。在源码中,这是通过不调用 recompute 函数来实现的。recompute 函数负责在依赖项变化时重新运行 effect 并更新闭包值。所以简单来说,当你不指定 deps 时,effect 在重新运行时什么也不会做 —— 它会继续使用之前闭包中的值。</p>
<p>举个例子:</p>
<pre><code class="language-jsx">function Counter () {
const = useState (0);

useEffect (() =&gt; {
    const foo = count;
    document.title = `You clicked ${foo} times`;
});
}
</code></pre>
<p>在第一次渲染时,foo 被设置为 count 的初始值 0。当我们更新 count 为 1 时,effect 会重新运行,但这时它不会重新读取 count。它会继续使用闭包中存储的 foo,其值仍为 0。<br>
所以 document.title 不会更新,它将保持 "You clicked 0 times"。这是因为我们没有指定 deps 数组,所以 React 认为 effect 不依赖任何值。每次渲染后重新运行 effect 仅仅是为了刷新副作用。它并不会读取最新的 props 或状态值。在源码中,effect 的重新运行如下所示:</p>
<pre><code class="language-js">function useEffect (create, deps) {
//...
if (didRender) {
    // 重新运行 effect, 但不会重新计算值
    create ();
}

if (depsChanged) recompute ();
}
</code></pre>
<p>所以当你不指定 deps 时,didRender 值会在每次渲染后变为 true,从而重新运行 effect。但是,由于 depsChanged 总是 false,所以 recompute 函数不会被调用。effect 在重新运行时只会调用 create 函数,但不会重新读取值或更新闭包中的存储值。所以它会继续使用闭包中的 “旧值”,而不是最新的 props 和状态。这就是当不指定依赖项 deps 时,useEffect 作用在重新运行时如何继续使用闭包中的 “旧值” 而非最新值的实现机制。</p>
<p>在 useEffect 源码中,“旧值” 是通过 useRef hook 保存的。useRef 返回一个可变的 ref 对象,其 .current 属性被用于存储任何值,这个值在组件的整个生命周期中持续存在。所以,useEffect 使用 useRef 来保存第一次执行时读取的 props 和状态的值,这些就是所谓的 “旧值”。</p>
<p>useEffect 的简化实现如下:</p>
<pre><code class="language-js">function useEffect (create, deps) {
const storedValue = useRef (null);// 使用 useRef 保存旧值

function recompute () {
    const newValue = create ();   // 重新运行,读取最新值
    storedValue.current = newValue;// 更新旧值
}

if (didRender) {
    create ();// 重新运行,使用旧值 storedValue.current
}

if (depsChanged) recompute ();// 如果 deps 变化,重新计算新值
}
</code></pre>
<ul>
<li>在初始渲染时,会运行 create 函数,读取一些值并将其赋值给 storedValue。这些就是 “旧值”。</li>
<li>如果没有指定依赖项,didRender 将在每次渲染后变为 true,重新运行 create 函数,但这时仍使用存储在 storedValue 中的 “旧值”。</li>
<li>如果指定了依赖项 deps,且 deps 发生变化,recompute 函数会重新运行 create,读取最新的值,并将其更新到 storedValue 中,覆盖 “旧值”。</li>
<li>如果依赖项 deps 没有变化,什么也不会发生 ——storedValue 中的 “旧值” 会继续被使用。</li>
</ul>
<p>所以,useRef hook 被用来在 effect 的多次执行之间保存 props 和状态的 “旧值”。每当依赖关系无变化时,这些 “旧值” 会继续被使用。通过指定依赖项,你可以确保在值变化时重新运行 effect, 并使用最新的 props 和状态值更新存储的 “旧值”。这就是 useEffect 源码中 “旧值” 如何被保存及使用的实现机制。理解它对于掌握 useEffect 的工作原理非常重要。</p>
<p>create 函数可以读取 storedValue 的值,因为:</p>
<ol>
<li>storedValue 是在 effect 函数内声明的。</li>
<li>create 函数也是在 effect 函数内定义的,所以它可以访问 effect 作用域中的变量,包括 storedValue。</li>
<li>这是闭包的结果,create 函数会捕获 surrounding scope 的变量,使其值得以在多次调用之间保持。</li>
</ol>
<p>举个例子:</p>
<pre><code class="language-js">function useEffect (create, deps) {
const storedValue = useRef (null);

function effect () {
    const create = () =&gt; {
      console.log (storedValue.current);// 可以访问 storedValue
    }
}

//...
}
</code></pre>
<p>这里,create 函数被定义在 effect 函数内部。所以它可以访问 effect 作用域中的变量,包括 storedValue。当 create 函数在后续调用中运行时,它会继续使用创建时捕获的 storedValue 变量。这是闭包的结果 —— 即使 effect 函数完成执行,create 函数所捕获的变量也会被保留在内存中,供后续调用使用。</p>
<p>总结一下:</p>
<ol>
<li>create 和 storedValue 都是在 effect 内声明的,所以 create 可以访问 storedValue。</li>
<li>create 函数捕获了 surrounding scope 的变量,使得这些变量在函数调用之间保持其值。这就是闭包。</li>
<li>所以,每当 create 被调用时,它都可以访问之前声明的 storedValue,并读取其当前值。</li>
<li>这就是 create 如何可以在多次调用之间共享并访问同一个 storedValue 的机制。</li>
</ol>
<p>这一点对理解 useEffect 的工作原理很重要。 effect 中声明的变量和函数都会被捕获在闭包中,并在多次 effect 执行之间共享。理解了这一点,useEffect 中 “旧值” 的保存和读取机制也就很清楚了。</p>
<p>在 useEffect 源码中,effect 函数会在以下情况被调用:</p>
<ol>
<li>在组件初始渲染时。此时它会执行 effect,读取 props 和状态的值,并将其存储以供后续执行使用。</li>
<li>如果你指定了依赖项 deps,且 deps 中的任何值发生变化时。这时它会重新执行 effect,读取最新的 props 和状态值,并更新存储的 “旧值”。</li>
<li>如果你不指定依赖项 deps,则在每次渲染后都会调用 effect。这时它会继续使用存储的 “旧值”。</li>
</ol>
<p>大致的 useEffect 实现如下:</p>
<pre><code class="language-js">function useEffect (create, deps) {
const effect = () =&gt; {
    const newValue = create ();// 读取最新值
    storedValue.current = newValue;// 更新旧值
}

if (!deps) {
    didRender = true;// 没有依赖项,每次渲染后运行
}

if (didRender) effect ();   // 运行 effect

if (deps &amp;&amp; depsChanged) {
    effect ();   // 如果有依赖项且变化了,运行 effect
}
}
</code></pre>
<p>根据是否指定了依赖项 deps 及其是否发生变化,effect 会在以下情况被调用:</p>
<ol>
<li>第一次渲染。此时会调用 effect,将 create 函数读取的值存储为 “旧值”。</li>
<li>如果指定了 deps 但未变化,什么也不会发生。继续使用存储的 “旧值”。</li>
<li>如果指定了 deps 且其发生变化,会调用 effect,通过 create 函数读取最新的值,并更新存储的 “旧值”。</li>
<li>如果不指定 deps,didRender 会在每次渲染后变为 true,从而调用 effect。但这时会继续使用存储的 “旧值”。</li>
</ol>
<p>effect 函数的调用与是否指定依赖项 deps 及 deps 是否发生变化直接相关。理解 effect 根据这些条件的不同调用方式,是理解 useEffect 的关键。useEffect 会在合适的时机调用 effect,以执行必要的副作用操作,同时确保你在 effect 中总是使用最新的 props 和状态值。这就是 useEffect 的强大之处。</p>
<details>
<summary>useEffect 大致实现 </summary>
<pre><code class="language-js">function useEffect (create, deps) {
const effect = () =&gt; {
    const newValue = create ();// Re-run create and get new value
    storedValue.current = newValue;// Update stored value
};

const storedValue = useRef (null);

const = useState (false);

if (didRenderRef.current &amp;&amp; deps === undefined) {
    throw new Error ('Must either specify deps or no deps');
}

const prevDeps = useRef (deps);
const didRenderRef = useRef (false);

if (depsChanged || !prevDeps.current) {
    prevDeps.current = deps;// Update prevDeps ref
    didRenderRef.current = true;
}

useLayoutEffect (() =&gt; {
    if (didRenderRef.current &amp;&amp; !depsChanged &amp;&amp; prevDeps.current !== deps) {
      setDepsChanged (true);// Trigger re-run of effect
    }
});

// Call the effect
useLayoutEffect (() =&gt; {
    effect ();
});

// Re-run effect if deps change
useLayoutEffect (() =&gt; {
    if (depsChanged &amp;&amp; didRenderRef.current) {
      effect ();
      didRenderRef.current = false;
      setDepsChanged (false);
    }
}, deps);

// Always re-run on mount
useLayoutEffect (effect, []);
}
</code></pre>
</details>
<h2 id="usememo">useMemo</h2>
<p>useMemo 用于优化组件的渲染性能。它会在依赖项变化时重新计算 memoized 值,并且只在依赖项变化时重新渲染组件。你应该在以下情况使用 useMemo:</p>
<ol>
<li>昂贵的计算:如果你有一个复杂的计算,它应该只在某些依赖项变化时重新运行,那么 useMemo 非常有用。它会记住最后计算的值,并仅在依赖项变化时重新计算。</li>
<li>避免不必要的渲染:如果你有一个组件,它在重新渲染时执行昂贵的 DOM 操作,那么你应该通过 useMemo 来优化它,使其只在依赖项变化时重新渲染。</li>
<li>依赖项变化时才重新计算值:如果你想基于 props 的某些值来计算一些数据,并且你只想在依赖 props 值变化时重新计算该数据。</li>
</ol>
<p>在 React 函数组件中,每当组件重新渲染时,其函数体都会被执行。这意味着任何计算的数据或渲染的元素都会重新计算和重新创建。这通常没什么问题,但如果计算或渲染代价高昂,它可能会造成性能问题。</p>
<p>举个例子:</p>
<pre><code class="language-jsx">const expensiveComputation = (a, b) =&gt; {
// 做一些昂贵的计算...
return result;
}

function MyComponent () {
const = useState (1);
const = useState (1);

const result = expensiveComputation (a, b);
//...
}
</code></pre>
<p>在这里,每当组件重新渲染时,expensiveComputation 都会被调用,即使 a 和 b 没有变化。使用 useMemo 可以解决这个问题:</p>
<pre><code class="language-jsx">function MyComponent () {
const = useState (1);
const = useState (1);
const result = useMemo (() =&gt; expensiveComputation (a, b), );

//...
}
</code></pre>
<p>现在,result 只会在 a 或 b 变化时重新计算。所以,useMemo 的主要目的就是为了避免 React 函数组件不必要的重复计算,提高组件的性能。</p>
<h3 id="usememo-的实现">useMemo 的实现</h3>
<p>useMemo 的实现比较简单,它基本上是 useEffect 的一个特例。</p>
<pre><code class="language-jsx">function useMemo (nextCreate, deps) {
currentlyRenderingMemo++;

const create = useRef (nextCreate);
const depsRef = useRef (deps);

function recompute () {
    currentlyRenderingMemo++;
    const memoizedValue = create.current ();
    memoized.current = memoizedValue;
    currentlyRenderingMemo--;
}

if (deps.current !== deps) {
    deps.current = deps;
    recompute ();
}

const memoized = useRef (null);
if (currentlyRenderingMemo === 0) {
    recompute ();
}

return memoized.current;
}
</code></pre>
<p>它做了以下几件事:</p>
<ol>
<li>当前渲染的 useMemo 数量加 1。这是为了避免在嵌套的 useMemo 调用中重复运行 effects。</li>
<li>用 useRef 创建对 create 函数和 deps 数组的引用。</li>
<li>定义 recompute 函数来调用 create 函数并更新 memoized 值。</li>
<li>如果 deps 变化了,调用 recompute 来重新计算 memoized 值。</li>
<li>如果这是第一个 useMemo 调用,调用 recompute 来计算初始 memoized 值。</li>
<li>返回 memoized 值。</li>
<li>在组件卸载时,自动清空 refs,相当于运行过清理函数。</li>
</ol>
<p>所以本质上,它只在依赖项变化时重新运行 create 函数,并记住最后的值,这与 useEffect 有些相似。但 useMemo 专注于记忆化值,而不产生任何副作用。这就是 React 中 useMemo 的简单实现原理。它通过跟踪依赖项和缓存上次计算的值来优化组件渲染性能。</p>
<h2 id="usecallback">useCallback</h2>
<p>useCallback 与 useMemo 类似,它也是用于优化性能的。但是它用于记忆化函数,而不是值。useCallback 会返回一个 memoized 回调函数,它可以确保函数身份在多次渲染之间保持不变,仅在某个依赖项变化时才会更新,这可以用于避免在每次渲染时都创建新的函数实例。所以,当你有一个会在多次渲染之间保持不变的函数时,使用 useCallback 是一个很好的优化手段。</p>
<p>举个例子,当你有一个函数作为事件处理程序时,它通常在创建后就不会改变。但是,如果你直接在渲染方法中定义这个函数,它会在每次渲染时重新创建。</p>
<pre><code class="language-jsx">function MyComponent () {
const = useState (1);

function handleClick () {
    setCount (c =&gt; c + 1);
}

return &lt;button onClick={handleClick}&gt;Increment&lt;/button&gt;
}
</code></pre>
<p>这里,handleClick 函数在每次渲染时都会重新定义。虽然它的逻辑在多次渲染之间没有变化。</p>
<p>使用 useCallback 优化这段代码:</p>
<pre><code class="language-jsx">function MyComponent () {
const = useState (1);

const handleClick = useCallback (() =&gt; {
    setCount (c =&gt; c + 1);
}, []);// 依赖项 [] 表示仅在第一次渲染时创建

return &lt;button onClick={handleClick}&gt;Increment&lt;/button&gt;
}
</code></pre>
<p>现在,handleClick 只会在第一次渲染时创建。在随后的渲染中,它都指向同一个函数实例。这可以避免在每次渲染时创建新的事件处理程序,从而优化组件的性能。</p>
<p>总结一下: useCallback 的主要作用是:</p>
<ol>
<li>记忆化函数实例,避免在每次渲染时创建新的函数。</li>
<li>当函数作为 props 传递给子组件时,可以让子组件避免不必要的重新渲染。</li>
</ol>
<p>与 useMemo 类似,你应该在依赖项变化时才更新回调函数。否则,它就没有意义了。总之,useCallback 主要用于性能优化,通过记忆化函数实例来避免不必要的重新创建和重新渲染。</p>
<h3 id="usecallback-的实现">useCallback 的实现</h3>
<p>useCallback 的实现也比较简单。它基本上就是用 useMemo 来记忆化一个函数。</p>
<pre><code class="language-js">function useCallback (callback, deps) {
return useMemo (() =&gt; callback, deps);
}
</code></pre>
<p>它直接调用 useMemo,传入 callback 函数和 deps 数组。</p>
<p>所以,useCallback 的工作原理是:</p>
<ol>
<li>在第一次渲染时,调用 callback 函数并记住结果。</li>
<li>在后续渲染中,如果 deps 没有变化,直接返回上次记住的函数。</li>
<li>如果 deps 变化了,再次调用 callback 并记住新结果。</li>
<li>在组件卸载时,自动清理 useMemo 的副作用。</li>
</ol>
<p>所以本质上,它就是把函数当作 useMemo 的创建函数来调用,并根据依赖项决定是否需要重新创建函数实例。这与事件处理程序的例子非常吻合。</p>
<p>举个具体例子:</p>
<pre><code class="language-js">function useCallback (callback, ) {
return useMemo (() =&gt; {
    callback ()   // 只在第一次渲染时调用
}, )      // 如果 a 或 b 变化时重新调用 callback
}
</code></pre>
<p>那么第一次渲染时会立即调用 callback,并记住结果。如果后续 a 或 b 变化,callback 会再次被调用,并更新记忆的值。如果 a 和 b 保持不变,直接返回上次记住的函数实例。这就是 useCallback 的简单实现原理。它通过将函数实例记忆化来确保函数身份在多次渲染之间保持一致,从而优化性能。综上,useCallback 的实现是基于 useMemo 的。它利用 useMemo 的记忆化特性来记忆化函数,以此来提高组件渲染性能。</p><br><br>
来源:https://www.cnblogs.com/guangzan/p/17329688.html
頁: [1]
查看完整版本: 理解 React 中的 useEffect、useMemo 与 useCallback