بررسی دقیق بهینهسازی توابع Ref در React. دریابید چرا دوبار فراخوانی میشوند، چگونه با useCallback از آن جلوگیری کنید و بر عملکرد برنامههای پیچیده مسلط شوید.
تسلط بر توابع Ref در React: راهنمای نهایی بهینهسازی عملکرد
در دنیای توسعه وب مدرن، عملکرد فقط یک ویژگی نیست؛ بلکه یک ضرورت است. برای توسعهدهندگانی که از React استفاده میکنند، ایجاد رابطهای کاربری سریع و واکنشگرا یک هدف اصلی است. در حالی که DOM مجازی React و الگوریتم آشتیسازی بیشتر کارهای سنگین را انجام میدهند، الگوها و APIهای خاصی وجود دارند که درک عمیق آنها برای باز کردن قفل عملکرد اوج بسیار مهم است. یکی از این حوزهها، مدیریت Refs است، به طور خاص، رفتار اغلب سوءتعبیر شده توابع Ref.
Refs راهی برای دسترسی به گرههای DOM یا عناصر React ایجاد شده در روش رندر ارائه میدهند - یک راه فرار ضروری برای کارهایی مانند مدیریت فوکوس، راهاندازی انیمیشنها یا ادغام با کتابخانههای DOM شخص ثالث. در حالی که useRef به یک استاندارد برای موارد ساده در کامپوننتهای تابعی تبدیل شده است، توابع Ref کنترل دقیقتر و قدرتمندتری بر زمان تنظیم و لغو یک مرجع ارائه میدهند. با این حال، این قدرت با یک ظرافت همراه است: یک تابع Ref میتواند چندین بار در طول چرخه حیات یک کامپوننت اجرا شود، که به طور بالقوه منجر به گلوگاههای عملکرد و اشکالات در صورت عدم مدیریت صحیح میشود.
این راهنمای جامع تابع Ref در React را رمزگشایی میکند. ما بررسی خواهیم کرد:
- توابع Ref چیست و چگونه با سایر انواع Ref متفاوت است.
- دلیل اصلی اینکه چرا توابع Ref دو بار فراخوانی میشوند (یک بار با
nullو یک بار با عنصر). - مشکلات عملکرد استفاده از توابع درونخطی برای توابع Ref.
- راه حل قطعی برای بهینهسازی با استفاده از هوک
useCallback. - الگوهای پیشرفته برای مدیریت وابستگیها و ادغام با کتابخانههای خارجی.
در پایان این مقاله، دانش لازم را برای استفاده از توابع Ref با اطمینان خواهید داشت و اطمینان حاصل خواهید کرد که برنامههای React شما نه تنها قوی هستند، بلکه از عملکرد بالایی نیز برخوردارند.
یک یادآوری سریع: توابع Ref چیست؟
قبل از اینکه به بهینهسازی بپردازیم، بیایید به طور خلاصه بررسی کنیم که تابع Ref چیست. به جای ارسال یک شی Ref ایجاد شده توسط useRef() یا React.createRef()، یک تابع را به ویژگی ref ارسال میکنید. این تابع هنگام Mount و Unmount شدن کامپوننت توسط React اجرا میشود.
React تابع Ref را با عنصر DOM به عنوان آرگومان هنگام Mount شدن کامپوننت فراخوانی میکند و هنگام Unmount شدن کامپوننت، آن را با null به عنوان آرگومان فراخوانی میکند. این به شما کنترل دقیقی در لحظات دقیقی میدهد که مرجع در دسترس قرار میگیرد یا در شرف نابودی است.
در اینجا یک مثال ساده در یک کامپوننت تابعی آورده شده است:
import React, { useState } from 'react';
function TextInputWithFocusButton() {
let textInput = null;
const setTextInputRef = element => {
console.log('Ref callback fired with:', element);
textInput = element;
};
const focusTextInput = () => {
// Focus the text input using the raw DOM API
if (textInput) textInput.focus();
};
return (
<div>
<input type="text" ref={setTextInputRef} />
<button onClick={focusTextInput}>
Focus the text input
</button>
</div>
);
}
در این مثال، setTextInputRef تابع Ref ما است. هنگامی که عنصر <input> رندر میشود، با آن فراخوانی میشود و به ما امکان میدهد آن را ذخیره کرده و بعداً از آن برای فراخوانی focus() استفاده کنیم.
مشکل اصلی: چرا توابع Ref دو بار اجرا میشوند؟
رفتار اصلی که اغلب توسعهدهندگان را گیج میکند، فراخوانی مضاعف تابع است. وقتی کامپوننتی با یک تابع Ref رندر میشود، تابع Ref معمولاً دو بار پشت سر هم فراخوانی میشود:
- اولین فراخوانی: با
nullبه عنوان آرگومان. - دومین فراخوانی: با نمونه عنصر DOM به عنوان آرگومان.
این یک باگ نیست. این یک انتخاب طراحی عمدی توسط تیم React است. فراخوانی با null نشان میدهد که Ref قبلی (در صورت وجود) در حال جدا شدن است. این به شما فرصتی حیاتی برای انجام عملیات پاکسازی میدهد. به عنوان مثال، اگر یک Event Listener به گره در رندر قبلی متصل کردهاید، فراخوانی null لحظه مناسبی برای حذف آن قبل از اتصال گره جدید است.
مشکل، با این حال، این چرخه Mount/Unmount نیست. مسئله عملکرد واقعی زمانی ایجاد میشود که این فراخوانی دوگانه در هر رندر مجدد اتفاق میافتد، حتی زمانی که وضعیت کامپوننت به شکلی به روز میشود که کاملاً نامربوط به خود Ref باشد.
تله توابع درونخطی
این پیادهسازی به ظاهر بیگناه را در داخل یک کامپوننت تابعی که دوباره رندر میشود، در نظر بگیرید:
import React, { useState } from 'react';
function FrequentUpdatesComponent() {
const [count, setCount] = useState(0);
return (
<div>
<h3>Counter: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<div
ref={(node) => {
// This is an inline function!
console.log('Ref callback fired with:', node);
}}
>
I am the referenced element.
</div>
</div>
);
}
اگر این کد را اجرا کنید و روی دکمه "Increment" کلیک کنید، موارد زیر را در کنسول خود در هر کلیک مشاهده خواهید کرد:
Ref callback fired with: null
Ref callback fired with: <div>...</div>
چرا این اتفاق میافتد؟ زیرا در هر رندر، شما یک نمونه تابع جدید برای ویژگی ref ایجاد میکنید: (node) => { ... }. در طول فرآیند آشتیسازی، React ویژگیها را از رندر قبلی با رندر فعلی مقایسه میکند. متوجه میشود که ویژگی ref تغییر کرده است (از نمونه تابع قدیمی به نمونه جدید). قرارداد React واضح است: اگر تابع Ref تغییر کند، ابتدا باید Ref قدیمی را با فراخوانی آن با null پاک کند و سپس با فراخوانی آن با گره DOM، Ref جدید را تنظیم کند. این چرخه پاکسازی/راهاندازی را به طور غیرضروری در هر رندر راهاندازی میکند.
برای یک console.log ساده، این یک ضربه جزئی به عملکرد است. اما تصور کنید تابع شما کار پرهزینهای انجام میدهد:
- اتصال و جدا کردن Event Listener های پیچیده (به عنوان مثال،
scroll،resize). - مقداردهی اولیه یک کتابخانه سنگین شخص ثالث (مانند یک نمودار D3.js یا یک کتابخانه نقشهبرداری).
- انجام اندازهگیریهای DOM که باعث Reflow طرحبندی میشوند.
اجرای این منطق در هر بهروزرسانی وضعیت میتواند به شدت عملکرد برنامه شما را کاهش دهد و اشکالات ظریف و ردیابی دشوار را معرفی کند.
راه حل: Memoizing با `useCallback`
راه حل این مشکل این است که اطمینان حاصل شود که React دقیقاً همان نمونه تابع را برای تابع Ref در رندرهای مجدد دریافت میکند، مگر اینکه صریحاً بخواهیم آن را تغییر دهیم. این یک مورد استفاده عالی برای هوک useCallback است.
useCallback یک نسخه Memoized از یک تابع را برمیگرداند. این نسخه Memoized تنها در صورتی تغییر میکند که یکی از وابستگیها در آرایه وابستگی آن تغییر کند. با ارائه یک آرایه وابستگی خالی ([])، میتوانیم یک تابع پایدار ایجاد کنیم که برای کل طول عمر کامپوننت باقی بماند.
بیایید مثال قبلی خود را با استفاده از useCallback بازسازی کنیم:
import React, { useState, useCallback } from 'react';
function OptimizedComponent() {
const [count, setCount] = useState(0);
// Create a stable callback function with useCallback
const myRefCallback = useCallback(node => {
// This logic now runs only when the component mounts and unmounts
console.log('Ref callback fired with:', node);
if (node !== null) {
// You can perform setup logic here
console.log('Element is mounted!');
}
}, []); // <-- Empty dependency array means the function is created only once
return (
<div>
<h3>Counter: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<div ref={myRefCallback}>
I am the referenced element.
</div>
</div>
);
}
اکنون، وقتی این نسخه بهینه شده را اجرا میکنید، فقط دو بار در مجموع Console Log را مشاهده خواهید کرد:
- یک بار هنگام Mount اولیه کامپوننت (
Ref callback fired with: <div>...</div>). - یک بار هنگام Unmount کامپوننت (
Ref callback fired with: null).
کلیک کردن روی دکمه "Increment" دیگر تابع Ref را راهاندازی نمیکند. ما با موفقیت از چرخه پاکسازی/راهاندازی غیرضروری در هر رندر مجدد جلوگیری کردهایم. React همان نمونه تابع را برای ویژگی ref در رندرهای بعدی میبیند و به درستی تشخیص میدهد که هیچ تغییری لازم نیست.
سناریوهای پیشرفته و بهترین شیوهها
در حالی که یک آرایه وابستگی خالی رایج است، سناریوهایی وجود دارد که تابع Ref شما نیاز دارد به تغییرات در Props یا State واکنش نشان دهد. اینجاست که قدرت آرایه وابستگی useCallback واقعاً میدرخشد.
مدیریت وابستگیها در تابع
تصور کنید که باید منطقی را در تابع Ref خود اجرا کنید که به یک بخش از State یا یک Prop بستگی دارد. به عنوان مثال، تنظیم یک ویژگی `data-` بر اساس تم فعلی.
function ThemedComponent({ theme }) {
const [internalState, setInternalState] = useState(0);
const themedRefCallback = useCallback(node => {
if (node !== null) {
// This callback now depends on the 'theme' prop
console.log(`Setting theme attribute to: ${theme}`);
node.setAttribute('data-theme', theme);
}
}, [theme]); // <-- Add 'theme' to the dependency array
return (
<div>
<p>Current Theme: {theme}</p>
<div ref={themedRefCallback}>This element's theme will update.</div>
{/* ... imagine a button here to change the parent's theme ... */}
</div>
);
}
در این مثال، ما theme را به آرایه وابستگی useCallback اضافه کردهایم. این به این معنی است:
- یک تابع
themedRefCallbackجدید فقط زمانی ایجاد میشود که Prop themeتغییر کند. - وقتی Prop
themeتغییر میکند، React نمونه تابع جدید را تشخیص میدهد و تابع Ref را دوباره اجرا میکند (ابتدا باnull، سپس با عنصر). - این به اثر ما - تنظیم ویژگی `data-theme` - اجازه میدهد تا با مقدار
themeبه روز شده دوباره اجرا شود.
این رفتار صحیح و مورد نظر است. ما به صراحت به React میگوییم که منطق Ref را هنگام تغییر وابستگیهای آن دوباره راهاندازی کند، در حالی که همچنان از اجرای آن در بهروزرسانیهای State نامرتبط جلوگیری میکنیم.
ادغام با کتابخانههای شخص ثالث
یکی از قدرتمندترین موارد استفاده برای توابع Ref، مقداردهی اولیه و از بین بردن نمونههای کتابخانههای شخص ثالث است که نیاز به اتصال به یک گره DOM دارند. این الگو کاملاً از ماهیت Mount/Unmount تابع Ref استفاده میکند.
در اینجا یک الگوی قوی برای مدیریت کتابخانهای مانند کتابخانه نمودار یا نقشه وجود دارد:
import React, { useRef, useCallback, useEffect } from 'react';
import SomeChartingLibrary from 'some-charting-library';
function ChartComponent({ data }) {
// Use a ref to hold the library instance, not the DOM node
const chartInstance = useRef(null);
const chartContainerRef = useCallback(node => {
// The node is null when the component unmounts
if (node === null) {
if (chartInstance.current) {
console.log('Cleaning up chart instance...');
chartInstance.current.destroy(); // Cleanup method from the library
chartInstance.current = null;
}
return;
}
// The node exists, so we can initialize our chart
console.log('Initializing chart instance...');
const chart = new SomeChartingLibrary(node, {
// Configuration options
data: data,
});
chartInstance.current = chart;
}, [data]); // Re-create the chart if the data prop changes
return <div className="chart-container" ref={chartContainerRef} style={{ height: '400px' }} />;
}
این الگو فوقالعاده تمیز و مقاوم است:
- مقداردهی اولیه: هنگامی که `div` Mount میشود، تابع `node` را دریافت میکند. یک نمونه جدید از کتابخانه نمودارسازی ایجاد میکند و آن را در `chartInstance.current` ذخیره میکند.
- پاکسازی: هنگامی که کامپوننت Unmount میشود (یا اگر `data` تغییر کند، و باعث اجرای مجدد میشود)، ابتدا تابع با `null` فراخوانی میشود. کد بررسی میکند که آیا نمونه نموداری وجود دارد یا خیر و در صورت وجود، متد `destroy()` آن را فراخوانی میکند و از نشت حافظه جلوگیری میکند.
- بهروزرسانیها: با گنجاندن `data` در آرایه وابستگی، اطمینان حاصل میکنیم که اگر دادههای نمودار نیاز به تغییر اساسی داشته باشند، کل نمودار به طور تمیز نابود شده و با دادههای جدید دوباره مقداردهی اولیه میشود. برای بهروزرسانیهای ساده داده، یک کتابخانه ممکن است یک متد `update()` ارائه دهد که میتواند در یک `useEffect` جداگانه مدیریت شود.
مقایسه عملکرد: چه زمانی بهینهسازی *واقعاً* اهمیت دارد؟
مهم است که با ذهنیتی عملگرایانه به عملکرد نزدیک شوید. در حالی که پیچیدن هر تابع Ref در `useCallback` عادت خوبی است، تأثیر عملکرد واقعی به طور چشمگیری بر اساس کاری که در داخل تابع انجام میشود متفاوت است.
سناریوهای تأثیر ناچیز
اگر تابع شما فقط یک تخصیص متغیر ساده را انجام دهد، سربار ایجاد یک تابع جدید در هر رندر ناچیز است. موتورهای مدرن جاوا اسکریپت در ایجاد تابع و جمعآوری زباله فوقالعاده سریع هستند.
مثال: ref={(node) => (myRef.current = node)}
در مواردی مانند این، در حالی که از نظر فنی کمتر بهینه است، بعید است که تا به حال تفاوتی در عملکرد را در یک برنامه دنیای واقعی اندازهگیری کنید. در دام بهینهسازی زودرس نیفتید.
سناریوهای تأثیر قابل توجه
هنگامی که تابع Ref شما هر یک از موارد زیر را انجام میدهد، همیشه باید از useCallback استفاده کنید:
- دستکاری DOM: اضافه کردن یا حذف مستقیم کلاسها، تنظیم ویژگیها یا اندازهگیری اندازههای عنصر (که میتواند Reflow طرحبندی را راهاندازی کند).
- Event Listener ها: فراخوانی `addEventListener` و `removeEventListener`. راهاندازی این در هر رندر راهی تضمینی برای معرفی اشکالات و مشکلات عملکرد است.
- نمونهسازی کتابخانه: همانطور که در مثال نمودار خود نشان دادیم، مقداردهی اولیه و از بین بردن اشیاء پیچیده پرهزینه است.
- درخواستهای شبکه: ایجاد یک تماس API بر اساس وجود یک عنصر DOM.
- ارسال Refs به کامپوننتهای Memoized: اگر یک تابع Ref را به عنوان یک Prop به یک کامپوننت فرزند ارسال کنید که در
React.memoپیچیده شده است، یک تابع درونخطی ناپایدار Memoization را میشکند و باعث میشود فرزند به طور غیرضروری دوباره رندر شود.
یک قانون سرانگشتی خوب: اگر تابع Ref شما شامل بیش از یک تخصیص ساده است، آن را با useCallback Memoize کنید.
نتیجهگیری: نوشتن کد قابل پیشبینی و با کارایی بالا
تابع Ref در React ابزاری قدرتمند است که کنترل دقیقی بر گرههای DOM و نمونههای کامپوننت ارائه میدهد. درک چرخه حیات آن - به طور خاص فراخوانی `null` عمدی در طول پاکسازی - کلید استفاده موثر از آن است.
ما آموختهایم که الگوی ضد رایج استفاده از یک تابع درونخطی برای ویژگی ref منجر به اجرای مجدد غیرضروری و بالقوه پرهزینه در هر رندر میشود. راه حل ظریف و اصطلاحی React است: تابع Ref را با استفاده از هوک useCallback پایدار کنید.
با تسلط بر این الگو، میتوانید:
- جلوگیری از گلوگاههای عملکرد: از منطق راهاندازی و جمعآوری پرهزینه در هر تغییر State خودداری کنید.
- حذف اشکالات: اطمینان حاصل کنید که Event Listenerها و نمونههای کتابخانه به طور تمیز و بدون تکراری یا نشت حافظه مدیریت میشوند.
- نوشتن کد قابل پیشبینی: کامپوننتهایی ایجاد کنید که منطق Ref آنها دقیقاً همانطور که انتظار میرود رفتار کند، فقط زمانی اجرا میشود که کامپوننت Mount، Unmount یا زمانی که وابستگیهای خاص آن تغییر میکنند.
دفعه بعد که برای حل یک مشکل پیچیده به یک Ref دست دراز میکنید، قدرت یک تابع Memoized را به خاطر بسپارید. این یک تغییر کوچک در کد شما است که میتواند تفاوت چشمگیری در کیفیت و عملکرد برنامههای React شما ایجاد کند و به تجربه بهتر برای کاربران در سراسر جهان کمک کند.