ریکت
01 شهریور 1402
داکیومنت ریکت - قسمت ششم
سلام دوستان امیدوارم خوب باشید و همیشه درحال یادگیری🧑💻. توی این قسمت از داکیومنت ریکت قرار هست، این موضوعات رو بررسی کنیم:
- بررسی عمیق تر استیت ها و فرایند رندر مجدد
- batching چیست؟
- mutation چیست؟
استیت شبیه snapshot
استیت ها شاید شبیه متغیر های جاوا اسکریپت باشند که میتونیم مقدار رو بخونیم یا بنویسیم. اما استیت ها بیشتر به تصویر لحظه ایی (snapshot) شبیه هستند که یکسری اطلاعات رو به سرعت به ما منتقل میکنند، تا وضعیت فعلی رو داشته باشیم. صدا زدن تابع setter مقدار فعلی استیت رو تغییر نمیده، ولی باعث ایجاد رندر مجدد میشود.
در زمان رندر کردن یک تصویر لحظه ایی گرفته میشود
JSX بازگشتی که از یک کامپوننت در React برگردانده میشود، در واقع نمایشی از UI در یک زمان خاص است. این نمایش شامل وضعیت فعلی کامپوننت است، از جمله پراپها، event handlers و متغیرهای محلی آن. این امکان را به React میدهد تا به طور بهینه کامپوننتها را هنگام تغییر وضعیتشان بهروزرسانی و رندر مجدد کند.
هنگامی که استیت یک کامپوننت تغییر میکند، React کامپوننت را مجدداً بررسی میکند و بر اساس تغییراتی که داخلش بوجود اومده، یک نمایش JSX جدید ایجاد میکند. سپس این JSX جدید با JSX قبلی مقایسه میشود تا حداقل تغییرات مورد نیاز برای بهروزرسانی DOM اصلی مشخص شود.
با استفاده از این رویکرد، React فرآیند رندرینگ را بهینهسازی میکند و فقط بخشهای لازم UI را بهروزرسانی میکند که باعث بهبود عملکرد میشود.
پس بصورت خلاصه در زمان رندر مجدد:
- ریکت کامپوننت هامون رو دوباره فراخوانی میکند.
- کامپوننت ما یک عکس (snapshot) جدید از JSX ما رو که آماده کرده تحویل میدهد.
- ریکت طبق اون عکس (snapshot) جدیدی که تحویلش دادیم، میاد دام رو آپدیت میکند.
در واقع وقتی یک استیت رو آپدیت میکنیم، ریکت ابتدا میاد مقدار اون استیت رو آپدیت میکنه و بعدش یک عکس (snapshot) جدید با مقادیر جدید استیت آماده میکنه و snapshot برای آپدیت کامپوننت ارسال میشه.
داخل مثال الان انتظار دارید وقتی روی دکمه 3+
کلیک کنید مقدار number
به 3
تغییر کند چون setNumber(number + 1)
سه بار تکرار شده.
اما اینطور نیست، وقتی کلیک میکنید این اتفاق میوفته:
- در
setNumber(number + 1)
اول میبینه مقدارnumber
برابر صفر هست پس یک عدد بهش اضافه میکنه وnumber
تبدیل به یک می شود. ریکت آماده رندر مجدد می شود. - در
setNumber(number + 1)
دوم میبینه مقدارnumber
برابر صفر هست پس یک عدد بهش اضافه میکنه وnumber
تبدیل به یک می شود. ریکت آماده رندر مجدد می شود. - در
setNumber(number + 1)
سوم میبینه مقدارnumber
برابر صفر هست پس یک عدد بهش اضافه میکنه وnumber
تبدیل به یک می شود. ریکت آماده رندر مجدد می شود.
در واقع ما مقدار number رو سه بار داریم 1 میکنیم.
<button onClick={() => {
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
}}>+3</button>
استیت در طول زمان
در مثال زیر داخل تابع onClick
دکمه اومدیم از Alert استفاده کردیم، فکر میکنید Alert چی نشون میده؟
همینطور که انتظار داریم با کلیک اول "0" رو نشون میده یعنی مقدار فعلی استیت و بعد رندر مجدد انجام میشود:
setNumber(0 + 5);
alert(0);
حالا اگه از setTimeout
استفاده کنیم، و Alert بعد رندر مجدد اجرا بشه الان بنظرتون "0" رو نشون میده یا "5"؟
اما هنوز هم "0" داره نمایش داده میشه، چرا؟ اگه از متد جایگزینی استفاده کنیم خیلی راحت میتونیم علتش رو متوجه بشیم:
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);
در زمانی که مقدار Alert مشخص شده، مقدار state ما "0" بوده، در نتیجه از عکس کامپوننت در اون زمان مقداردهی شده، حالا که کامپوننت رندر مجدد شده مقدار استیت تغییر کرده و یک عکس جدید از کامپوننت گرفته شده، فرض کنید در این حالت Alert بیاد اجرا بشه، انتظار میره که مقدار "0" رو به ما نشون بده چون داره از عکس قبلی کامپوننت استفاده میکنه، نه عکس جدید، که توقع داشته باشیم "5" رو نشون بده.
batching چیست؟
در بالا ما یک مثال داشتیم که بصورت زیر بود و داخل onClick
سه بار از تابع setter استفاده کرده بودیم:
<button onClick={() => {
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
}}>+3</button>
در نهایت هم مقدار استیت number
همون "1" میشد. اما چرا و چطوری میشه حلش کرد؟
در کد بالا ما سه بار setNumber
رو داریم صدا میزنیم اما سه بار رندر مجدد انجام نمیشه! ریکت منتظر میمونه تا تمام setNumber
ها اجرا بشن بعدش رندر مجدد میکنه!
اگه همون مثال آشپزخونه رو بزنیم، ریکت که گارسون هستش وقتی سر میز شما میاد منتظر هست تا تمام سفارش هاتون رو بگیره و بعد اتمام سفارش میز شما رو ترک میکنه.
به اینکار ریکت batching گفته میشه، که باعث میشه جلو رندر های مجدد ناقص رو بگیره و اپ ما سریع تر بشه.
آپدیت کردن استیت چندین بار قبل از رندر بعدی
در تابع setter استیت ما میتونیم، مقداری که داخل صف برای رندر مجدد قرار دارد (یا بهتره بگیم مقدار فعلی و آپدیت شده استیت) رو بگیریم و اون رو مجدد آپدیت کنیم، مثل setNumber(n => n + 1)
.
اینجا به n => n + 1
میگن updater function وقتی که این رو به تابع setter پاس میدیم:
- ریکت این فانکشن رو داخل صف قرار میده تا بقیه کد های event handler ما ران بشن.
- بعد از رندر مجدد، ریکت میره سراغ این فانکشن که توی صف بوده و مقدار نهایی استیت رو تحویل میده.
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
این سه خط بالا به این شکل اجرا میشن:
-
setNumber(n => n + 1)
: اینn => n + 1
یک تابع هست پس داخل صف قرار میگیره. -
setNumber(n => n + 1)
: اینn => n + 1
یک تابع هست پس داخل صف قرار میگیره. -
setNumber(n => n + 1)
: اینn => n + 1
یک تابع هست پس داخل صف قرار میگیره.
بعد از رندر مجدد که useState
رو فراخوانی میکنیم، ریکت سراغ این صف میره. مقدار قبلی number
صفر هست، پس در فانکشن اول مقدار n
صفر میشه. باز مقدار استیت رو از فانکشن اولی میگیره و تحویل فانکشن دومی میده و...
queued update | n | returns |
---|---|---|
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 1 = 3 |
در نهایت مقدار 3
رو به useState
تحویل میده.
با استفاده از جایگذاری خیلی راحت میتونید متوجه مقدار استیت بشید. مثلا تلاش کنید اینو خودتون حدس بزنید:
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
mutation چیست؟
داخل استیت ها یک مفهومی داریم به اسم mutation. حالا با یک مثال اینو توضیح میدیم.
فرض کنید یک استیت تعریف کردین بصورت زیر:
const [position, setPosition] = useState({ x: 0, y: 0 });
حالا برای آپدیت کردنش بجای اینکه از تابع setter استفاده کنیم به این شکل بیایم استیت رو آپدیت کنیم:
position.x = 5;
به اینکار mutation گفته میشود. داخل ریکت آبجکت ها از نظر تکنیکی mutable هستن ولی باید باهاشون شبیه immutable ها مثل اعداد, رشته و بولین ها رفتار کنیم. یعنی مقدار آبجکت قبلی رو با آبجکت جدید بیایم عوض کنیم.
در این مثال زمانی که موس روی صفحه حرکت میکند ما مقدار x
و y
را که داخل استیت position
قرار داره بصورت mutable آپدیت میکنیم.
خوب حالا انتظار داریم مقدار استیت ما آپدیت بشه، اما اینطوری نیست. ریکت هیچ ایده ایی نداره که این آبجکت ما آپدیت شده و رندر مجددی اتفاق نمیوفته.
برای اینکه این مشکل حل بشه از تابع setter میایم استفاده میکنیم.
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
با استفاده از setPosition
به ریکت میگیم:
- مقدار
position
رو با آبجکت جدید عوض کن. - کامپوننت رو دوباره رندر کن.
Immer
ریکت پیشنهاد داده برای اینکه update handler ها مختصر بشن، مخصوصا اگر آبجکت های تو در تو باشه از useImmer استفاده کنید.
در واقع خودش متوجه میشه کدوم قسمت از آبجکت ما تغییر کرده و بر اساس تغییری که داشتیم آبجکت جدید رو ایجاد میکنه و استیت ما آپدیت میشه.
آپدیت کردن آرایه بدون mutation
تمام مواردی که درباره کار با آبجکت ها گفتیم درباره آرایه ها هم صدق میکنه. یعنی نباید آرایه را به اینصورت مقداردهی کنیم:
arr[0] = 'bird'
یا از متد های ()push
یا ()pop
استفاده کنیم. هر بار که لازم هست آرایه را آپدیت کنیم، باید یک آرایه جدید ایجاد کنیم و از طریق تابع setter
استیت مقدار جدید آرایه را آپدیت کنیم.
این جدول خیلی بهتون کمک میکنه:
-- | avoid (mutates the array) | prefer (returns a new array) |
---|---|---|
adding | push , unshift | concat , [...arr] spread syntax (example) |
removing | pop , shift , splice | filter , slice (example) |
replacing | splice , arr[i] = ... assignment | map (example) |
sorting | reverse , sort | copy the array first (example) |
خوب بچه ها امیدوارم این قسمت براتون مفید بوده باشه. خوشحال میشم اگه انتقادی یا پیشنهادی دارید برام بنویسید تا محتوای بهتری تولید بشه ❤️
منابع:
این پست برات مفید بود؟
0
0
0
0
نظرات