به روز رسانی Monaco شکست Fastly Fiddle: در اینجا نحوه حل آن با useCallback در React است.
من و همکارم دورا اخیراً وابستگی های Fastly Fiddle را به روز کردیم و ناگهان متوجه شدیم که ورودی کاربر در ویرایشگر کدمان نامنظم و غیرقابل استفاده کند است. ما آن را با انتقال برخی از حالت ها از مؤلفه به یک متغیر محلی، که به لطف useCallback در سراسر رندرها ادامه داشت، رفع کردیم.
این چیزی است که در UI دیدیم:
با باز کردن پانل شبکه، میتوانیم درخواستی به سرور برای هر ضربه زدن، 1 ثانیه پس از فشار دادن کلید مشاهده کنیم:
آخ. انحراف ما به وضوح شکسته شده است – و نه تنها این، بلکه همچنین قرار است درخواستهای ذخیرهسازی را که در حال انجام هستند، در صورت انجام یک ضربه کلید جدید، لغو کنیم، و به وضوح کار نمیکند.
به نظر می رسد که نتیجه این است، اگر حالت را در تابعی که به خاطر سپرده شده است بخوانید، نسخه قدیمی آن حالت را دریافت خواهید کرد.
در مورد ما، موناکو، مؤلفه ویرایشگر کد، یک تابع را حفظ می کرد، یک نسخه قدیمی از آن را فراخوانی می کرد، و در نتیجه ما هرگز نتوانستیم بگوییم که یک عملیات ذخیره از قبل در حال انجام است، بنابراین ما عملکرد موجود را لغو نکردیم. عملیات، و راه اندازی یک جدید.
کد مشکل ساز
به نظر می رسد بسیاری از مشکلات عملکرد و اشکالات رفتاری در برنامه های React ناشی از درک بیش از حد مبهم از آنچه باعث می شود یک کامپوننت دوباره رندر شود، چه اتفاقی می افتد زمانی که یک کامپوننت دوباره رندر می شود و در چه زمینه ای یک تابع فراخوانی می شود، نشات می گیرد. خوشبختانه باهاش دوستم ایوان آکولوف که اساساً عملکرد وب به شکل انسانی است، و او به من کمک کرد تا این را بفهمم.
در Fastly Fiddle یک متغیر حالت داریم saveOperation
که توسط الف خوانده و به روز می شود saveHandler
تابع، و همچنین به اجزای فرزند منتقل می شود:
const [saveOperation, setSaveOperation] = useState();
const saveHandler = async (userInput) => {
if (saveOperation) saveOperation.abort();
const debounceDelay = abortableWait(DEBOUNCE_DELAY);
setSaveOperation(debounceDelay);
await debounceDelay;
...
setSaveOperation(null);
}
render(<Main onSave={saveHandler} />)
ما سعی می کنیم قبل از ذخیره کار کاربر برای یک دوره بیکاری منتظر بمانیم (‘debounce’). هر بار که ضربههای کلید جدیدی را میبینیم، ذخیره معلق – یا هر درخواست API در حین پرواز که از قبل در حال انجام است را لغو میکنیم.
ما این کار را با استفاده از یک الگوی وعدهی قابل سقط انجام میدهیم – احتمالاً یک راه اصطلاحیتر برای انجام این کار در React وجود دارد، اما به نظر میرسد که این روش بسیار خوب کار میکند. saveHandler
هر بار که کامپوننت رندر می شود دوباره تعریف می شود، اما باید خوب باشد – saveOperation
حالت جزء است که در سراسر رندرها باقی خواهد ماند. درست؟
مشکل حفظ کردن
با این حال، یک خطر وجود دارد – اگر چیزی در پایینتر درخت مؤلفه مرجع را به خاطر بسپارد saveHandler
، ممکن است در نهایت یک نسخه قدیمی از تابع را فراخوانی کنیم که نسخه قدیمی آن را ضبط کرده است saveOperation
.
معلوم شد، این دقیقاً همان چیزی است که اتفاق می افتد. پس از بهروزرسانی وابستگی، متوجه شدیم که وقتی ضربههای کلیدی وارد یک مؤلفه ویرایشگر کد میشود که توسط موناکو ساخته میشود، فراخوانی حاصل از saveHandler
یافت saveOperation
بودن undefined
هر زمان.
را <Main>
جزء در نهایت عبور می کند saveHandler
عملکرد موناکو به این صورت است:
<MonacoEditor
theme="fiddle"
key={props.codeContext}
options={monacoOptions}
value={props.value}
onChange={props.saveHandler}
/>
نسخه جدید موناکو سپس آن را به خاطر میسپارد onChange
callback، به این معنی که وقتی کلیدها وارد ویرایشگر می شوند، saveHandler
که اخراج می شود یکی است که آخرین ارزش را ندارد saveOperation
، بنابراین نمی دانیم ذخیره ای در حال انجام است، بنابراین آن را لغو نمی کنیم و همه جهنم ها از بین می روند.
وضعیت پیشرفت یک ضدالگو است؟
پس راه حل چیست؟ من می توانم بررسی کنم که چرا موناکو در حال حفظ کردن کنترلر است و شاید سعی کنم آن را متقاعد کنم که این کار را انجام ندهد، اما این مشکل نشان می دهد که استفاده از useState
برای ذخیره کردن saveOperation
شاید در وهله اول بهترین ایده نباشد. جدای از وابسته کردن تابع به داده در زمینه والد، استفاده از حالت به معنای ما نیز هست هر بار که کامپوننت را به روز می کنیم، آن را دوباره رندر می کنیم. از آنجا که saveOperation
بر خروجی رندر مؤلفه تأثیر نمی گذارد، به نظر درست نیست.
من تصمیم گرفته ام که این نوع داده را “وضعیت پیشرفت” بنامم، زیرا به طور موثر داده ها را از یک فراخوانی یک تابع به دیگری منتقل می کند تا به فراخوانی بعدی اجازه دهد رفتار خود را بر اساس دانستن اینکه در کجای قبلی را ترک کرده ایم، تنظیم کند. یکی
useState
واضح ترین راه برای انجام این کار است، اما راه خوبی نیست. روش React-y برای انجام این کار در واقع این است useRef
:
راه حل 1: useRef
const saveOperation = useRef(null);
const saveHandler = async (userInput) => {
if (saveOperation.current) saveOperation.current.abort();
const debounceDelay = abortableWait(DEBOUNCE_DELAY);
saveOperation.current = debounceDelay;
await debounceDelay;
...
saveOperation.current = null;
}
در حال بروز رسانی saveOperation
اکنون رندر را راهاندازی نمیکند، و این حتی اگر کار کند saveHandler
حفظ می شود
دلیل اینکه این کار می کند همین دلیل است useRef
متغیرها همیشه چیز عجیبی دارند .current
چیزی از آنها آویزان است. متغیر ایجاد شده توسط useRef
تغییرناپذیر است، بنابراین ثبت آن در یک تابع خوب است، زیرا می دانیم که نمی توان آن را دوباره تخصیص داد: مقدار saveOperation
(که یک شی است) هرگز تغییر نخواهد کرد. با این حال خواص از آن شی می توان تغییر دهید، و تابعی که مرجعی را به شیء ثبت کرده است، آن ویژگی های به روز شده را می بیند. به همین دلیل است که داورها یک .current
ویژگی.
راه حل 2: استفاده از بسته شدن برگشت به تماس
واکنش نشان دهید useCallback
hook یک ارجاع به یک تابع ایجاد می کند و سپس از آن در رندرهای بعدی کامپوننت استفاده مجدد می کند، نه اینکه هر بار تابع را دوباره تعریف کند. ما متوجه شدیم، شما میتوانید از این با یک عبارت تابع فراخوانی فوری (IIFE) برای گرفتن مقداری که بین رندرها باقی میماند استفاده کنید:
const saveHandler = useCallback((() => {
let saveOperation;
return async (userInput) => {
if (saveOperation) saveOperation.abort();
saveOperation = abortableWait(DEBOUNCE_DELAY);
await saveOperation;
...
saveOperation = null;
};
})(), []);
اکنون، هنگامی که کامپوننت برای اولین بار رندر می شود، React IIFE را اجرا می کند و تابع حاصل را به saveHandler
. تابع برگشتی به saveOperation
که در یک بسته ایجاد شده توسط IIFE به دام افتاده است.
من این راه حل را دوست دارم زیرا:
- به جای تکیه بر چارچوب برای حل مشکل، بیشتر به اصول جاوا اسکریپت متکی است
- دامنه محدودتری برای داده های من ایجاد می کند (
saveOperation
خارج از دسترس نیستsaveHandler
) - این ساده ترین استفاده از آن است
saveOperation
– بدون نیاز به استفاده از تابع تنظیم کننده و بدون نیاز به استفاده از a.current
ویژگی.
با این حال، اسناد React در useCallback
این هشدار را داشته باشید:
در آینده، React ممکن است ویژگیهای بیشتری را اضافه کند که از دور انداختن حافظه نهان استفاده میکنند – برای مثال، اگر React در آینده پشتیبانی داخلی برای لیستهای مجازی اضافه کند، منطقی است که حافظه پنهان را برای مواردی که از آن خارج میشوند دور بریزید. نمای جدول مجازی شده اگر به useCallback به عنوان بهینهسازی عملکرد تکیه میکنید، این باید با انتظارات شما مطابقت داشته باشد.
بنابراین در آینده ممکن است پاسخ تماس من بیشتر تعریف شود… اما برای موارد استفاده ای مانند debouncing، به نظر می رسد که هنوز هم در بیشتر مواقع خوب باشد.
نتیجه
در این تمرین رفع اشکال چند درس خوب وجود دارد: استفاده بیش از حد نکنید useState
. دوست داشتن را یاد بگیر useRef
. و خوب است که React راه حلی برای این دارد و که ما همچنین می توانیم آن را با استفاده از زبان های اولیه JS حل کنیم.
خوب، ادامه بده و من را به خاطر اشتباه کردن React تحریک کن. من اهمیتی نمی دهم، React به هر حال عجیب است.