Musician / WebHead
ریاکت: واقعا تفاوت فانکشن کامپوننت با کلاس کامپوننت چیه؟
دقیقا فانکشن کامپوننتها چجوری با کلاس کامپوننتها متفاوتند؟
برای مدت زیادی، تنها جوابی که درجا به ذهن میرسید این بود که کلاس کامپوننت ها امکانات بیشتری در اختیار ما میذارند ( مثل مدیریت state ). با اومدن Hooks ها این گذاره دیگه بکار نمیاد.
شاید شنیده باشید که یکیشون نسبت به اون یکی پرفورمنس بهتری داره. کدومشون؟ خیلی از اعداد و ارقامی که منتشر میشه ناقص هستند و خیلی نمیشه بهشون اتکایی کرد، منم بر اساس اونها نتیجه گیری نمیکنم. پرفرومنس در درجهی اول به اینکه کدتون دقیقا داره چیکار میکنه بستگی داره، تا اینکه کلاس یا فانکشنال بودن کامپوننت بخواد روش تاثیری بذاره. تا جایی که ما مشاهده کردیم، تفاوت پرفورمنس بسیار ناچیزه، البته استراتژیهای بهینه سازی کمی متفاوت از این جریان هستند.
در هر حالت پیشنهاد ما این نیست که بشینید کدتون رو بازنویسی کنید، اگرم اینکار رو میخواید انجام بدید باید دلایل بیشتری داشته باشید. hooks هنوز در ابتدای راه قرار داره ( مثل خود React در سال 2014 ) و خیلی از بهترین روشها هنوز پیدا نشدند و توی فیلمهای آموزشی دیده نمیشد.
خب این مارو به کجا میرسونه؟ آیا اصلا تفاوت اساسی بین فانکشنال کامپوننتها با کلاس کامپوننتها هست؟ البته که هست، توی الگوی ذهنی.
در این مقاله ما به بزرگترین تفاوت بین این دو نگاه میکنیم. از زمانی که در سال 2015 فانکشن کامپوننتها معرفی شدند این تفاوت وجود داشت.
فانکشن کامپوننتها مقدار (value) رندر شده را ذخیره میکنند.
بریم ببینیم که این یعنی چی.
این کمپوننت رو در نظر داشته باشید:
یک دکمه Follow نمایش داده شده که یکجورایی درخواست Follow ( مثل شبکههای اجتماعی ) رو با کمک setTimeout شبیه سازی میکنه و بعد از 3 ثانیه، پیام تاییدیه رو به شکل alert نمایش میده. اگر prop.user مثلا 'Dan' باشه، بعد از 3 ثانیه پیغام 'Followed Dan' رو نمایش میده. بسیار ساده.
( این نکته رو بگم که مهم نیست که در مثال بالا از arrow function استفاده بشه یا function declarations. در هر صورت handleClick کاملا یکسان عمل میکنه )
اگر بخوایم مثال بالا رو به شکل کلاس کامپوننت بنویسیم چجوری میشه؟ یکجور ترجمهی ساده انگارانش میشه به این شکل:
اینکه فکر کنیم این دوتا کد کاملا یکسال هستند خیلی تفکر رایجیه. مردم اکثرا خیلی راحت و بدون هیچ نگرانی و شناخت تفاوت مفاهیم این دو نوع، کدهاشون رو از این مدل به اون مدل تغییر میدن و فکر میکنند که کاملا یکسان هستند.
درواقع، این دو تیکه کد تفاوت ظریفی باهم دیگه دارند. خیلی خوب بهشون نگاه کنید، تفاوتشون رو هنوز پیدا نکردید؟ شخصا برای من خیلی طول کشید تا متوجه تفاوت بشم.
کمی پایینتر کاملا این تفاوت براتون شفاف و واضح میشه، اما اگر قبلش خودتون میخواید امتحان کنید تا تفاوت رو پیدا کنید، میتونید از این دمو استفاده کنید. کمی جلوتر دقیقا تفاوتشون رو توضیح میدیم.
قبل از اینکه ادامه بدیم این نکته رو هم اضافه کنم که تفاوتی که میخوایم راجع بهش صحبت کنیم ربطی به React Hooks نداره. در مثالهای بالا حتی از Hooks استفاده نشده.
این موضوع کاملا در مورد تفاوت فانکشنها و کلاسها در React هست. اگر در نظر دارید که توی برنامههاتون بیشتر از فانکشنها استفاده کنید، به نظرم باید خوب به این تفاوت مسلط بشید.
این تفاوت رو ما با یک باگی که خیلی توی اپلیکیشنهای ریاکت شایع هست نشون میدیم.
این نمونه مثال رو باز کنید. در این مثال یک منو برای انتخاب پروفایل قرار گرفته، بهمراه دو دکمه فالو کردن که یکی به شکل فانکشنال نوشته شده و یکی به شکل کلاس. هر دو هم میخوان اینکه کی فالو شده رو به شکل alert نشون بدند.
حالا این کارها رو به ترتیب انجام بدید:
- یکی از دکمههای فالو رو کلیک کنید.
- پروفایل انتخاب شده رو قبل از 3 ثانیه تغییر بدید.
- پیغام نمایش داده شده رو بخونید.
مطمئنا متوجه تفاوت عجیب و غریبی میشید:
- اگر صفحه روی Dan باشه و شما دکمهی فالوی فانکشنال رو بزنید و زیر 3 ثانیه، صفحه رو به Sophie تغییر بدید، میبینید که پیغام نمایش داده شده همچنان 'Followed Dan' ـه.
- اگر همینکار رو با دکمه فالوی کلاس انجام بدید و زیر 3 ثانیه، صفحه رو تغییر بدید میبینید که پیغام نمایش داده شده میشه 'Followed Sophie'
در این مثال، رفتار درست همون اتفاق اول هست. اگر من کسی رو فالو کردم ولی بعد رفتم به پروفایل شخص دیگهای، کامپوننت من نباید گیج بشه که من کی رو فالو کردم. کلاس کامپوننت اینجا گیج میزنه و مسبب باگ میشه.
بیاید دقیقتر نگاه کنیم که چرا کامپوننت کلاس ما اینطور رفتار میکنه؛ برای اینکار باید متد showMessage ـش رو بررسی کنیم.
این متد کلاس ما یوزر رو از روی this.props.user میخونه. توی ری اکت props ها غیر قابل تغییر هستند (immutable). پس هیچوقت نمیتونند عوض بشند. اما اما اما ! این this همیشه قابل تغییر بوده! ?
مسلما تمام غایت وجود this توی کلاسها همینه. خود ریاکت به مرور اون رو تغییر میده تا بتونه جدیدترین ورژن رو توی متدهای render و lifecycle داشته باشه.
پس اگر کامپوننت ما زمانی که درخواستی فرستاده شده ری-رندر بشه، this.props تغییر میکنه. متد showMessage هم user رو از "زیادی جدیدترین" props ما میخونه.
این مشاهده مارو در معرض درک جالبی از طبیعت رابط کاربری قرار میده. اگر ما میگیم که رابط کاربری به شکل مفهومی مثل فانکشنی هست که state حاضر اپلیکیشن رو میخونه، پس! event handlers ها ( مثل showMessage ) هم قسمتی از نتیجه render هستند - دقیقا مانند خروجی بصری. event handler های ما "متعلق" به رندر خودشون با state و props همون رندر هستند.
بیاید فرض کنیم که فانکشنال کامپوننت وجود نداشت. چطور میتونستیم این مشکل رو حل کنیم؟
ما میخوایم که یکجورایی ارتباط render با prop صحیح رو با showMessage که اون prop رو میخونه "تعمیر" کنیم. یکجاهای در مسیر props ما گم میشه.
یک راهی که وجود داره اینه که مقدار this.props رو زودتر بخونیم و صریحا اون مقدار رو به تابع showMessage بدیم
این روش کامل جواب میده و کار میکنه. اما این روش کد رو به طرز قابل ملاحظهای طولانی میکنه و به مرور زمان مستعد خطا میشه. اگر به بیشتر از یک prop نیاز داشتیم چی؟ اگر همزمان نیاز بود که به state هم دسترسی داشته باشیم چی؟ اگر showMessage خودش یک متد دیگهای رو صدا میزد و اون متد مقدار دیگه ای props رو میخواست بخونه یا میخواست state رو بخونه، ما دوباره همین مشکل رو داشتیم. اون زمان میبایست مقدارهای this.props و this.state رو به شکل argument به تمام متدهایی که توسط showMessage صدا زده میشدند پاس میدادیم.
انجام اینکار تمام ارگونومیای که توسط کلاسها ارائه میشه رو از بین میره. همچنین به خاطر سپردن مسیر کد رو و اجرا رو سختتر میکنه و بخاطر همینم هست که اکثرا برنامه نویسها تمام زورشون رو برای رفع باگ میذارند.
بطور مشابه، قراردادن alert درون handleClick هم به نظر نمیاد مشکل مارو حل کنه. ما میخوایم که ساختار کدمون به شکلی باشه که به ما اجازه بده که بتونیم به متدهای بیشتر و متفاوتی تقسیمش کنیم اما! خیالمون راحت باشه که مقدار state و props صحیح مرتبط با همون رندر-کال خودش خونده و اجرا میشه.
این مشکل حتی فقط مربوط به ریاکت نیست! شما میتونید این باگ رو با تمام کتابخونههای رابط کاربری که از this استفاده میکنند شبیه سازی کنید.
شاید اگر متدهارو توی constructor قرار بدیم مشکلمون حل بشه؟
خیر، این به هیچ وجه مشکل رو حل نمیکنه. یادتون باشه که مشکل ما اینه که مقدار this.props رو داریم دیر میخونیم، ربطی به سینتکسی که استفاده میکنیم نداره! البته اگر کاملا از closure استفاده کنیم مشکل ما حل میشه.
اکثرا همه Coluser رو ندیده میگیرند چون شاید درک اینکه یک مقدار در طی زمان میتونه تغییر کنه (mutate بشه) سخت باشه. اما توی ریاکت ما این مشکل رو نداریم، چون state و props ما غیرقابل تغییر هستند (حداقل بطور قوی این موضوع پیشنهاد داده شده).
این یعنی اگر ما یک state و props رو درون render خودش به دام بندازید (!) میتونید مطمئنا باشید که همیشه مقدار اون دقیقا همون چیزی هست که مربوط به همون render خودشه!
توی مثال بالا، تونستید مقدار prop رو زمان رندر ضبط کنید!
با این روش، هر کدی درون اون رندر (مثل همون showMessage) صددرصد مقدار props مربوط به همون render رو میبینه. ایاکت دیگه مهرههای مارو تکون نمیده!
مثال بالا کاملا درسته ولی در عین حال عجیب هم هست! چه معنی داره از کلاس کامپوننت استفاده کنیم ولی فانکشنهامون رو توی متد render تعریف کنیم بجای اینکه بخوایم از متدهای کلاس استفاده کنیم؟
پس، میتونیم خیلی راحت کدمون رو ساده کنیم و اون "پوسته" ی کلاس دورش رو حذف کنیم ?
دقیقا مثل مثال قبل، توی این حالت هم props کماکان ضبط و ذخیره میشه - ریاکت به شکل آرگیومنت اونارو پاس میده و برخلاف this ، دیگه ابجکت props توسط ریاکت mutate نمیشه.
حتی اگر از object destruction استفاده کنید این موضوع واضح تر میشه:
زمانی که کامپوننت مادر، ProfilePage رو رندر میگیره، ریاکت دوباره دکمهی فالو فانکشنال رو صدا میزنه. ولی event handler های ما همچنان "متعلق" به رندر قبلی هستند و مقدار user قبلی رو درون خودشون نگه داشتند و متد showMessage از همون استفاده میکنه.
بخاطر همین موضوعه که توی مثال ما، ورژن فانکشنال، زمانی که سوفی رو فالو میکنید ولی درجا پروفایل رو به سانیل تغییر میدید ( رندر جدید ) همچنان مقدار رندر قبلی یعنی 'Followed Sophie' رو نشون میده. چون اون event hanlder متعلق به رندر قبلی بوده، و فانکشنال کامپوننت مقدار قبلی رو توی خودش نگه داشته.
این رفتار کاملا درسته!
حالا شاید بهتر این جمله رو درک کنیم:
فانکشن کامپوننتها مقادیر رندر شده را ذخیره میکنند.
با هوکها هم میشه این نظام رو برای state ها هم بخوبی اجرا کرد. این مثال رو در نظر بگیرید:
با اینکه این برنامهی ما خیلی مسنجر خوبی نیست، اما نکتهی مدنظر مارو بخوبی نمایان میکنه: اگر من پیامی فرستادم، کامپوننت ما نباید گیچ بشه که چه پیامی فرستاده شده.
توی این فانکشن کامپوننت ما، message مقدار state ایی که "متعلق" به رندری که تابع event handler توسط مرورگر باهاش صدا زده شده رو ذخیره میکنه. پس مقدار message برابر با اونچیزی هست که موقع کلیک روی 'Send" توی input بوده!
خب تا اینجای کار ما میدونیم که فانکشنها توی ریاکت بطور پیشفرض مقادیر state و props رو ذخیره میکنند. اما چی میشه اگر ما بخوایم "جدیدترین" مقادیر props و state رو بخونیم که "متعلق" به این رندر "نیستند" ؟ چی میشه اگر ما بخوایم اونهارو از "آینده" بخونیم؟
توی کلاس کامپوننتها شما خیلی راحت با this.props و this.state این مقادیر رو میخونید، چون خود this تغییر پذیره و همیشه آخرین و جدیدترین مقادیر رو نشون میده.
توی فانکشنال کمپوننت هم شما میتونید مقادیر تغییر پذیر داشته باشید که بین تمام رندرهای کامپوننتتون به "اشتراک" گذاشته بشه. بهش میگن 'ref' :
البته باید خودتون مدیریتش کنید.
هوک useRef یکجورایی توی زمین instance ها بازی میکنه. یکجور راه گریزی به دنیای mutable هاست. حتی بطور بصری میشه this.something رو یکجورایی آینهی something.current دونست؛ مفهوم یکسانی دارند.
بطور پیش فرض، ریاکت توی فانکشنال کامپوننت هیچ رفرنسی به جدیدترین مقادیر props و state ایجاد نمیکنه. در بیشتر مواقع شما بهش نیازی ندارید، اما اگر در یک شرایطی نیاز داشتید، میتونید به شکل دستی خودتون این رفرنس رو ایجاد کنید:
اگر ما مقدار message درون showMessage رو بخونیم میبینیم که زمان کلیک دکمه Send داشتیم رو نشون میده، اما اگر مقدار latesMessage.current رو بخونیم، میبینیم که ما جدیدترین مقدار رو دریافت میکنیم - حتی اگر بعد از کلیک روی Send هم تایپ کنیم باز اون مقدار لحاظ میشه.
بطور کلی، بهتره که در زمان رندر خیلی از رفرنسها استفاده نشه، چون اونها تغییر پذیرند و خوبه که ازشون مقادیر رو نخونیم، ما میخوایم که رندرهای ما پیش بینی پذیر باشند. اما اگر تحت شرایطی نیاز بود که آخرین مقدار از state یا prop رو بخونید، اینکه بخواید دستی اینکار رو انجام بدید خیلی آزاردهنده میشه. برای اینکار میتونید از useEffect استفاده کنید:
با اینکار، ما مطمئنیم که هربار Dom اپدیت شد رفرنس ما هم آپدیت میشه و این به ما این اطمینان رو میده که تغییر پذیری این مقدار باعث break شدن امکانهایی مثل Suspense و Time Slicing نمیشه.
توی این مقاله، ما به الگوهایی که باعث خراب شدن کلاس کامپوننتها میشه نگاه کردیم و دیدیم که با کمک Closure چطور میتونیم اون رو حل کنیم. البته شاید متوجه این موضوع شده باشید که زمانی که میخواید hooks هارو با کمک dependency بهینه کنید، باگهایی از سمت closure ایجاد میشه. این به این معنیه که کلوژر مشکله؟ بعید بدونم.
همونطور که توی مثالهای بالا دیدیم، دراصل کلوژر مشکلی که سخت بود دیدنش رو کامل حل میکنه. همزمان هم باعث میشه راحتتر بشه کدی نوشت که توی حالت Concurrent درست کار کنه. این امکان به این دلیل وجود داره که منطق موجود درون کامپوننت ما مقادیر متعلق به رندر خودش رو حفظ میکنه.
توی تمام حالتهایی که تا به حال دیدم، این مشکل کلوژر بیشتر به این دلیل رخ میده که ما فرض بر این داریم که "توابع تغییر نمیکنند" و یا "مقدار props همیشه ثابت میمونه". موضوع اینه که مسئله اینها نیست و امیدوارم که این مقاله تونسته باشه این رو مسئله واضح نشون بده.
فانکشنها مقادیر state و props رو درون خودشون ذخیره میکنند. این باگ نیست بلکه خاصیت فانکشنال کامپوننتهاست.
فانکشنها در ریاکت همیشه مقادیر خودشون رو ذخیره میکنند - و حالا شما هم میدونید که چرا
این مقاله ترجمهای بود از مقالهای از Dan Abramov از اعضای تیم ریاکت و نویسنده Redux.
زمانی که این مقاله رو خوندم حیفام اومد که جامعه برنامهنویس ایران دسترسی به این مقاله رو نداشته باشه و میدونستم خیلیها شاید هنوز راحت مقالههای انگلیسی رو نتونند بخونند. برای همین تصمیم گرفتم حتما این مقاله رو ترجمه کنم و اینجا قرار بدم.
مطالب دیگهی من که میتونید بخونید:
مطلبی دیگر از این انتشارات
هر کاری یه اسکریپت پایتون داره - Python Cli
مطلبی دیگر از این انتشارات
اگر اینطوری هستی ... #کامپیوتر_نخون
مطلبی دیگر از این انتشارات
کتاب پایتون مقدماتی