المطابقة (Reconciliation) في React

من موسوعة حسوب

تُزوّدنا React بواجهة برمجة تطبيقات (API) صريحة بحيث لا نقلق بشأن التغييرات التي تطرأ في كل تحديث. يجعل هذا من كتابة التطبيقات أمرًا أسهل بكثير، ولكن قد لا يكون من الواضح كثيرًا كيفيّة تطبيق هذا في React. تشرح هذه الصفحة الخيارات التي وضعناها في خوارزمية المقارنة (diffing) بحيث تكون تحديثات المُكوّنات متوقعة وفي نفس الوقت سريعة كفاية لأجل التطبيقات عالية الأداء.

البداية

عندما تستخدم React في نقطة زمنية محدّدة بإمكانك التفكير في التابع render()‎ كأنّه يُنشِئ شجرة من عناصر React، وعند التحديث التالي للخاصيّات props أو الحالة state سيُعيد التابع render()‎ شجرة مختلفة من عناصر React. تحتاج بعدها React لأن تعرف كيف ستُحدِّث واجهة المستخدم بكفاءة لُتطابِق آخر تحديث للشجرة.

هنالك بعض الحلول العامة لهذه المشكلة الحسابية لتوليد أقل عدد من العمليات المطلوبة للتحويل من شجرة إلى أخرى. على أية حال تمتلك حالة الخوارزميات تعقيدًا من الترتيب O(n^3)‎ حيث n هو عدد العناصر الموجودة في الشجرة.

إن استخدمنا هذا في React فسيتطلّب عرض 1000 عنصر من الأس (1^) بليون مقارنة، وهذا مُكلِف جدًّا. تُنفِّذ React بدلًا من ذلك خوارزمية إرشاديّة O(n)‎ بناءً على افتراضين هما:

  1. سيُنتِج العنصران من نوعين مختلفين أشجار مختلفة.
  2. يُمكِن للمُطوّر أن يُلمِّح للعناصر الأبناء التي قد تكون مستقرة خلال تصييرات مختلفة عن طريق خاصيّة مفتاح (key) للإشارة إليها.

عمليًّا تكون هذه الافتراضات صحيحة تقريبًا لكل حالات الاستخدام العمليّة.

خوارزميّة المقارنة

عند مقارنة شجرتين تُقارِن React في البداية بين العنصرين الجذريين لهما. يختلف هذا السلوك اعتمادًا على أنواع العناصر الجذريّة.

العناصر من أنواع مختلفة

عندما يكون للعناصر الجذرية أنواع مختلفة تُجزِّء React الشجرة القديمة وتبني شجرة جديدة من الصفر، مُنطلِقةً من العنصر <a> إلى <img>، أو من العنصر <Article> إلى <Comment>، أو من العنصر <Button> إلى <div>، تُؤدّي أي من هذه العناصر إلى إعادة البناء بشكلٍ كامل.

عند تجزئة الشجرة تُدمَّر عُقَد DOM وتستقبل نُسَخ المُكوّنات التابع componentWillUnmount()‎. وعند بناء شجرة جديدة تُدخَل عُقَد DOM الجديدة ضمن DOM وتستقبل نُسَخ المُكوّنات التابع componentWillMount()‎ ثمّ التابع componentDidMount()‎، ونفقد أي حالة مرتبطة بالشجرة القديمة.

تتعرّض المُكوِّنات الموجودة تحت العنصر الجذري للفصل (unmount) وتدمير حالتها. على سبيل المثال عند إجراء خوارزمية المقارنة على الشيفرة التالية:

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

ستُدمِّر المُكوّن Counter القديم وتُعيد إنشاء واحد جديد.

عناصر DOM من نفس النوع

عند مقارنة عنصري DOM من نفس النوع، تبحث React في خاصيّاتهما وتبقي على نفس عُقدة DOM التحتية مع تحديث الخاصيّات المتغيّرة فقط، على سبيل المثال:

<div className="before" title="stuff" />

<div className="after" title="stuff" />

عن طريق مقارنة هذين العنصرين تعرف React أنّها يجب أن تُعدِّل فقط الخاصيّة className في عقدة DOM. عند تحديث الخاصيّة style تعرف React أنّها يجب أن تُحدِّث فقط الخاصيّات التي تغيّرت، على سبيل المثال:

<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />

عند التحويل بين هذين العنصرين تعرف React أنّها يجب أن تُعدِّل التنسيق color وليس fontWeight.

بعد التعامل مع عقدة DOM تُكرِّر React نفس العمليّة للعناصر الأبناء.

عناصر المكونات من نفس النوع

عند تحديث المُكوّن تبقى نسخة المُكوّن على حالها من أجل الاحتفاظ بالحالة عبر التصييرات التالية. تُحدِّث React الخاصيّات props لنسخة المُكوّن لتُطابِق العنصر الجديد وتستدعي التوابع componentWillReceiveProps()‎ و componentWillUpdate()‎ في النسخة.

يُستدعى بعد ذلك التابع render()‎ وتتكرر خوارزمية المقارنة على النتيجة السابقة والنتيجة الجديدة.

التكرار على العناصر الأبناء

عند حدوث التكرار (Recursing) على العناصر الأبناء لعقدة DOM، تمر React افتراضيًّا عبر قائمتين للعناصر الأبناء بنفس الوقت وتُولِّد تغييرًا عندما تجد أي فرق.

على سبيل المثال عند إضافة عنصر في نهاية العناصر الأبناء يعمل التحويل بين هاتين الشجرتين بشكلٍ جيّد:

<ul>
  <li>العنصر الأول</li>
  <li>العنصر الثاني</li>
</ul>

<ul>
  <li>العنصر الأول</li>
  <li>العنصر الثاني</li>
  <li>العنصر الثالث</li>
</ul>

ستُطابِق React بين الشجرتين <li>العنصر الأول</li>، ثم بين الشجرتين <li>العنصر الثاني</li>، وبعدها تُدخِل الشجرة   <li>العنصر الثالث</li>. إن طبّقت ذلك بشكلٍ ساذج فسيمتلك إدخال عنصر في البداية أداءً أسوأ. على سبيل المثال يعمل التحويل بين هاتين الشجرتين بشكلٍ سيّء:

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

ستُبدِّل React كل عنصر ابن بدلًا من إدراكها إمكانيّة إبقاء الشجرتين الفرعيتين <li>Duke</li> و <li>Villanova</li> دون تغيير. قد تكون قلة الكفاءة هذه مشكلة هامة.

المفاتيح

لحل هذه المشكلة تدعم React خاصيّة المفتاح key. عندما يكون للعناصر الأبناء مفاتيح فستستخدم React المفتاح لمطابقة العناصر الأبناء في الشجرة الأصلية مع العناصر الأبناء في الشجرة التالية. على سبيل المثال تُؤدّي إضافة مفتاح key للمثال السابق الذي لا يعمل بكفاءة إلى جعل عملية تحويل الشجرة تعمل بكفاءة:

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

تعرف الآن React أنّ العنصر الذي يمتلك المفتاح 2014 هو الجديد، والعناصر التي لديها المفاتيح 2015 و 2016 قد انتقلت فقط. لا يكون إيجاد مفتاح أمرًا صعبًا في الممارسة العمليّة. قد يكون للعنصر الذي تريد عرضه مُعرِّف (ID) مُسبقًا، لذا قد يأتي المفتاح من بياناتك فقط:

<li key={item.id}>{item.name}</li>

عندما لا يكون هذا هو الحال بإمكانك إضافة خاصيّة للمُعرِّف (ID) جديدة إلى نموذج بياناتك أو إجراء hash على بعض المحتوى لديك لتوليد مفتاح. يجب أن يكون المفتاح فريدًا بين العناصر الأشقاء له فقط وليس فريدًا بشكلٍ عام.

وكملجأ أخير بإمكانك تمرير فهرس العنصر في المصفوفة كمفتاح. يعمل هذا بشكلٍ جيّد إن كانت العناصر لا تخضع لإعادة الترتيب إطلاقًا، ولكن ستكون إعادة الترتيب بطيئة.

قد تُسبِّب إعادة الترتيب أيضًا مشاكل مع حالة المُكوِّن عند استخدام الفهارس كمفاتيح. تُحدَّث نُسَخ المُكوِّنات ويُعاد استخدامها بناءً على مفتاحها. إن كان المفتاح عبارة عن فهرس فسيؤدّي نقل العنصر إلى تغييره. وكنتيجة لذلك قد تختلط حالة المُكوّنات بالنسبة لأمور مثل حقول الإدخال غير المضبوطة وتُحدَّث بطرق غير مُتوقّعة.

تجد هنا مثالًا عن المشاكل التي قد تُسبّبها الفهارس كمفاتيح على موقع CodePen. وهنا إصدار مُحدَّث من نفس المثال يُظهِر كيف أنّ عدم استخدام الفهارس كمفاتيح سيُصلِح مشاكل إعادة الترتيب، والترتيب، وإرفاق العناصر.

مفاضلات

من الهام تذكر أنّ خوارزمية المُطابَقة هي تفصيل تنفيذي. قد تُعيد React تصيير كامل التطبيق عند كل حدث وستكون النتيجة النهائية نفسها. ولنكون واضحين تعني إعادة التصيير في هذا السياق استدعاء التابع render لكل المُكوّنات، وليس فصل ووصل هذه المُكوِّنات من البداية. ستُطبِّق فقط الفروقات مع اتباع القواعد المنصوصة في الأقسام السابقة.

نُعيد بشكلٍ دوري تحسين الإرشادات لجعل حالات الاستخدام الشائعة أسرع. حاليًّا تستطيع التعبير عن حقيقة انتقال شجرة فرعيّة من بين العناصر الأشقاء لها، ولكن لا تستطيع القول أنّها انتقلت لمكانٍ آخر، حيث ستُعيد الخوارزمية تصيير كامل الشجرة الفرعيّة.

وبسبب اعتماد React على هذه الإرشادات إن لم تُحقِّق الأغراض المرجوّة من ورائها فسيتأثر الأداء بشكل كبير.

  1. لن تُحاوِل الخوارزمية مطابقة الشجرة الفرعية للمُكوّنات مختلفة الأنواع. فإن وجدتَ نفسك تُبدِّل بين نوعين مُكوِّنين مع الحصول على نتيجة مشابهة فقد ترغب بجعلهما من نفس النوع. لم نجد هذا مشكلة في الممارسة العمليّة.
  2. يجب أن تكون المفاتيح مستقرّة، ومتوقعة، وفريدة. تُؤدّي المفاتيح غير المستقرة (كتلك الناتجة عن التابع Math.random()‎) إلى إعادة إنشاء نُسَخ المُكوّنات وعُقَد DOM بشكلٍ غير ضروري، والذي قد يُسبّب انخفاضًا في الأداء وخسارة الحالة في المُكوّنات الأبناء.

انظر أيضًا

 مصادر