صف کردن چندین بروزرسانی وضعیت در React

تنظیم یک متغیر وضعیت، رندری دیگر را در صف قرار میدهد. اما گاهی ممکن است قبل از اینکه بخواهید رندر بعدی را در صف قرار دهید، عملیاتهای بر روی مقدار فعلی انجام دهید. برای انجام این کار، درک اینکه React چگونه بهروزرسانیهای وضعیت را دستهبندی میکند، به شما کمک میکند.
React بهروزرسانیهای وضعیت را دستهبندی میکند
ممکن است انتظار داشته باشید که کلیک بر روی دکمه "+3" شمارشگر را سه بار افزایش دهد زیرا که این دکمه setNumber(number + 1)
را سه بار فراخوانی میکند:
مشاهده کد و اجرای آن در CodeSandbox
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
با این حال، همانطور که ممکن است از بخش قبلی به خاطر داشته باشید، مقادیر وضعیت هر رندر ثابت هستند، مقدار number
در داخل تابع رویداد اولین رندر همیشه 0
است، صرفنظر از اینکه چند بار setNumber(1)
را فراخوانی میکنید، همان صفر باقی می ماند:
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
اما یک عامل دیگر نیز وجود دارد. React قبل از اینکه بهروزرسانیهای وضعیت را پردازش کند، منتظر اجرای به همین دلیل است که رندر مجدد فقط پس از فراخوانی همه setNumber()
ها اتفاق میافتد.
ممکن است به یاد پیشخدمتی که در رستوران سفارش میگیرد، بیفتید. پیشخدمت به محض دریافت اولین سفارش به آشپزخانه نمیرود! بلکه او به شما اجازه میدهد تا سفارش خود را کامل کنید، تغییراتی در آن ایجاد کنید و حتی از دیگران در میز سفارش بگیرد.

این به شما این امکان را میدهد که چندین متغیر وضعیت را، حتی از چندین کامپوننت، بدون ایجاد رندر مجدد بیش از حد، بهروزرسانی کنید. اما این به این معنی است که UI تا بعد از تمام شدن تابع رویداد شما، و هر کدی که در آن وجود دارد، بهروزرسانی نخواهد شد. این رفتار که به عنوان دستهبندی (Batching) نیز شناخته میشود، باعث میشود که برنامه React شما بسیار سریعتر اجرا شود. همچنین از روبهرو شدن با رندرهای "نیمهتمام" گیجکننده که فقط برخی از متغیرها بهروزرسانی شدهاند، جلوگیری میکند.
React بین -- هر کلیک بهطور جداگانه مدیریت میشود. React فقط زمانی دستهبندی میکند که دسته بندی، ایمن باشد. این اطمینان میدهد که به عنوان مثال، اگر در کلیک اول، دکمه یک فرم را غیرفعال کند، کلیک دوم آن باعث ارسال دوباره اطلاعات نخواهد نشد.
بهروزرسانی چندین باره یک وضعیت قبل از رندر بعدی
این یک مورد غیرمعمول است، اما اگر بخواهید قبل از رندر بعدی یک متغیر وضعیت را چندین بار بهروزرسانی کنید، به جای اینکه مقدار وضعیت بعدی مانند setNumber(number + 1)
را ارسال کنید، میتوانید یک تابع بفرستید که وضعیت بعدی را بر اساس وضعیت قبلی در صف محاسبه کند، مانند setNumber(n => n + 1)
. این یک راه برای مشخص کردن "کاریست که React باید با مقدار وضعیت انجام دهد"، به جای اینکه فقط آن را جایگزین کند.
اکنون سعی کنید شمارشگر را افزایش دهید:
مشاهده و اجرای کد در CodeSandbox
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
</>
)
}
در اینجا، n => n + 1
به عنوان تابع بهروزرسانی شناخته میشود. وقتی شما آن را به یک تنظیمکننده وضعیت ارسال میکنید:
- React این تابع را به صف اضافه میکند تا پس از اجرای تمام دیگر کدها در تابع رویداد پردازش شود.
- در رندر بعدی، React از به سراغ صف میرود و وضعیت بهروزرسانی نهایی را به شما میدهد.
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
این گونه است که React این خطوط کد را در حین اجرای تابع رویداد پردازش میکند:
setNumber(n => n + 1)
:n => n + 1
یک تابع است. React آن را به صف اضافه میکند.setNumber(n => n + 1)
:n => n + 1
یک تابع است. React آن را به صف اضافه میکند.setNumber(n => n + 1)
:n => n + 1
یک تابع است. React آن را به صف اضافه میکند.
وقتی شما useState
را در رندر بعدی فراخوانی میکنید، React از به سراغ صف میرود. وضعیت قبلی number
برابر با 0
بود، بنابراین React این را به عنوان آرگومان n
به اولین تابع بهروزرسانی میدهد. سپس React مقدار بازگشتی تابع بهروزرسانی قبلی شما را میگیرد و به تابع بهروزرسانی بعدی بهعنوان n
منتقل میکند و همینطور ادامه میدهد:
بهروزرسانی در صف | n | چیزی که برمیگرداند |
---|---|---|
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 1 = 3 |
ری اکت مقدار 3
را به عنوان نتیجه نهایی ذخیره میکند و از useState
باز میگرداند.
به این دلیل است که کلیک بر روی دکمه "+3" در مثال بالا، وضعیت را سه واحد افزایش میدهد.
اگر پس از جایگزینی وضعیت آن را بهروزرسانی کنید چه اتفاقی می افتد؟
در تابع رویداد زیر چه اتفاقی می افتد؟ فکر میکنید مقدار number
در رندر بعدی چه خواهد بود؟
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
مشاهده کد و نتیجه اجرای آن در CodeSandbox
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>Increase the number</button>
</>
)
}
کاری که تابع رویداد می کند به شرح زیر است:
setNumber(number + 5)
: مقدارnumber
برابر0
است، بنابراینsetNumber(0 + 5)
اجرا میشود. React "جایگزینی با را به صف خود اضافه میکند.setNumber(n => n + 1)
: دستورn => n + 1
یک تابع بهروزرسانی است. React آن تابع را به صف خود اضافه میکند.
در رندر بعدی، React به سراغ صف وضعیت میرود:
بهروزرسانی در صف | n | برمیگرداند |
---|---|---|
"جایگزین با 5 " | 0 (استفاده نشده) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
ری اکت مقدار 6
را به عنوان نتیجه نهایی ذخیره میکند و از useState
باز میگرداند.
شما ممکن است متوجه شده باشید که setState(5)
واقعاً مانند setState(n => 5)
عمل میکند، اما n
استفاده نشده است!
اگر وضعیت را پس از بهروزرسانی آن جایگزین کنید چه اتفاقی میافتد؟
اجازه دهید یک مثال دیگر را امتحان کنیم. حدس بزنید مقدار number
در رندر بعدی چه خواهد بود؟
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
مشاهده کد و نتیجه اجرای آن در CodeSandbox
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>Increase the number</button>
</>
)
}
ری اکت کدهای بالا را در حین اجرای تابع رویداد به صورت زیر پردازش میکند:
setNumber(number + 5)
: مقدارnumber
برابر0
است، بنابراینsetNumber(0 + 5)
اجرا میشود. React "جایگزینی با را به صف خود اضافه میکند.setNumber(n => n + 1)
: دستورn => n + 1
یک تابع بهروزرسانی است. React آن تابع را به صف خود اضافه میکند.setNumber(42)
: React "جایگزینی با را به صف خود اضافه میکند.
در رندر بعدی، React به سراغ صف وضعیت میرود:
بهروزرسانی در صف | n | برمیگرداند |
---|---|---|
"جایگزینی با 5 " | 0 (استفاده نشده) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
"جایگزینی با 42 " | 6 (استفاده نشده) | 42 |
پس React 42
را به عنوان نتیجه نهایی ذخیره میکند و از useState
باز میگرداند.
برای خلاصه کردن، این گونه میتوانید کار setNumber
را در نظر بگیرید:
- یک تابع بهروزرسانی (برای مثال
n => n + 1
) به صف اضافه میشود. - هر مقدار دیگری (برای مثال، عدد
5
) "جایگزین با را به صف اضافه میکند، در حالی که موارد موجود در صف را نادیده میگیرد.
پس از اتمام تابع رویداد، React یک رندر مجدد را فعال میکند. در حین رندر مجدد، React صف را پردازش میکند. توابع بهروزرسانی در حین رندر اجرا میشوند، بنابراین توابع بهروزرسانی باید باشند و فقط نتیجه را بازگردانی کنند. سعی نکنید حالت را داخل آنها تنظیم کنید یا دیگر اثرات جانبی را اجرا کنید. در حالت سختگیرانه، React هر تابع بهروزرسانی را دو بار اجرا میکند (اما نتیجه دوم را نادیده میگیرد) تا به شما کمک کند تا اشتباهات را پیدا کنید.
روش نامگذاری متغیرها
نام گذاری آرگومان تابع بهروزرسانی، قاعدتا براساس حروف اول متغیر وضعیت مربوطه انجام میشود:
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
اگر نام گذاری واضح تر را ترجیح می دهید، راه رایج دیگر استفاده از نام کامل متغیر وضعیت است، مانند setEnabled(enabled => !enabled)
، یا استفاده از پیشوند مانند setEnabled(prevEnabled => !prevEnabled)
.
جمع بندی
- تنظیم مقدار متغیر وضعیت، مقدار وضعیت در رندر موجود را تغییر نمیدهد، بلکه درخواست رندر جدیدی ایجاد میکند.
- React بهروزرسانیهای وضعیت را پس از اتمام اجرای توابع رویداد پردازش میکند. به این کار دستهبندی (Batching) می گویند.
- برای بهروزرسانی چندین باره وضعیتها در یک رویداد، میتوانید از تابع بهروزرسانی
setNumber(n => n + 1)
استفاده کنید.
چالش ها
1. اصلاح شمارنده درخواستها
شما روی یک برنامه بازار هنری کار میکنید که به کاربر اجازه میدهد چندین سفارش برای یک کالای هنری را همزمان ارسال کند. هر بار که کاربر دکمه "خرید" را فشار میدهد، شمارنده "در حال انتظار" باید یک واحد افزایش یابد. پس از سه ثانیه، شمارنده "در حال انتظار" باید کاهش یابد و شمارنده "تکمیل شده" باید افزایش یابد.
اما شمارنده "در حال انتظار" به صورت مورد نظر عمل نمیکند. زمانی که شما "خرید" را فشار میدهید، به -1
کاهش مییابد (که نباید امکانپذیر باشد!). و اگر شما به سرعت دو بار کلیک کنید، هر دو شمارنده به نظر میرسد که به صورت غیرقابل پیشبینی عمل میکنند.
چرا این اتفاق میافتد؟ هر دو شمارنده را اصلاح کنید.
مشاهده و اصلاح کد در CodeSandbox
import { useState } from 'react';
export default function RequestTracker() {
const [pending, setPending] = useState(0);
const [completed, setCompleted] = useState(0);
async function handleClick() {
setPending(pending + 1);
await delay(3000);
setPending(pending - 1);
setCompleted(completed + 1);
}
return (
<>
<h3>
Pending: {pending}
</h3>
<h3>
Completed: {completed}
</h3>
<button onClick={handleClick}>
Buy
</button>
</>
);
}
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
2. پیادهسازی صف وضعیت خودتان
در این چالش، شما یک قسمت کوچک از React را از ابتدا پیادهسازی خواهید کرد! این چالش به اندازهای که به نظر میرسد سخت نیست.
در پیشنمایش sandbox اسکرول کنید. متوجه خواهید شد که چهار مورد آزمایشی نشان داده شده می شود که به مثالهایی که قبلاً در این صفحه دیدهاید مربوط میشوند. وظیفه شما این است که تابع getFinalState
را پیادهسازی کنید تا نتیجه صحیح را برای هر یک از این موارد برگرداند. اگر آن را به درستی پیادهسازی کنید، تمام چهار تست باید موفق شوند.
شما دو آرگومان خواهید داشت: baseState
وضعیت اولیه (مانند 0
)، و queue
یک آرایه است که شامل ترکیبی از اعداد (مانند 5
) و توابع بهروزرسانی (مانند n => n + 1
) به ترتیب اضافهشده است.
وظیفه شما این است که وضعیت نهایی را برگردانید، درست مانند جداولی که در این صفحه نشان داده شدهاند!
مشاهده و حل این چالش در CodeSandbox
import { getFinalState } from './processQueue.js';
function increment(n) {
return n + 1;
}
increment.toString = () => 'n => n+1';
export default function App() {
return (
<>
<TestCase
baseState={0}
queue={[1, 1, 1]}
expected={1}
/>
<hr />
<TestCase
baseState={0}
queue={[
increment,
increment,
increment
]}
expected={3}
/>
<hr />
<TestCase
baseState={0}
queue={[
5,
increment,
]}
expected={6}
/>
<hr />
<TestCase
baseState={0}
queue={[
5,
increment,
42,
]}
expected={42}
/>
</>
);
}
function TestCase({
baseState,
queue,
expected
}) {
const actual = getFinalState(baseState, queue);
return (
<>
<p>Base state: <b>{baseState}</b></p>
<p>Queue: <b>[{queue.join(', ')}]</b></p>
<p>Expected result: <b>{expected}</b></p>
<p style={{
color: actual === expected ?
'green' :
'red'
}}>
Your result: <b>{actual}</b>
{' '}
({actual === expected ?
'correct' :
'wrong'
})
</p>
</>
);
}
اگر احساس میکنید که به بنبست رسیدهاید، با این ساختار کد شروع کنید:
export function getFinalState(baseState, queue) {
let finalState = baseState;
for (let update of queue) {
if (typeof update === 'function') {
// TODO: apply the updater function
} else {
// TODO: replace the state
}
}
return finalState;
}
خطوط گمشده را پر کنید!
جواب چالش ها
چالش اول
داخل تابع رویداد handleClick
، مقادیر pending
و completed
مطابق با آنچه که در زمان رویداد کلیک بودهاند هستند. برای اولین رندر، pending
برابر 0
بود، بنابراین setPending(pending - 1)
به setPending(-1)
تبدیل میشود، که اشتباه است. از آنجا که شما میخواهید شمارندهها را افزایش یا کاهش دهید، به جای تنظیم آنها به یک مقدار مشخص که در زمان کلیک تعیین شده است، میتوانید توابع بهروزرسانی را ارسال کنید:
import { useState } from 'react';
export default function RequestTracker() {
const [pending, setPending] = useState(0);
const [completed, setCompleted] = useState(0);
async function handleClick() {
setPending(p => p + 1);
await delay(3000);
setPending(p => p - 1);
setCompleted(c => c + 1);
}
return (
<>
<h3>
Pending: {pending}
</h3>
<h3>
Completed: {completed}
</h3>
<button onClick={handleClick}>
Buy
</button>
</>
);
}
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
این اطمینان میدهد که وقتی شمارندهای را افزایش یا کاهش میدهید، این کار را نسبت به جدیدترین وضعیت آن انجام میدهید به جای اینکه وضعیت در زمان کلیک چه بوده است.
چالش دوم
این دقیقاً الگوریتمی است که React برای محاسبه وضعیت نهایی استفاده میکند:
مشاهده کد و نتیجه اجرا در CodeSandbox
export function getFinalState(baseState, queue) {
let finalState = baseState;
for (let update of queue) {
if (typeof update === 'function') {
// Apply the updater function.
finalState = update(finalState);
} else {
// Replace the next state.
finalState = update;
}
}
return finalState;
}
حال شما میدانید که این بخش از React چگونه کار میکند!