قلاب useCallback React را یاد بگیرید. مفهوم یاداشت سازی توابع چیست، چه زمانی (و چه زمانی نه) باید از آن استفاده کرد، و چگونه می توان اجزای خود را برای عملکرد بهینه کرد.
React useCallback: بررسی عمیق بهینه سازی عملکرد و یاداشت سازی توابع
در دنیای توسعه وب مدرن، React به دلیل رابط کاربری اعلانی و مدل رندرینگ کارآمد خود متمایز است. با این حال، با افزایش پیچیدگی برنامه ها، اطمینان از عملکرد مطلوب به یک مسئولیت حیاتی برای هر توسعه دهنده تبدیل می شود. React مجموعه قدرتمندی از ابزارها را برای مقابله با این چالش ها ارائه می دهد، و در میان مهمترین - و اغلب سوء تفاهم شده - قلاب های بهینه سازی هستند. امروز، ما در حال بررسی عمیق یکی از آنها هستیم: useCallback.
این راهنمای جامع قلاب useCallback را رمزگشایی می کند. ما مفهوم اساسی جاوا اسکریپت را که آن را ضروری می کند، بررسی خواهیم کرد، نحو و مکانیک آن را درک می کنیم، و مهمتر از همه، دستورالعمل های روشنی را در مورد اینکه چه زمانی باید - و نباید - در کد خود به آن دسترسی پیدا کنید، ایجاد می کنیم. در پایان، شما مجهز خواهید بود که از useCallback نه به عنوان یک گلوله جادویی، بلکه به عنوان یک ابزار دقیق برای سریعتر و کارآمدتر کردن برنامه های React خود استفاده کنید.
مشکل اصلی: درک برابری ارجاعی
قبل از اینکه بتوانیم از کاری که useCallback انجام می دهد قدردانی کنیم، ابتدا باید یک مفهوم اصلی در جاوا اسکریپت را درک کنیم: برابری ارجاعی. در جاوا اسکریپت، توابع اشیایی هستند. این بدان معنی است که وقتی دو تابع (یا هر دو شی) را مقایسه می کنید، محتوای آنها را مقایسه نمی کنید، بلکه ارجاع آنها - مکان خاص آنها در حافظه - را مقایسه می کنید.
این قطعه ساده جاوا اسکریپت را در نظر بگیرید:
const func1 = () => { console.log('Hello'); };
const func2 = () => { console.log('Hello'); };
console.log(func1 === func2); // Outputs: false
حتی اگر func1 و func2 کد یکسانی داشته باشند، دو شی تابع جداگانه هستند که در آدرس های حافظه مختلف ایجاد شده اند. بنابراین، آنها برابر نیستند.
چگونه این بر اجزای React تأثیر می گذارد
یک کامپوننت تابعی React، در هسته خود، تابعی است که هر بار که کامپوننت نیاز به رندر شدن دارد، اجرا می شود. این اتفاق زمانی می افتد که وضعیت آن تغییر کند، یا زمانی که کامپوننت والد آن دوباره رندر شود. وقتی این تابع اجرا می شود، همه چیز در داخل آن، از جمله متغیرها و اعلانات تابع، از ابتدا دوباره ایجاد می شود.
بیایید به یک کامپوننت معمولی نگاه کنیم:
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
// This function is re-created on every single render
const handleIncrement = () => {
console.log('Creating a new handleIncrement function');
setCount(count + 1);
};
return (
Count: {count}
);
};
هر بار که روی دکمه "Increment" کلیک می کنید، وضعیت count تغییر می کند و باعث می شود کامپوننت Counter دوباره رندر شود. در طول هر رندر مجدد، یک تابع handleIncrement کاملاً جدید ایجاد می شود. برای یک کامپوننت ساده مانند این، تاثیر عملکرد ناچیز است. موتور جاوا اسکریپت در ایجاد توابع فوق العاده سریع است. پس چرا حتی باید نگران این موضوع باشیم؟
چرا ایجاد مجدد توابع به یک مشکل تبدیل می شود
مشکل خود ایجاد تابع نیست. مشکل واکنش زنجیره ای است که می تواند در هنگام ارسال به عنوان یک prop به کامپوننت های فرزند ایجاد کند، به خصوص آنهایی که با React.memo بهینه شده اند.
React.memo یک کامپوننت مرتبه بالاتر (HOC) است که یک کامپوننت را یاداشت می کند. این کار با انجام یک مقایسه سطحی از props کامپوننت انجام می شود. اگر props جدید با props قدیمی یکسان باشند، React از رندر مجدد کامپوننت صرف نظر می کند و از آخرین نتیجه رندر شده دوباره استفاده می کند. این یک بهینه سازی قدرتمند برای جلوگیری از چرخه های رندر غیر ضروری است.
اکنون، بیایید ببینیم مشکل ما با برابری ارجاعی از کجا می آید. تصور کنید یک کامپوننت والد داریم که یک تابع handler را به یک کامپوننت فرزند یاداشت شده ارسال می کند.
import React, { useState } from 'react';
// A memoized child component that only re-renders if its props change.
const MemoizedButton = React.memo(({ onIncrement }) => {
console.log('MemoizedButton is rendering!');
return ;
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(false);
// This function is re-created every time ParentComponent renders
const handleIncrement = () => {
setCount(count + 1);
};
return (
Parent Count: {count}
Other State: {String(otherState)}
);
};
در این مثال، MemoizedButton یک prop دریافت می کند: onIncrement. ممکن است انتظار داشته باشید که وقتی روی دکمه "Toggle Other State" کلیک می کنید، فقط ParentComponent دوباره رندر می شود زیرا count تغییر نکرده است، و بنابراین تابع onIncrement از نظر منطقی یکسان است. با این حال، اگر این کد را اجرا کنید، هر بار که روی "Toggle Other State" کلیک می کنید، عبارت "MemoizedButton is rendering!" را در کنسول خواهید دید.
چرا این اتفاق می افتد؟
وقتی ParentComponent دوباره رندر می شود (به دلیل setOtherState)، یک نمونه جدید از تابع handleIncrement ایجاد می کند. وقتی React.memo props را برای MemoizedButton مقایسه می کند، می بیند که oldProps.onIncrement !== newProps.onIncrement به دلیل برابری ارجاعی. تابع جدید در یک آدرس حافظه متفاوت است. این بررسی ناموفق فرزند یاداشت شده ما را مجبور به رندر مجدد می کند و هدف React.memo را به طور کامل از بین می برد.
این سناریوی اصلی است که useCallback به کمک می آید.
راه حل: یاداشت سازی با `useCallback`
قلاب useCallback برای حل همین مشکل طراحی شده است. این به شما امکان می دهد تعریف یک تابع را بین رندرها یاداشت کنید، و اطمینان حاصل کنید که برابری ارجاعی را حفظ می کند مگر اینکه وابستگی های آن تغییر کنند.
نحو
const memoizedCallback = useCallback(
() => {
// The function to memoize
doSomething(a, b);
},
[a, b], // The dependency array
);
- آرگومان اول: تابع inline callback که می خواهید یاداشت کنید.
- آرگومان دوم: آرایه وابستگی.
useCallbackفقط در صورتی یک تابع جدید را برمی گرداند که یکی از مقادیر موجود در این آرایه از آخرین رندر تغییر کرده باشد.
بیایید مثال قبلی خود را با استفاده از useCallback دوباره فاکتورگیری کنیم:
import React, { useState, useCallback } from 'react';
const MemoizedButton = React.memo(({ onIncrement }) => {
console.log('MemoizedButton is rendering!');
return ;
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(false);
// Now, this function is memoized!
const handleIncrement = useCallback(() => {
setCount(count + 1);
}, [count]); // Dependency: 'count'
return (
Parent Count: {count}
Other State: {String(otherState)}
);
};
اکنون، وقتی روی "Toggle Other State" کلیک می کنید، ParentComponent دوباره رندر می شود. React قلاب useCallback را اجرا می کند. مقدار count را در آرایه وابستگی خود با مقدار رندر قبلی مقایسه می کند. از آنجایی که count تغییر نکرده است، useCallback دقیقاً همان نمونه تابع را که دفعه قبل برگردانده بود، برمی گرداند. وقتی React.memo props را برای MemoizedButton مقایسه می کند، متوجه می شود که oldProps.onIncrement === newProps.onIncrement است. بررسی با موفقیت انجام می شود و رندر مجدد غیر ضروری فرزند با موفقیت رد می شود! مشکل حل شد.
تسلط بر آرایه وابستگی
آرایه وابستگی مهمترین بخش استفاده صحیح از useCallback است. این به React می گوید که چه زمانی می توان تابع را دوباره ایجاد کرد. اشتباه گرفتن آن می تواند منجر به اشکالات ظریفی شود که ردیابی آنها دشوار است.
آرایه خالی: `[]`
اگر یک آرایه وابستگی خالی ارائه دهید، به React می گویید: "این تابع هرگز نیازی به ایجاد مجدد ندارد. نسخه رندر اولیه برای همیشه خوب است."
const stableFunction = useCallback(() => {
console.log('This will always be the same function');
}, []); // Empty array
این یک مرجع بسیار پایدار ایجاد می کند، اما با یک نکته مهم همراه است: مشکل "بستار قدیمی". بستار زمانی است که یک تابع متغیرها را از دامنه ای که در آن ایجاد شده است "به خاطر می آورد". اگر callback شما از وضعیت یا props استفاده می کند اما آنها را به عنوان وابستگی فهرست نمی کنید، مقادیر اولیه آنها را می بندد.
مثال یک بستار قدیمی:
const StaleCounter = () => {
const [count, setCount] = useState(0);
const handleLogCount = useCallback(() => {
// This 'count' is the value from the initial render (0)
// because `count` is not in the dependency array.
console.log(`Current count is: ${count}`);
}, []); // WRONG! Missing dependency
return (
Count: {count}
);
};
در این مثال، مهم نیست چند بار روی "Increment" کلیک می کنید، کلیک روی "Log Count" همیشه "Current count is: 0" را چاپ می کند. تابع handleLogCount با مقدار count از اولین رندر گیر کرده است زیرا آرایه وابستگی آن خالی است.
آرایه صحیح: `[dep1, dep2, ...]`
برای رفع مشکل بستار قدیمی، باید هر متغیری را از دامنه کامپوننت (وضعیت، props، و غیره) که تابع شما از آن استفاده می کند در داخل آرایه وابستگی قرار دهید.
const handleLogCount = useCallback(() => {
console.log(`Current count is: ${count}`);
}, [count]); // CORRECT! Now it depends on count.
اکنون، هر زمان که count تغییر کند، useCallback یک تابع handleLogCount جدید ایجاد می کند که مقدار جدید count را می بندد. این روش صحیح و ایمن برای استفاده از قلاب است.
نکته حرفه ای: همیشه از بسته eslint-plugin-react-hooks استفاده کنید. این بسته یک قانون exhaustive-deps ارائه می دهد که به طور خودکار در صورتی که وابستگی را در قلاب های useCallback، useEffect یا useMemo خود از دست بدهید، به شما هشدار می دهد. این یک شبکه ایمنی ارزشمند است.
الگوها و تکنیک های پیشرفته
1. به روز رسانی های تابعی برای جلوگیری از وابستگی ها
گاهی اوقات شما یک تابع پایدار می خواهید که وضعیت را به روز کند، اما نمی خواهید هر بار که وضعیت تغییر می کند، آن را دوباره ایجاد کنید. این برای توابعی که به قلاب های سفارشی یا ارائه دهندگان زمینه ارسال می شوند، رایج است. می توانید این کار را با استفاده از فرم به روز رسانی تابعی یک تنظیم کننده وضعیت به دست آورید.
const handleIncrement = useCallback(() => {
// `setCount` can take a function that receives the previous state.
// This way, we don't need to depend on `count` directly.
setCount(prevCount => prevCount + 1);
}, []); // The dependency array can now be empty!
با استفاده از setCount(prevCount => ...)، تابع ما دیگر نیازی به خواندن متغیر count از دامنه کامپوننت ندارد. از آنجایی که به هیچ چیز وابسته نیست، می توانیم با خیال راحت از یک آرایه وابستگی خالی استفاده کنیم و تابعی ایجاد کنیم که واقعاً برای کل چرخه عمر کامپوننت پایدار است.
2. استفاده از `useRef` برای مقادیر فرار
اگر callback شما نیاز به دسترسی به آخرین مقدار یک prop یا وضعیت دارد که خیلی مکرر تغییر می کند، اما نمی خواهید callback خود را ناپایدار کنید، چه؟ می توانید از useRef برای نگه داشتن یک مرجع قابل تغییر به آخرین مقدار بدون ایجاد رندر مجدد استفاده کنید.
const VeryFrequentUpdates = ({ onEvent }) => {
const [value, setValue] = useState('');
// Keep a ref to the latest version of the onEvent callback
const onEventRef = useRef(onEvent);
useEffect(() => {
onEventRef.current = onEvent;
}, [onEvent]);
// This internal callback can be stable
const handleInternalAction = useCallback(() => {
// ...some internal logic...
// Call the latest version of the prop function via the ref
if (onEventRef.current) {
onEventRef.current();
}
}, []); // Stable function
// ...
};
این یک الگوی پیشرفته است، اما در سناریوهای پیچیده مانند رفع جهش، محدود کردن سرعت یا ارتباط با کتابخانه های شخص ثالث که نیاز به مراجع callback پایدار دارند، مفید است.
نصیحت حیاتی: چه زمانی از `useCallback` استفاده نکنید
تازه واردان به قلاب های React اغلب در دام پیچیدن هر تابع در useCallback می افتند. این یک الگوی ضد است که به عنوان بهینه سازی زودرس شناخته می شود. به یاد داشته باشید، useCallback رایگان نیست. این یک هزینه عملکرد دارد.
هزینه `useCallback`
- حافظه: باید تابع یادداشت شده را در حافظه ذخیره کند.
- محاسبه: در هر رندر، React همچنان باید قلاب را فراخوانی کند و موارد موجود در آرایه وابستگی را با مقادیر قبلی آنها مقایسه کند.
در بسیاری از موارد، این هزینه می تواند بیشتر از فایده باشد. سربار فراخوانی قلاب و مقایسه وابستگی ها ممکن است بیشتر از هزینه ایجاد مجدد تابع و اجازه دادن به یک کامپوننت فرزند برای رندر مجدد باشد.
از `useCallback` استفاده نکنید زمانی که:
- تابع به یک عنصر HTML بومی ارسال می شود: کامپوننت هایی مانند
<div>،<button>یا<input>به برابری ارجاعی برای handlers رویداد خود اهمیتی نمی دهند. ارسال یک تابع جدید بهonClickدر هر رندر کاملاً خوب است و هیچ تاثیری بر عملکرد ندارد. - کامپوننت دریافت کننده یادداشت نشده است: اگر callback را به یک کامپوننت فرزند ارسال می کنید که در
React.memoپیچیده نشده است، یاداشت کردن callback بی فایده است. کامپوننت فرزند به هر حال هر زمان که والد آن دوباره رندر شود، دوباره رندر می شود. - تابع تعریف شده و در چرخه رندر یک کامپوننت واحد استفاده می شود: اگر یک تابع به عنوان یک prop ارسال نمی شود یا به عنوان یک وابستگی در قلاب دیگری استفاده نمی شود، دلیلی برای یاداشت کردن مرجع آن وجود ندارد.
// NO need for useCallback here
const handleClick = () => { console.log('Clicked!'); };
return ;
قانون طلایی: فقط از useCallback به عنوان یک بهینه سازی هدفمند استفاده کنید. از React DevTools Profiler برای شناسایی کامپوننت هایی که غیر ضروری دوباره رندر می شوند استفاده کنید. اگر کامپوننتی را یافتید که در React.memo پیچیده شده است و هنوز به دلیل یک prop callback ناپایدار دوباره رندر می شود، این زمان عالی برای اعمال useCallback است.
`useCallback` در مقابل `useMemo`: تفاوت کلیدی
یکی دیگر از نکات رایج سردرگمی، تفاوت بین useCallback و useMemo است. آنها بسیار شبیه هستند، اما اهداف متمایزی را دنبال می کنند.
useCallback(fn, deps)نمونه تابع را یاداشت می کند. این به شما همان شی تابع را بین رندرها برمی گرداند.useMemo(() => value, deps)مقدار بازگشتی یک تابع را یاداشت می کند. تابع را اجرا می کند و نتیجه آن را به شما برمی گرداند و فقط زمانی که وابستگی ها تغییر می کنند، آن را دوباره محاسبه می کند.
اساساً، `useCallback(fn, deps)` فقط یک شیرین کننده نحوی برای `useMemo(() => fn, deps)` است. این یک قلاب راحت برای مورد استفاده خاص یاداشت کردن توابع است.
چه زمانی از کدام یک استفاده کنیم؟
- از
useCallbackبرای توابعی که به کامپوننت های فرزند ارسال می کنید برای جلوگیری از رندر مجدد غیر ضروری استفاده کنید (به عنوان مثال، handlers رویداد مانندonClick،onSubmit). - از
useMemoبرای محاسبات گران قیمت، مانند فیلتر کردن یک مجموعه داده بزرگ، تبدیل داده های پیچیده، یا هر مقداری که زمان زیادی برای محاسبه می برد و نباید در هر رندر دوباره محاسبه شود، استفاده کنید.
// Use case for useMemo: Expensive calculation
const visibleTodos = useMemo(() => {
console.log('Filtering list...'); // This is expensive
return todos.filter(t => t.status === filter);
}, [todos, filter]);
// Use case for useCallback: Stable event handler
const handleAddTodo = useCallback((text) => {
dispatch({ type: 'ADD_TODO', text });
}, []); // Stable dispatch function
return (
);
نتیجه گیری و بهترین شیوه ها
قلاب useCallback یک ابزار قدرتمند در جعبه ابزار بهینه سازی عملکرد React شما است. این به طور مستقیم به مشکل برابری ارجاعی می پردازد و به شما امکان می دهد props توابع را تثبیت کنید و پتانسیل کامل `React.memo` و سایر قلاب ها مانند `useEffect` را باز کنید.
نکات کلیدی:
- هدف:
useCallbackیک نسخه یادداشت شده از یک تابع callback را برمی گرداند که فقط در صورتی تغییر می کند که یکی از وابستگی های آن تغییر کرده باشد. - مورد استفاده اصلی: برای جلوگیری از رندر مجدد غیر ضروری کامپوننت های فرزند که در
React.memoپیچیده شده اند. - مورد استفاده ثانویه: برای ارائه یک وابستگی تابع پایدار برای سایر قلاب ها، مانند
useEffect، برای جلوگیری از اجرای آنها در هر رندر. - آرایه وابستگی حیاتی است: همیشه تمام متغیرهای محدوده کامپوننت را که تابع شما به آنها وابسته است، قرار دهید. از قانون ESLint
exhaustive-depsبرای اعمال این کار استفاده کنید. - این یک بهینه سازی است، نه یک پیش فرض: هر تابع را در
useCallbackنپیچید. این می تواند به عملکرد آسیب برساند و پیچیدگی غیر ضروری را اضافه کند. ابتدا برنامه خود را پروفایل کنید و بهینه سازی ها را به صورت استراتژیک در جایی که بیشتر مورد نیاز است، اعمال کنید.
با درک "چرایی" پشت useCallback و رعایت این بهترین شیوه ها، می توانید فراتر از حدس و گمان حرکت کنید و شروع به ایجاد بهبودهای عملکردی آگاهانه و تاثیرگذار در برنامه های React خود کنید و تجربیات کاربری ایجاد کنید که نه تنها از نظر ویژگی غنی هستند، بلکه روان و پاسخگو هستند.