Kodokon kodokon.com

تحت الغطاء: التوفيق، التجميع، والعرض المتزامن

اكتشف كيف يوفّق Fiber بين الشجرة، ويجمّع التحديثات، ويجعل عمل العرض قابلاً للمقاطعة.

11 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

منذ React 16، صار المحرّك الداخلي يُدعى Fiber: كل عنصر في شجرتك يُمثَّل بعقدة fiber، وهي وحدة عمل. في الواقع، يحتفظ React بـ شجرتين (تقنية التخزين المؤقّت المزدوج): الشجرة current التي تعكس ما هو معروض على الشاشة، والشجرة workInProgress التي تُبنى أثناء العرض. يجري العمل في مرحلتين متمايزتين: مرحلة العرض (استدعاء مكوّناتك، حساب الفروقات)، وهي قابلة للمقاطعة والإعادة، ثم مرحلة الإيداع (تطبيق التعديلات على الـ DOM، تشغيل التأثيرات)، وهي دائماً متزامنة وغير قابلة للمقاطعة. يعتمد التوفيق على استدلالين يخفّضان تكلفة المقارنة إلى تكلفة خطّية: إذا تغيّر نوع عنصر في موضع معيّن، يدمّر React الشجرة الفرعية بأكملها ويعيد بناءها، بما في ذلك الحالة؛ وبالنسبة إلى القوائم، تتيح خاصية key لـ React مطابقة العناصر بين عرضين.

JSX
function List({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <input defaultValue={item.label} />
        </li>
      ))}
    </ul>
  );
}
// With key={index}, deleting the first item
// would shift the content of the remaining inputs.
مفتاح ثابت يحافظ على حالة كل صفّ وعلى الـ DOM الخاص به.

الآلية الأساسية الثانية: التجميع. حين تُطلَق عدّة تحديثات للحالة ضمن السياق نفسه، يجمعها React في عرض واحد. قبل React 18، كان هذا التجميع يعمل فقط داخل معالِجات أحداث React؛ ومنذ createRoot، صار تلقائياً في كل مكان: المؤقّتات، الوعود، المستمعون الأصليون. ولهذا أيضاً تُرجِع قراءة الحالة مباشرةً بعد setState القيمة القديمة: فالتحديث مُجدوَل، لا مُطبَّق.

JSX
function handleTick() {
  setCount((c) => c + 1);
  setFlag((f) => !f);
  // React 18: a single render, even here.
}

setTimeout(handleTick, 100);
// Before React 18, this timer would trigger
// two successive renders.
التجميع التلقائي يشمل الآن كل سياق.

العرض المتزامن يدفع المنطق أبعد: يمكن مقاطعة مرحلة العرض أو استئنافها أو حتى التخلّي عنها وإعادة تشغيلها إذا ورد تحديث أكثر إلحاحاً. يرتّب React التحديثات حسب الأولوية (الـ lanes): ضغطة مفتاح ملحّة، أمّا تصفية قائمة موسومة بـ startTransition فليست كذلك. النتيجة المباشرة: يجب أن تكون دوال مكوّناتك نقيّة، لأن React قد يستدعيها عدّة مرّات من دون أن يُودِع النتيجة أبداً. حالة حدّية أخرى: مخزن خارجي (Redux، Zustand، مفردة محلية الصنع) قُرِئ أثناء عرض قابل للمقاطعة يمكن أن يتغيّر في منتصف العرض، منتِجاً واجهة غير متّسقة تُعرَف بالـ tearing (التمزّق). واجهة useSyncExternalStore تحلّ هذه المشكلة بإجبار عرض متزامن عند تغيّر المخزن.

JAVASCRIPT
import { useSyncExternalStore } from 'react';

function useWindowWidth() {
  return useSyncExternalStore(
    (notify) => {
      window.addEventListener('resize', notify);
      return () =>
        window.removeEventListener('resize', notify);
    },
    () => window.innerWidth
  );
}
قراءة مخزن خارجي من دون خطر التمزّق.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. أثناء التوفيق، ماذا يفعل React إذا صار عنصر <div> عنصراً <span> في الموضع نفسه؟
    • يحدّث الوسم مع الإبقاء على الأبناء
    • يدمّر الشجرة الفرعية بأكملها، بما في ذلك الحالة، ويعيد بناءها
    • يقارن الأبناء بشكل تعاودي قبل اتخاذ القرار
    • يُطلق خطأ توفيق
  2. مع React 18 وcreateRoot، كم عدد عمليات العرض التي يُطلقها استدعاءان لـ setState داخل setTimeout؟
    • اثنتان، لأن التجميع يُطبَّق فقط على أحداث React
    • واحدة فقط، بفضل التجميع التلقائي
    • صفر، إذ تُتجاهَل التحديثات خارج الأحداث
  3. أيّ مرحلة من عمل React قابلة للمقاطعة في الوضع المتزامن؟
    • مرحلة الإيداع فقط
    • مرحلة العرض فقط
    • كلتا المرحلتين
    • لا شيء، إذ يبقى React متزامناً بالكامل