فارسی

یاد بگیرید چگونه از توابع پاکسازی Effect در React برای جلوگیری از نشت حافظه و بهینه‌سازی عملکرد اپلیکیشن خود استفاده کنید. راهنمایی جامع برای توسعه‌دهندگان React.

پاکسازی Effect در React: تسلط بر پیشگیری از نشت حافظه

هوک useEffect در React ابزاری قدرتمند برای مدیریت عوارض جانبی (side effects) در کامپوننت‌های تابعی شماست. با این حال، اگر به درستی استفاده نشود، می‌تواند منجر به نشت حافظه شود و بر عملکرد و پایداری اپلیکیشن شما تأثیر بگذارد. این راهنمای جامع به پیچیدگی‌های پاکسازی Effect در React می‌پردازد و دانش و مثال‌های عملی را برای جلوگیری از نشت حافظه و نوشتن اپلیکیشن‌های React قوی‌تر در اختیار شما قرار می‌دهد.

نشت حافظه چیست و چرا بد است؟

نشت حافظه زمانی اتفاق می‌افتد که اپلیکیشن شما حافظه‌ای را تخصیص می‌دهد اما پس از اتمام نیاز، آن را به سیستم باز نمی‌گرداند. با گذشت زمان، این بلوک‌های حافظه‌ی آزاد نشده انباشته شده و منابع سیستم را بیشتر و بیشتر مصرف می‌کنند. در اپلیکیشن‌های وب، نشت حافظه می‌تواند به صورت‌های زیر ظاهر شود:

در React، نشت حافظه اغلب در هوک‌های useEffect هنگام کار با عملیات ناهمزمان، اشتراک‌ها (subscriptions) یا شنوندگان رویداد (event listeners) رخ می‌دهد. اگر این عملیات‌ها هنگام unmount شدن یا re-render شدن کامپوننت به درستی پاکسازی نشوند، می‌توانند در پس‌زمینه به اجرا ادامه دهند، منابع را مصرف کنند و به طور بالقوه باعث ایجاد مشکل شوند.

درک useEffect و عوارض جانبی (Side Effects)

قبل از پرداختن به پاکسازی effect، بیایید به طور خلاصه هدف useEffect را مرور کنیم. هوک useEffect به شما امکان می‌دهد عوارض جانبی را در کامپوننت‌های تابعی خود انجام دهید. عوارض جانبی عملیاتی هستند که با دنیای خارج تعامل دارند، مانند:

هوک useEffect دو آرگومان را می‌پذیرد:

  1. یک تابع حاوی عارضه جانبی.
  2. یک آرایه اختیاری از وابستگی‌ها (dependencies).

تابع عارضه جانبی پس از رندر شدن کامپوننت اجرا می‌شود. آرایه وابستگی به React می‌گوید که چه زمانی effect را دوباره اجرا کند. اگر آرایه وابستگی خالی باشد ([])، effect فقط یک بار پس از رندر اولیه اجرا می‌شود. اگر آرایه وابستگی حذف شود، effect پس از هر رندر اجرا می‌شود.

اهمیت پاکسازی Effect

کلید جلوگیری از نشت حافظه در React، پاکسازی هرگونه عارضه جانبی در زمانی است که دیگر مورد نیاز نیستند. اینجاست که تابع پاکسازی وارد عمل می‌شود. هوک useEffect به شما اجازه می‌دهد یک تابع را از تابع عارضه جانبی بازگردانید. این تابع بازگردانده شده، تابع پاکسازی است و زمانی اجرا می‌شود که کامپوننت unmount می‌شود یا قبل از اینکه effect دوباره اجرا شود (به دلیل تغییر در وابستگی‌ها).

در اینجا یک مثال ساده آورده شده است:


import React, { useState, useEffect } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Effect ran');

    // این تابع پاکسازی است
    return () => {
      console.log('Cleanup ran');
    };
  }, []); // آرایه وابستگی خالی: فقط یک بار هنگام mount شدن اجرا می‌شود

  return (
    

Count: {count}

); } export default MyComponent;

در این مثال، console.log('Effect ran') یک بار هنگام mount شدن کامپوننت اجرا می‌شود. console.log('Cleanup ran') زمانی اجرا می‌شود که کامپوننت unmount شود.

سناریوهای رایج نیازمند پاکسازی Effect

بیایید برخی از سناریوهای رایج را که در آنها پاکسازی effect حیاتی است، بررسی کنیم:

۱. تایمرها (setTimeout و setInterval)

اگر از تایمرها در هوک useEffect خود استفاده می‌کنید، ضروری است که آنها را هنگام unmount شدن کامپوننت پاک کنید. در غیر این صورت، تایمرها حتی پس از از بین رفتن کامپوننت به کار خود ادامه می‌دهند و منجر به نشت حافظه و خطاهای احتمالی می‌شوند. به عنوان مثال، یک مبدل ارز با به‌روزرسانی خودکار را در نظر بگیرید که نرخ‌های ارز را در فواصل زمانی مشخص واکشی می‌کند:


import React, { useState, useEffect } from 'react';

function CurrencyConverter() {
  const [exchangeRate, setExchangeRate] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      // شبیه‌سازی واکشی نرخ ارز از یک API
      const newRate = Math.random() * 1.2;  // مثال: نرخ تصادفی بین ۰ و ۱.۲
      setExchangeRate(newRate);
    }, 2000); // به‌روزرسانی هر ۲ ثانیه

    return () => {
      clearInterval(intervalId);
      console.log('Interval cleared!');
    };
  }, []);

  return (
    

Current Exchange Rate: {exchangeRate.toFixed(2)}

); } export default CurrencyConverter;

در این مثال، از setInterval برای به‌روزرسانی exchangeRate هر ۲ ثانیه یک‌بار استفاده می‌شود. تابع پاکسازی از clearInterval برای متوقف کردن اینتروال هنگام unmount شدن کامپوننت استفاده می‌کند و از ادامه اجرای تایمر و ایجاد نشت حافظه جلوگیری می‌کند.

۲. شنوندگان رویداد (Event Listeners)

هنگام افزودن شنوندگان رویداد در هوک useEffect، باید آنها را هنگام unmount شدن کامپوننت حذف کنید. عدم انجام این کار می‌تواند منجر به اتصال چندین شنونده رویداد به یک عنصر شود که باعث رفتار غیرمنتظره و نشت حافظه می‌شود. به عنوان مثال، کامپوننتی را تصور کنید که به رویدادهای تغییر اندازه پنجره گوش می‌دهد تا طرح‌بندی خود را برای اندازه‌های مختلف صفحه تنظیم کند:


import React, { useState, useEffect } from 'react';

function ResponsiveComponent() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      setWindowWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
      console.log('Event listener removed!');
    };
  }, []);

  return (
    

Window Width: {windowWidth}

); } export default ResponsiveComponent;

این کد یک شنونده رویداد resize را به پنجره اضافه می‌کند. تابع پاکسازی از removeEventListener برای حذف شنونده هنگام unmount شدن کامپوننت استفاده می‌کند و از نشت حافظه جلوگیری می‌کند.

۳. اشتراک‌ها (Websockets, RxJS Observables و غیره)

اگر کامپوننت شما با استفاده از وب‌سوکت‌ها، RxJS Observables یا سایر مکانیزم‌های اشتراک، به یک جریان داده مشترک می‌شود، لغو اشتراک (unsubscribe) هنگام unmount شدن کامپوننت بسیار حیاتی است. فعال نگه داشتن اشتراک‌ها می‌تواند منجر به نشت حافظه و ترافیک شبکه غیرضروری شود. مثالی را در نظر بگیرید که در آن یک کامپوننت برای دریافت قیمت‌های لحظه‌ای سهام به یک فید وب‌سوکت مشترک می‌شود:


import React, { useState, useEffect } from 'react';

function StockTicker() {
  const [stockPrice, setStockPrice] = useState(0);
  const [socket, setSocket] = useState(null);

  useEffect(() => {
    // شبیه‌سازی ایجاد یک اتصال WebSocket
    const newSocket = new WebSocket('wss://example.com/stock-feed');
    setSocket(newSocket);

    newSocket.onopen = () => {
      console.log('WebSocket connected');
    };

    newSocket.onmessage = (event) => {
      // شبیه‌سازی دریافت داده قیمت سهام
      const price = parseFloat(event.data);
      setStockPrice(price);
    };

    newSocket.onclose = () => {
      console.log('WebSocket disconnected');
    };

    newSocket.onerror = (error) => {
      console.error('WebSocket error:', error);
    };

    return () => {
      newSocket.close();
      console.log('WebSocket closed!');
    };
  }, []);

  return (
    

Stock Price: {stockPrice}

); } export default StockTicker;

در این سناریو، کامپوننت یک اتصال WebSocket به یک فید سهام برقرار می‌کند. تابع پاکسازی از socket.close() برای بستن اتصال هنگام unmount شدن کامپوننت استفاده می‌کند و از فعال ماندن اتصال و ایجاد نشت حافظه جلوگیری می‌کند.

۴. واکشی داده با AbortController

هنگام واکشی داده در useEffect، به‌ویژه از APIهایی که ممکن است پاسخ‌دهی آنها زمان‌بر باشد، باید از یک AbortController برای لغو درخواست fetch در صورتی که کامپوننت قبل از تکمیل درخواست unmount شود، استفاده کنید. این کار از ترافیک شبکه غیرضروری و خطاهای احتمالی ناشی از به‌روزرسانی state کامپوننت پس از unmount شدن آن جلوگیری می‌کند. در اینجا مثالی برای واکشی داده‌های کاربر آورده شده است:


import React, { useState, useEffect } from 'react';

function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/user', { signal });
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        setUser(data);
      } catch (err) {
        if (err.name === 'AbortError') {
          console.log('Fetch aborted');
        } else {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    return () => {
      controller.abort();
      console.log('Fetch aborted!');
    };
  }, []);

  if (loading) {
    return 

Loading...

; } if (error) { return

Error: {error.message}

; } return (

User Profile

Name: {user.name}

Email: {user.email}

); } export default UserProfile;

این کد از AbortController برای لغو درخواست fetch در صورتی که کامپوننت قبل از بازیابی داده‌ها unmount شود، استفاده می‌کند. تابع پاکسازی متد controller.abort() را برای لغو درخواست فراخوانی می‌کند.

درک وابستگی‌ها در useEffect

آرایه وابستگی در useEffect نقش بسیار مهمی در تعیین زمان اجرای مجدد effect دارد. این آرایه همچنین بر تابع پاکسازی تأثیر می‌گذارد. درک نحوه کار وابستگی‌ها برای جلوگیری از رفتار غیرمنتظره و اطمینان از پاکسازی صحیح، ضروری است.

آرایه وابستگی خالی ([])

وقتی یک آرایه وابستگی خالی ([]) ارائه می‌دهید، effect فقط یک بار پس از رندر اولیه اجرا می‌شود. تابع پاکسازی نیز فقط هنگام unmount شدن کامپوننت اجرا خواهد شد. این روش برای عوارض جانبی که فقط یک بار نیاز به تنظیم دارند، مانند مقداردهی اولیه یک اتصال وب‌سوکت یا افزودن یک شنونده رویداد سراسری، مفید است.

وابستگی‌ها با مقادیر

وقتی یک آرایه وابستگی با مقادیر ارائه می‌دهید، effect هر زمان که هر یک از مقادیر موجود در آرایه تغییر کند، دوباره اجرا می‌شود. تابع پاکسازی *قبل* از اجرای مجدد effect اجرا می‌شود و به شما این امکان را می‌دهد که effect قبلی را قبل از تنظیم effect جدید پاکسازی کنید. این امر برای عوارض جانبی که به مقادیر خاصی بستگی دارند، مانند واکشی داده بر اساس شناسه کاربر یا به‌روزرسانی DOM بر اساس state کامپوننت، مهم است.

این مثال را در نظر بگیرید:


import React, { useState, useEffect } from 'react';

function DataFetcher({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    let didCancel = false;

    const fetchData = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        const result = await response.json();
        if (!didCancel) {
          setData(result);
        }
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();

    return () => {
      didCancel = true;
      console.log('Fetch cancelled!');
    };
  }, [userId]);

  return (
    
{data ?

User Data: {data.name}

:

Loading...

}
); } export default DataFetcher;

در این مثال، effect به پراپ userId بستگی دارد. هر زمان که userId تغییر کند، effect دوباره اجرا می‌شود. تابع پاکسازی، فلگ didCancel را به true تغییر می‌دهد، که از به‌روزرسانی state در صورتی که درخواست fetch پس از unmount شدن کامپوننت یا تغییر userId کامل شود، جلوگیری می‌کند. این کار از نمایش هشدار "Can't perform a React state update on an unmounted component" جلوگیری می‌کند.

حذف آرایه وابستگی (با احتیاط استفاده کنید)

اگر آرایه وابستگی را حذف کنید، effect پس از هر رندر اجرا می‌شود. این کار به طور کلی توصیه نمی‌شود زیرا می‌تواند منجر به مشکلات عملکردی و حلقه‌های بی‌نهایت شود. با این حال، موارد نادری وجود دارد که ممکن است ضروری باشد، مانند زمانی که نیاز دارید بدون لیست کردن صریح props یا state به عنوان وابستگی، به آخرین مقادیر آنها در داخل effect دسترسی داشته باشید.

مهم: اگر آرایه وابستگی را حذف کنید، باید در مورد پاکسازی هرگونه عارضه جانبی *بسیار* مراقب باشید. تابع پاکسازی قبل از *هر* رندر اجرا می‌شود که می‌تواند ناکارآمد باشد و در صورت عدم مدیریت صحیح، به طور بالقوه باعث ایجاد مشکل شود.

بهترین شیوه‌ها برای پاکسازی Effect

در اینجا چند مورد از بهترین شیوه‌ها برای استفاده از پاکسازی effect آورده شده است:

ابزارهایی برای تشخیص نشت حافظه

چندین ابزار می‌توانند به شما در تشخیص نشت حافظه در اپلیکیشن‌های React کمک کنند:

نتیجه‌گیری

تسلط بر پاکسازی effect در React برای ساخت اپلیکیشن‌های React قوی، کارآمد و با مصرف حافظه بهینه، ضروری است. با درک اصول پاکسازی effect و پیروی از بهترین شیوه‌های ذکر شده در این راهنما، می‌توانید از نشت حافظه جلوگیری کرده و تجربه کاربری روانی را تضمین کنید. به یاد داشته باشید که همیشه عوارض جانبی را پاکسازی کنید، مراقب وابستگی‌ها باشید و از ابزارهای موجود برای تشخیص و رفع هرگونه نشت حافظه احتمالی در کد خود استفاده کنید.

با به کارگیری دقیق این تکنیک‌ها، می‌توانید مهارت‌های توسعه React خود را ارتقا دهید و اپلیکیشن‌هایی بسازید که نه تنها کاربردی، بلکه کارآمد و قابل اعتماد نیز باشند و به تجربه کاربری بهتر برای کاربران در سراسر جهان کمک کنند. این رویکرد پیشگیرانه در مدیریت حافظه، توسعه‌دهندگان باتجربه را متمایز می‌کند و قابلیت نگهداری و مقیاس‌پذیری بلندمدت پروژه‌های React شما را تضمین می‌کند.