برنامه نویسی

به روز رسانی 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 هر زمان.

saveOperation همیشه تعریف نشده است

را <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 به دام افتاده است.

من این راه حل را دوست دارم زیرا:

  1. به جای تکیه بر چارچوب برای حل مشکل، بیشتر به اصول جاوا اسکریپت متکی است
  2. دامنه محدودتری برای داده های من ایجاد می کند (saveOperation خارج از دسترس نیست saveHandler)
  3. این ساده ترین استفاده از آن است saveOperation – بدون نیاز به استفاده از تابع تنظیم کننده و بدون نیاز به استفاده از a .current ویژگی.

با این حال، اسناد React در useCallback این هشدار را داشته باشید:

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

بنابراین در آینده ممکن است پاسخ تماس من بیشتر تعریف شود… اما برای موارد استفاده ای مانند debouncing، به نظر می رسد که هنوز هم در بیشتر مواقع خوب باشد.

نتیجه

در این تمرین رفع اشکال چند درس خوب وجود دارد: استفاده بیش از حد نکنید useState. دوست داشتن را یاد بگیر useRef. و خوب است که React راه حلی برای این دارد و که ما همچنین می توانیم آن را با استفاده از زبان های اولیه JS حل کنیم.

خوب، ادامه بده و من را به خاطر اشتباه کردن React تحریک کن. من اهمیتی نمی دهم، React به هر حال عجیب است.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا