آشنایی با مفاهیم React.memo ،useMemo و useCallback — به زبان ساده

۱۰۰۰ بازدید
آخرین به‌روزرسانی: ۱۹ شهریور ۱۴۰۲
زمان مطالعه: ۵ دقیقه
آشنایی با مفاهیم React.memo ،useMemo و useCallback — به زبان ساده

پیش‌نیاز مطالعه این مقاله داشتن دانشی مقدماتی از React است. در این مقاله تلاش می‌کنیم تفاوت‌های بین useMemo و useCallback را توضیح دهیم. این دو جزء قلاب‌های ری‌اکت محسوب می‌شوند. از طرف دیگر با این که React.memo یک قلاب ری‌اکت نیست، اما آن را نیز توضیح می‌دهیم، زیرا وجود کلمه memo در نام آن ممکن است موجب بروز سردرگمی شود. در هر صورت همه این موارد با بهینه‌سازی اپلیکیشن‌های React مرتبط هستند.

React.memo چیست؟

اگر با React.PureComponent (+) آشنا باشید، در این صورت React.memo برای شما کاملاً سرراست خواهد بود، زیرا کاملاً مشابه React.PureComponent است. ما از React.PureComponent با کامپوننت کلاس استفاده می‌کنیم، اما React.memo به همراه کامپوننت‌های تابع عمل می‌کند. پیشنهاد می‌کنیم این مثال (+) را بررسی کنید تا با طرز کار آن آشنا شوید.

نکته: همه مثال‌هایی که در ادامه می‌بینید تنها برای بیان ایده‌های اصلی استفاده شده‌اند. در عمل در چنین کاربردهای ساده‌ای نیازمند بهینه‌سازی نیستیم.

1const App = () => {
2   const [count1, setCount1] = React.useState(0)
3   const [count2, setCount2] = React.useState(0)
4
5   const increaseCounter1 = () => {
6      setCount1(count1 => count1 + 1)
7   }
8
9   return (
10      <>
11         <button onClick={increaseCounter1}>Increase counter 1</button>
12         <Counter value={count1}>Counter 1</Counter>
13         <Counter value={count2}>Coutner 2</Counter>
14      </>
15   )
16}
17const Counter = ({value, children}) => {
18   console.log('Render: ', children)
19
20   return (
21      <div>
22         {children}: {value}
23      </div>
24   )
25}
26
27export default Counter

هر بار که کاربر روی دکمه کلیک می‌کند، حالت count1 تغییر می‌یابد و موجب می‌شود که اپلیکیشن هر دو شمارنده را رندر مجدد کند که این رندرها به عنوان «رندر مجدد غیرضروری» شناخته می‌شوند. با این حال، ما صرفاً انتظار داریم که counter1 رندر مجدد شود، چون هیچ چیزی در مورد counter2 تغییر نخواهد یافت. در عمل، هر دو شمارنده رندر مجدد می‌شوند.

چگونه می‌توانیم این مشکل را رفع کنیم؟ پاسخ در React.memo است. تنها چیزی که نیاز داریم این است که کامپوننت شمارنده را درون React.memo قرار دهیم.

1const Counter = ({value, children}) => {
2   console.log('Render: ', children)
3
4   return (
5      <div>
6         {children}: {value}
7      </div>
8   )
9}
10
11export default React.memo(Counter)

React.memo به صورت پیش‌فرض همه props ارسالی به کامپوننت را از طریق referential equality مقایسه می‌کند. اگر این props تغییری نیافته باشند، React.memo از آخرین نتیجه رندر مجدد شده استفاده می‌کند و از این رو از رندر شدن مجدد کامپوننت جلوگیری می‌کند. در مثال زیر React.memo بررسی می‌کند آیا هیچ تغییری در مورد prop-های value و children از آخرین رندر به بعد صورت گرفته است یا نه. از آنجا که دکمه ما تنها مقدار counter1 را تغییر می‌دهد، React.memo از رندر شدن مجدد counter2 جلوگیری می‌کند.

همچنین می‌توانیم مقایسه پیش‌فرض React.memo را با ارائه یک تابع مقایسه سفارشی به عنوان آرگومان دوم دور بزنیم.

فایل React.memo

1const Counter = () => {
2
3   const areEqual = (prevProps, nextProps) => {
4     /*
5     return true if passing nextProps to render would return
6     the same result as passing prevProps to render,
7     otherwise return false
8     */
9   } 
10}
11
12export default React.memo(Counter, areEqual)

استفاده از useMemo و useCallback

توضیح خود را با بیان تعریف از مستندات رسمی آغاز می‌کنیم. useMemo یک مقدار «درون حافظه‌ای» (memoized) بازگشت می‌دهد.

1React.useMemo(() => {
2  fooFunction()
3}, [dependencies])

useCallback یک callback به صورت «درون حافظه‌ای» (memoized) بازگشت می‌دهد:

1React.useCallback(() => {
2  fooFunction()
3}, [dependencies])

در ادامه آن را جزءبه‌جزء تشریح می‌کنیم. React.useMemo و همچنین React.useCallback به عنوان آرگومان نخست، یک تابع و برای آرگومان دوم آرایه وابستگی‌ها را می‌گیرند. قلاب یک مقدار جدید را تنها در صورتی بازگشت می‌دهد که یکی از مقادیر وابستگی تغییر یابد (React.useCallback). تفاوت اصلی در این است که React.useMemo اقدام به فراخوانی fooFunction می‌کند و نتیجه‌اش را بازگشت می‌دهد، در حالی که React.useCallback اقدام به بازگرداندن fooFunction بدون فراخوانی آن می‌کند. به مثال زیر توجه کنید:

1const App = () => {
2   const fooFunction = () => {
3      return 'Foo is just Food without D'
4   }
5
6   const useMemoResult = React.useMemo(fooFunction, [])
7   const useCallbackResult = React.useCallback(fooFunction, [])
8
9   console.log('useMemoResult: ', useMemoResult)
10   console.log('useCallbackResult: ', useCallbackResult)
11
12   return <p>Foo is just food without D</p>
13}

مثال را اجرا کنید و سپس به کنسول نگاه کنید، خروجی زیر را می‌بینید:

React.useMemo اقدام به اجرای fooFunction می‌کند که یک رشته به صورت Foo is just Food without D تولید می‌کند؛ در حالی که React.useCallback صرفاً یک fooFunction بدون فراخوانی کردن آن بازگشت می‌دهد. در بخش بعدی در مورد طرز کار آن در ری‌اکت صحبت می‌کنیم.

useMemo

به طور نرمال می‌توانیم از React.useMemo در مواردی که یک مقدار پرهزینه را محاسبه می‌کنیم استفاده کنیم. در این حالت در موارد رندر مجدد کامپوننت دیگر نیاز نیست که آن مقدار را مجدداً محاسبه کنیم.

1const Me = ({girlFriendWords}) => {
2
3    // Provided that girlFriendWords is a string
4   
5   const myReply = decideWhatToSay (girlFriendWords)
6
7   return <p>{myReply}</p>
8}

تصور کنید که محاسبه مقدار myReply کلی انرژی گرفته است و اگر بخواهیم آن را به طور مکرر حساب کنیم و رندر مجدد بگیریم چه قدر اتلاف انرژی خواهد بود. در این وضعیت React.useMemo به کار می‌آید:

1const Me = ({girlFriendWords}) => {
2
3    // Provided that girlFriendWords is a string
4   
5   const myReply = React.memo(() => decideWhatToSay (girlFriendWords), [girlFriendWords])
6
7   return <p>{myReply}</p>
8}

React.useMemo اقدام به گرفتن [girlFriendWords] به عنوان آرایه وابستگی‌ها می‌کند و معنی آن این است که تابع decideWhatToSay را تنها زمانی اجرا خواهد کرد که مقدار girlFriendWords تغییر پیدا کند. بدین ترتیب موفق شد‌ه‌ایم اپلیکیشن خود را بهینه‌سازی کنیم.

useCallback

اکنون به مثال شمارنده خود باز می‌گردیم. در این بخش تلاش می‌کنیم آن را کمی تغییر دهیم. شمارنده ما هم‌اینک تابع onClick را به عنوان یک prop می‌گیرد. آیا می‌توانید حدس بزنید کامپوننت Counter2 در زمان تغییر یافت مقدار count1 رندر مجدد خواهد شد یا نه؟

1const App = () => {
2   const [count1, setCount1] = React.useState(0)
3   const [count2, setCount2] = React.useState(0)
4
5   const increaseCounter1 = () => {
6      setCount1(count1 => count1 + 1)
7   }
8   
9   const increaseCounter2 = () => {
10          setCount1(count2 => count1 + 1)
11    }
12
13   return (
14      <>
15         <Counter value={count1} onClick={increaseCounter1}>Counter 1</Counter>
16         <Counter value={count2} onClick={increaseCounter2}>Coutner 2</Counter>
17      </>
18   )
19}
20const Counter = ({value, children, onClick}) => {
21   console.log('Render: ', children)
22
23   return (
24      <Button onClick={}>
25         {children}: {value}
26      </div>
27   )
28}
29
30export default React.memo(Counter)

حتی زمانی که از React.memo استفاده می‌کنیم، کامپوننت counter2 همچنان زمانی که count1 تغییر یابد، رندر مجدد خواهد شد، زیرا React.memo از reference equality برای جلوگیری از رندرهای غیرضروری بهره می‌گیرد. با این حال زمانی که اپلیکیشن رندر مجدد می‌شود، increaseCounter2 از نو ایجاد می‌شود و از این رو props مربوط به OnClick که به کامپوننت Counter ارسال می‌شوند، هر بار متفاوت هستند و به همین جهت موجب رندر مجدد می‌شوند. روش آسان برای اجتناب از این مشکل، جلوگیری از ایجاد مجدد تابع increaseCounter2 در زمان رندر شدن مجدد اپلیکیشن است. به این منظور از React.useCallback کمک می‌گیریم:

1const App = () => {
2   const [count1, setCount1] = React.useState(0)
3   const [count2, setCount2] = React.useState(0)
4
5   const increaseCounter1 = React.useCallback(() => {
6      setCount1(count1 => count1 + 1)
7   }, [])
8   
9   const increaseCounter2 = React.useCallback(() => {
10          setCount2(count2 => count1 + 1)
11    }, [])
12    
13   return (
14      <>
15         <Counter value={count1} onClick={increaseCounter1}>Counter 1</Counter>
16         <Counter value={count2} onClick={increaseCounter2}>Coutner 2</Counter>
17      </>
18   )
19}

اگر به آرایه وابستگی‌ها نگاهی بیاندازید می‌بینید که خالی است، زیرا می‌خواهیم این تابع را تنها یک بار ایجاد کنیم. به این ترتیب props مربوط به OnClick که به کامپوننت Counter ارسال می‌شوند، همواره یکسان هستند.

سخن پایانی

پیش از هر گونه اقدام به بهینه‌سازی رندهای مجدد باید ابتدا هزینه را محاسبه کنیم. بهینه‌سازی همواره خود هزینه‌ای در پی دارد. React.memo مشابه React.PureComponent است به جز این که برای کامپوننت کارکردی استفاده می‌شود؛ در حالی که React.PureComponent تنها برای کامپوننت‌های کلاس مورد استفاده قرار می‌گیرد.

React.useMemo یک مقدار memorized بازگشت می‌دهد در حالی که React.useCallback یک Callback به صورت memorized بازمی‌گرداند. بدین ترتیب به پایان این مقاله می‌رسیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱۷ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
shot-code
۲ دیدگاه برای «آشنایی با مفاهیم React.memo ،useMemo و useCallback — به زبان ساده»

عالی بود

سلام
خیلی ممنونم از آموزش خوبتون
واقعا مقالات که قرار میدن ایشون
با صبر وحوصله توضیح داده شده و اینکه با مینی پروجکت ها توضیح میدن خیلی خوب و حساب شده کار میکنن و خیلی بهتر و بیشتر برای کسی که میخواد خوب یادبگیره جا میافته
واقعا خوب و حساب شده کار میکنن برای مقالات برنامه نویسی ای که میدن
آرزوی موفقیت براشون دارم موفق باشین

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *