React: RFClarification: لماذا يعتبر "setState" غير متزامن؟

تم إنشاؤها على ١١ نوفمبر ٢٠١٧  ·  31تعليقات  ·  مصدر: facebook/react

لقد حاولت لفترة طويلة فهم سبب عدم تزامن setState . وبعد أن فشلت في العثور على إجابة لها في الماضي ، توصلت إلى استنتاج مفاده أن ذلك كان لأسباب تاريخية وربما من الصعب تغييره الآن. ومع ذلك ، أشار gaearon إلى وجود سبب واضح ، لذلك أشعر بالفضول لمعرفة ذلك :)

على أي حال ، هذه هي الأسباب التي أسمعها كثيرًا ، لكنني أعتقد أنها لا يمكن أن تكون كل شيء لأنه من السهل جدًا مواجهتها

مطلوب setState غير متزامن للعرض غير المتزامن

يعتقد الكثير في البداية أنه بسبب كفاءة العرض. لكنني لا أعتقد أن هذا هو السبب وراء هذا السلوك ، لأن الحفاظ على مزامنة setState مع عرض غير متزامن يبدو تافهاً بالنسبة لي ، شيء على غرار:

Component.prototype.setState = (nextState) => {
  this.state = nextState
  if (!this.renderScheduled)
     setImmediate(this.forceUpdate)
}

في الواقع ، على سبيل المثال ، يسمح mobx-react بالتخصيصات المتزامنة للأشياء التي يمكن ملاحظتها ولا يزال يحترم الطبيعة غير المتزامنة للعرض

هناك حاجة إلى setState غير المتزامن لمعرفة الحالة التي _rendered_

الحجة الأخرى التي أسمعها أحيانًا هي أنك تريد التفكير في الحالة التي تم _ تقديمها _ وليست الحالة _ المطلوبة _. لكنني لست متأكدًا من أن هذا المبدأ له مزايا كبيرة أيضًا. من الناحية المفاهيمية ، يبدو الأمر غريبًا بالنسبة لي. التقديم هو أحد الآثار الجانبية ، فالحالة تدور حول الحقائق. اليوم ، أبلغ من العمر 32 عامًا ، وفي العام المقبل سأبلغ 33 عامًا ، بغض النظر عما إذا كان المكون المالك قادرًا على إعادة التصيير هذا العام أم لا :).

لرسم موازٍ (ربما ليس جيدًا): إذا لم تكن قادرًا على قراءة الإصدار الأخير من مستند Word المكتوب ذاتيًا حتى تقوم بطباعته ، فسيكون ذلك محرجًا جدًا. لا أعتقد ، على سبيل المثال ، أن محركات الألعاب تعطيك ملاحظات حول حالة اللعبة التي تم عرضها بالضبط والإطارات التي تم إسقاطها أيضًا.

ملاحظات مثيرة للاهتمام: في غضون عامين mobx-react لم يسألني أحد مطلقًا السؤال: كيف أعرف أن مراقبي يتم عرضها؟ هذا السؤال يبدو غير مناسب في كثير من الأحيان.

لقد واجهت بعض الحالات التي كان فيها معرفة البيانات التي تم تقديمها ذا صلة. كانت الحالة التي أتذكرها هي المكان الذي أحتاج فيه إلى معرفة أبعاد البكسل لبعض البيانات لأغراض التخطيط. ولكن تم حل ذلك بأناقة باستخدام didComponentUpdate ولم يعتمد حقًا على setState كونه غير متزامن أيضًا. تبدو هذه الحالات نادرة جدًا بحيث يصعب معها تصميم واجهة برمجة التطبيقات حولها بشكل أساسي. إذا كان من الممكن القيام به بطريقة ما ، فهذا يكفي على ما أعتقد


ليس لدي أدنى شك في أن فريق React على دراية بالارتباك الذي تحدثه الطبيعة غير المتزامنة لـ setState كثير من الأحيان ، لذلك أظن أن هناك سببًا جيدًا آخر للدلالات الحالية. اخبرني المزيد :)

Discussion

التعليق الأكثر فائدة

لذا إليك بعض الأفكار. هذه ليست إجابة كاملة بأي حال من الأحوال ، ولكن ربما لا يزال هذا مفيدًا أكثر من عدم قول أي شيء.

أولاً ، أعتقد أننا نتفق على أن تأخير التسوية لتحديثات المجموعة أمر مفيد. أي أننا نتفق على أن إعادة العرض بشكل متزامن setState() ستكون غير فعالة في كثير من الحالات ، ومن الأفضل تجميع التحديثات إذا علمنا أننا سنحصل على العديد منها على الأرجح.

على سبيل المثال ، إذا كنا داخل متصفح click handler ، وكلاهما Child و Parent call setState ، لا نريد إعادة العرض Child مرتين ، وبدلاً من ذلك يفضل وضع علامة عليها على أنها متسخة ، وإعادة عرضها معًا قبل الخروج من حدث المتصفح.

أنت تسأل: لماذا لا يمكننا القيام بنفس الشيء بالضبط (التجميع) ولكن اكتب تحديثات setState الفور إلى this.state دون انتظار نهاية التسوية. لا أعتقد أن هناك إجابة واحدة واضحة (أي من الحلين له مقايضات) ولكن إليك بعض الأسباب التي يمكنني التفكير فيها.

ضمان الاتساق الداخلي

حتى إذا تم تحديث state بشكل متزامن ، فلن يتم تحديث props . (لا يمكنك معرفة props حتى تعيد عرض المكون الأصلي ، وإذا قمت بذلك بشكل متزامن ، فسيخرج التجميع من النافذة.)

في الوقت الحالي ، العناصر التي يوفرها React ( state ، props ، refs ) متوافقة داخليًا مع بعضها البعض. هذا يعني أنه إذا كنت تستخدم هذه الكائنات فقط ، فمن المضمون أنها تشير إلى شجرة تمت تسويتها بالكامل (حتى لو كانت نسخة قديمة من تلك الشجرة). لماذا هذا مهم؟

عندما تستخدم الحالة فقط ، إذا تم مسحها بشكل متزامن (كما اقترحت) ، فسيعمل هذا النمط:

console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2

ومع ذلك ، لنفترض أن هذه الحالة يجب رفعها لتتم مشاركتها عبر بعض المكونات ، لذا يمكنك نقلها إلى أحد الوالدين:

-this.setState({ value: this.state.value + 1 });
+this.props.onIncrement(); // Does the same thing in a parent

أريد أن أوضح أنه في تطبيقات React النموذجية التي تعتمد على setState() هذا هو النوع الوحيد الأكثر شيوعًا لإعادة البناء الخاصة بـ React والذي يمكنك القيام به يوميًا .

ومع ذلك ، فإن هذا يكسر رمزنا!

console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0

هذا لأنه ، في النموذج الذي اقترحته ، سيتم مسح this.state على الفور ولكن لن يتم مسح this.props . ولا يمكننا على الفور دفع this.props بدون إعادة تقديم الأصل ، مما يعني أنه سيتعين علينا التخلي عن التجميع (والذي ، اعتمادًا على الحالة ، يمكن أن يؤدي إلى تدهور الأداء بشكل كبير).

هناك أيضًا حالات أكثر دقة حول كيفية تعطل ذلك ، على سبيل المثال إذا كنت تخلط البيانات من props (لم يتم مسحها بعد) و state (يُقترح مسحها على الفور) لإنشاء حالة جديدة : https://github.com/facebook/react/issues/122#issuecomment -81856416. يقدم المراجعون نفس المشكلة: https://github.com/facebook/react/issues/122#issuecomment -22659651.

هذه الأمثلة ليست نظرية على الإطلاق. في الواقع ، كانت ارتباطات React Redux تحتوي على هذا النوع من المشاكل بالضبط لأنها تخلط بين دعائم React وحالة non-React: https://github.com/reactjs/react-redux/issues/86 ، https://github.com/ رد فعل / ردإكس / سحب / 99 ، https://github.com/reactjs/react-redux/issues/292 ، https://github.com/reactjs/redux/issues/1415 ، https: // github. كوم / ردجس / ردإكس-ريوكس / قضايا / 525.

لا أعرف لماذا لم يصطدم مستخدمو MobX بهذا ، لكن حدسي هو أنهم قد يصطدمون بمثل هذه السيناريوهات ولكنهم يعتبرونهم خطأهم. أو ربما لم يقرؤوا الكثير من props وبدلاً من ذلك يقرؤون مباشرة من كائنات MobX القابلة للتغيير.

إذن كيف تحل React هذا الأمر اليوم؟ في React ، يتم تحديث كل من this.state و this.props فقط بعد التسوية والتدفق ، لذلك سترى 0 تتم طباعته قبل إعادة البناء وبعده. هذا يجعل حالة الرفع آمنة.

نعم ، قد يكون هذا غير مريح في بعض الحالات. خاصة بالنسبة للأشخاص القادمين من خلفيات OO والذين يريدون فقط تغيير الحالة عدة مرات بدلاً من التفكير في كيفية تمثيل تحديث كامل https://github.com/facebook/react/issues/122#issuecomment -19888472.

لا يزال ، لديك خيار نقل الحالة التي تريد قراءتها على الفور إلى كائن قابل للتغيير الجانبي ، خاصة إذا لم تستخدمه كمصدر للحقيقة للتقديم. وهو إلى حد كبير ما يتيح لك MobX القيام به 🙂.

لديك أيضًا خيار لمسح الشجرة بأكملها إذا كنت تعرف ما تفعله. واجهة برمجة التطبيقات تسمى ReactDOM.flushSync(fn) . لا أعتقد أننا وثقناها حتى الآن ، لكننا بالتأكيد سنفعل ذلك في مرحلة ما خلال دورة الإصدار 16.x. لاحظ أنه يفرض فعليًا إعادة عرض كاملة للتحديثات التي تحدث داخل المكالمة ، لذا يجب عليك استخدامها باعتدال. بهذه الطريقة لا يكسر ضمان الاتساق الداخلي بين props و state و refs .

وخلاصة القول ، لا يؤدي نموذج React دائمًا إلى الرمز الأكثر إيجازًا ، ولكنه متسق داخليًا ويضمن أن حالة الرفع آمنة .

تمكين التحديثات المتزامنة

من الناحية المفاهيمية ، تتصرف React كما لو كانت تحتوي على قائمة انتظار تحديث واحدة لكل مكون. هذا هو السبب في أن المناقشة منطقية على الإطلاق: نناقش ما إذا كان يجب تطبيق التحديثات على this.state الفور أم لا لأنه ليس لدينا شك في أنه سيتم تطبيق التحديثات بهذا الترتيب الدقيق. ومع ذلك ، لا يجب أن يكون هذا هو الحال ( هاها ).

مؤخرًا ، كنا نتحدث كثيرًا عن "العرض غير المتزامن". أعترف أننا لم نقم بعمل جيد جدًا في إيصال ما يعنيه ذلك ، ولكن هذه هي طبيعة البحث والتطوير: إنك تلاحق فكرة تبدو واعدة من الناحية المفاهيمية ، لكنك لا تفهم آثارها حقًا إلا بعد قضاء وقت كافٍ معها.

إحدى الطرق التي شرحنا بها "العرض غير المتزامن" هي أن React يمكنها تعيين أولويات مختلفة للمكالمات setState() اعتمادًا على المكان الذي تأتي منه: معالج الحدث ، استجابة الشبكة ، الرسوم المتحركة ، إلخ .

على سبيل المثال، إذا كنت تكتب رسالة، setState() المكالمات في TextBox الحاجة المكونة أن يتم مسح على الفور. ومع ذلك ، إذا تلقيت رسالة جديدة أثناء الكتابة ، فمن الأفضل على الأرجح تأخير عرض MessageBubble إلى حد معين (على سبيل المثال ثانية) بدلاً من ترك التلعثم في الكتابة بسبب حظر مسلك.

إذا سمحنا لبعض التحديثات "بأولوية أقل" ، فيمكننا تقسيم عرضها إلى أجزاء صغيرة من بضعة أجزاء من الثانية حتى لا تكون ملحوظة للمستخدم.

أعلم أن تحسينات الأداء مثل هذه قد لا تبدو مثيرة للغاية أو مقنعة. يمكنك أن تقول: "لسنا بحاجة إلى هذا مع MobX ، تتبع التحديث لدينا سريع بما يكفي فقط لتجنب إعادة العرض". لا أعتقد أن هذا صحيح في جميع الحالات (على سبيل المثال ، بغض النظر عن مدى سرعة MobX ، لا يزال يتعين عليك إنشاء عُقد DOM والقيام بالعرض على طرق العرض المثبتة حديثًا). ومع ذلك ، إذا كان هذا صحيحًا ، وإذا قررت بوعي أنك على ما يرام مع التفاف الكائنات دائمًا في مكتبة JavaScript محددة تتعقب عمليات القراءة والكتابة ، فربما لا تستفيد من هذه التحسينات كثيرًا.

لكن العرض غير المتزامن لا يتعلق فقط بتحسينات الأداء.

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

ومع ذلك ، إذا كان التنقل سريعًا بدرجة كافية (في غضون ثانية أو نحو ذلك) ، فإن الوميض وإخفاء القرص الدوار على الفور يتسبب في تدهور تجربة المستخدم. والأسوأ من ذلك ، إذا كان لديك مستويات متعددة من المكونات مع تبعيات مختلفة غير متزامنة (بيانات ، رمز ، صور) ، ينتهي بك الأمر بسلسلة من المغازل التي تومض لفترة وجيزة واحدة تلو الأخرى. يعد هذا أمرًا مزعجًا من الناحية المرئية ويجعل تطبيقك أبطأ من الناحية العملية بسبب كل عمليات إعادة تدفق DOM. وهو أيضًا مصدر الكثير من التعليمات البرمجية المعيارية.

ألن يكون رائعًا إذا كنت تقوم بعمل بسيط setState() يعرض طريقة عرض مختلفة ، فيمكننا "البدء" في عرض العرض المحدّث "في الخلفية"؟ تخيل أنه بدون كتابة أي رمز تنسيق بنفسك ، يمكنك اختيار إظهار زر دوار إذا استغرق التحديث أكثر من حد معين (على سبيل المثال ثانية) ، وإلا دع React ينفذ انتقالًا سلسًا عندما تكون التبعيات غير المتزامنة للشجرة الفرعية الجديدة بالكامل راضية . علاوة على ذلك ، بينما نحن "ننتظر" ، تظل "الشاشة القديمة" تفاعلية (على سبيل المثال ، يمكنك اختيار عنصر مختلف للانتقال إليه) ، وتفرض React أنه إذا استغرق الأمر وقتًا طويلاً ، فيجب عليك إظهار قرص دوار.

اتضح أنه مع نموذج React الحالي وبعض التعديلات على دورات الحياة ، يمكننا في الواقع تنفيذ ذلك! يعملacdlite على هذه الميزة خلال الأسابيع القليلة الماضية ، وسينشر RFC لها قريبًا.

لاحظ أن هذا ممكن فقط لأن this.state لا يتم مسحه على الفور. إذا تم مسحه على الفور ، فلن يكون لدينا أي طريقة لبدء عرض "نسخة جديدة" من العرض في الخلفية بينما لا يزال "الإصدار القديم" مرئيًا وتفاعليًا. سوف تتعارض تحديثات الدولة المستقلة.

لا أريد أن أسرق الرعد من acdlite فيما يتعلق بالإعلان عن كل هذا ولكن آمل أن يبدو هذا مثيرًا بعض الشيء على الأقل. أتفهم أن هذا لا يزال يبدو وكأنه برنامج بخار ، أو أننا لا نعرف حقًا ما نفعله. آمل أن نتمكن من إقناعك بخلاف ذلك في الأشهر المقبلة ، وأنك ستقدر مرونة نموذج React. وبقدر ما أفهم ، فإن هذه المرونة ممكنة جزئيًا على الأقل بفضل عدم مسح تحديثات الحالة على الفور.

ال 31 كومينتر

كلنا ننتظر gaearon .

Kaybarax مرحبًا ، إنها عطلة نهاية الأسبوع ؛-)

تضمين التغريدة خطأي. رائع.

أنا ستعمل الخروج على أطرافهم وهنا أقول انه بسبب الجرعات المتعددة setState الصورة في نفس القراد.

أنا ذاهب في إجازة الأسبوع المقبل ، لكن من المحتمل أن أذهب يوم الثلاثاء ، لذا سأحاول الرد يوم الاثنين.

دالة enqueueUpdate (مكون) {
ضمان حقن () ؛

// أجزاء مختلفة من الكود الخاص بنا (مثل ReactCompositeComponent's
// _renderValidatedComponent) تفترض أن استدعاءات العرض ليست متداخلة ؛
// تحقق من أن هذا هو الحال. (يتم استدعاء هذا بواسطة كل تحديث عالي المستوى
// دالة ، مثل setState ، و forceUpdate ، وما إلى ذلك ؛ خلق و
// تدمير مكونات المستوى الأعلى محمي في ReactMount.)

if (! batchingStrategy.isBatchingUpdates) {
BatchingStrategy.batchedUpdates (enqueueUpdate ، مكون) ؛
إرجاع؛
}

dirtyComponents.push (مكون) ؛
إذا كان (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1 ؛
}
}

mweststrate فقط
أنا متأكد من أننا نتفق جميعًا على أنه سيكون من الأسهل بكثير التفكير في الحالة إذا كانت setState متزامنة.
مهما كانت أسباب جعل setState غير متزامنة ، فأنا لست متأكدًا من رد فعل الفريق جيدًا مقارنةً بالعيوب التي قد تقدم ، على سبيل المثال صعوبة التفكير في الحالة الآن والارتباك الذي يجلب للمطورين.

mweststrate من المثير للاهتمام أنني سألت نفس السؤال هنا: https://discuss.reactjs.org/t/historic-reasons-behind-setstate-not-being-immediately-visible/8487

أنا شخصياً قد رأيت ارتباكاً لدى مطورين آخرين حول هذا الموضوع. gaearon سيكون من الرائع الحصول على تفسير لهذا عندما يكون لديك بعض الوقت :)

عذرًا ، إنها نهاية العام وقد تأخرنا قليلاً في GitHub وما إلى ذلك في محاولة إنهاء كل شيء كنا نعمل عليه قبل العطلات.

أعتزم العودة إلى هذا الموضوع ومناقشته. ولكنه أيضًا هدف متحرك إلى حد ما لأننا نعمل حاليًا على ميزات تفاعل غير متزامن تتعلق مباشرة بكيفية تحديث this.state ومتى يتم تحديثه. لا أريد قضاء الكثير من الوقت في كتابة شيء ما ، ثم يتعين علي إعادة كتابته لأن الافتراضات الأساسية قد تغيرت. لذا أود أن أبقي هذا مفتوحًا ، لكني لا أعرف حتى الآن متى سأتمكن من إعطاء إجابة نهائية.

لذا إليك بعض الأفكار. هذه ليست إجابة كاملة بأي حال من الأحوال ، ولكن ربما لا يزال هذا مفيدًا أكثر من عدم قول أي شيء.

أولاً ، أعتقد أننا نتفق على أن تأخير التسوية لتحديثات المجموعة أمر مفيد. أي أننا نتفق على أن إعادة العرض بشكل متزامن setState() ستكون غير فعالة في كثير من الحالات ، ومن الأفضل تجميع التحديثات إذا علمنا أننا سنحصل على العديد منها على الأرجح.

على سبيل المثال ، إذا كنا داخل متصفح click handler ، وكلاهما Child و Parent call setState ، لا نريد إعادة العرض Child مرتين ، وبدلاً من ذلك يفضل وضع علامة عليها على أنها متسخة ، وإعادة عرضها معًا قبل الخروج من حدث المتصفح.

أنت تسأل: لماذا لا يمكننا القيام بنفس الشيء بالضبط (التجميع) ولكن اكتب تحديثات setState الفور إلى this.state دون انتظار نهاية التسوية. لا أعتقد أن هناك إجابة واحدة واضحة (أي من الحلين له مقايضات) ولكن إليك بعض الأسباب التي يمكنني التفكير فيها.

ضمان الاتساق الداخلي

حتى إذا تم تحديث state بشكل متزامن ، فلن يتم تحديث props . (لا يمكنك معرفة props حتى تعيد عرض المكون الأصلي ، وإذا قمت بذلك بشكل متزامن ، فسيخرج التجميع من النافذة.)

في الوقت الحالي ، العناصر التي يوفرها React ( state ، props ، refs ) متوافقة داخليًا مع بعضها البعض. هذا يعني أنه إذا كنت تستخدم هذه الكائنات فقط ، فمن المضمون أنها تشير إلى شجرة تمت تسويتها بالكامل (حتى لو كانت نسخة قديمة من تلك الشجرة). لماذا هذا مهم؟

عندما تستخدم الحالة فقط ، إذا تم مسحها بشكل متزامن (كما اقترحت) ، فسيعمل هذا النمط:

console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2

ومع ذلك ، لنفترض أن هذه الحالة يجب رفعها لتتم مشاركتها عبر بعض المكونات ، لذا يمكنك نقلها إلى أحد الوالدين:

-this.setState({ value: this.state.value + 1 });
+this.props.onIncrement(); // Does the same thing in a parent

أريد أن أوضح أنه في تطبيقات React النموذجية التي تعتمد على setState() هذا هو النوع الوحيد الأكثر شيوعًا لإعادة البناء الخاصة بـ React والذي يمكنك القيام به يوميًا .

ومع ذلك ، فإن هذا يكسر رمزنا!

console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0

هذا لأنه ، في النموذج الذي اقترحته ، سيتم مسح this.state على الفور ولكن لن يتم مسح this.props . ولا يمكننا على الفور دفع this.props بدون إعادة تقديم الأصل ، مما يعني أنه سيتعين علينا التخلي عن التجميع (والذي ، اعتمادًا على الحالة ، يمكن أن يؤدي إلى تدهور الأداء بشكل كبير).

هناك أيضًا حالات أكثر دقة حول كيفية تعطل ذلك ، على سبيل المثال إذا كنت تخلط البيانات من props (لم يتم مسحها بعد) و state (يُقترح مسحها على الفور) لإنشاء حالة جديدة : https://github.com/facebook/react/issues/122#issuecomment -81856416. يقدم المراجعون نفس المشكلة: https://github.com/facebook/react/issues/122#issuecomment -22659651.

هذه الأمثلة ليست نظرية على الإطلاق. في الواقع ، كانت ارتباطات React Redux تحتوي على هذا النوع من المشاكل بالضبط لأنها تخلط بين دعائم React وحالة non-React: https://github.com/reactjs/react-redux/issues/86 ، https://github.com/ رد فعل / ردإكس / سحب / 99 ، https://github.com/reactjs/react-redux/issues/292 ، https://github.com/reactjs/redux/issues/1415 ، https: // github. كوم / ردجس / ردإكس-ريوكس / قضايا / 525.

لا أعرف لماذا لم يصطدم مستخدمو MobX بهذا ، لكن حدسي هو أنهم قد يصطدمون بمثل هذه السيناريوهات ولكنهم يعتبرونهم خطأهم. أو ربما لم يقرؤوا الكثير من props وبدلاً من ذلك يقرؤون مباشرة من كائنات MobX القابلة للتغيير.

إذن كيف تحل React هذا الأمر اليوم؟ في React ، يتم تحديث كل من this.state و this.props فقط بعد التسوية والتدفق ، لذلك سترى 0 تتم طباعته قبل إعادة البناء وبعده. هذا يجعل حالة الرفع آمنة.

نعم ، قد يكون هذا غير مريح في بعض الحالات. خاصة بالنسبة للأشخاص القادمين من خلفيات OO والذين يريدون فقط تغيير الحالة عدة مرات بدلاً من التفكير في كيفية تمثيل تحديث كامل https://github.com/facebook/react/issues/122#issuecomment -19888472.

لا يزال ، لديك خيار نقل الحالة التي تريد قراءتها على الفور إلى كائن قابل للتغيير الجانبي ، خاصة إذا لم تستخدمه كمصدر للحقيقة للتقديم. وهو إلى حد كبير ما يتيح لك MobX القيام به 🙂.

لديك أيضًا خيار لمسح الشجرة بأكملها إذا كنت تعرف ما تفعله. واجهة برمجة التطبيقات تسمى ReactDOM.flushSync(fn) . لا أعتقد أننا وثقناها حتى الآن ، لكننا بالتأكيد سنفعل ذلك في مرحلة ما خلال دورة الإصدار 16.x. لاحظ أنه يفرض فعليًا إعادة عرض كاملة للتحديثات التي تحدث داخل المكالمة ، لذا يجب عليك استخدامها باعتدال. بهذه الطريقة لا يكسر ضمان الاتساق الداخلي بين props و state و refs .

وخلاصة القول ، لا يؤدي نموذج React دائمًا إلى الرمز الأكثر إيجازًا ، ولكنه متسق داخليًا ويضمن أن حالة الرفع آمنة .

تمكين التحديثات المتزامنة

من الناحية المفاهيمية ، تتصرف React كما لو كانت تحتوي على قائمة انتظار تحديث واحدة لكل مكون. هذا هو السبب في أن المناقشة منطقية على الإطلاق: نناقش ما إذا كان يجب تطبيق التحديثات على this.state الفور أم لا لأنه ليس لدينا شك في أنه سيتم تطبيق التحديثات بهذا الترتيب الدقيق. ومع ذلك ، لا يجب أن يكون هذا هو الحال ( هاها ).

مؤخرًا ، كنا نتحدث كثيرًا عن "العرض غير المتزامن". أعترف أننا لم نقم بعمل جيد جدًا في إيصال ما يعنيه ذلك ، ولكن هذه هي طبيعة البحث والتطوير: إنك تلاحق فكرة تبدو واعدة من الناحية المفاهيمية ، لكنك لا تفهم آثارها حقًا إلا بعد قضاء وقت كافٍ معها.

إحدى الطرق التي شرحنا بها "العرض غير المتزامن" هي أن React يمكنها تعيين أولويات مختلفة للمكالمات setState() اعتمادًا على المكان الذي تأتي منه: معالج الحدث ، استجابة الشبكة ، الرسوم المتحركة ، إلخ .

على سبيل المثال، إذا كنت تكتب رسالة، setState() المكالمات في TextBox الحاجة المكونة أن يتم مسح على الفور. ومع ذلك ، إذا تلقيت رسالة جديدة أثناء الكتابة ، فمن الأفضل على الأرجح تأخير عرض MessageBubble إلى حد معين (على سبيل المثال ثانية) بدلاً من ترك التلعثم في الكتابة بسبب حظر مسلك.

إذا سمحنا لبعض التحديثات "بأولوية أقل" ، فيمكننا تقسيم عرضها إلى أجزاء صغيرة من بضعة أجزاء من الثانية حتى لا تكون ملحوظة للمستخدم.

أعلم أن تحسينات الأداء مثل هذه قد لا تبدو مثيرة للغاية أو مقنعة. يمكنك أن تقول: "لسنا بحاجة إلى هذا مع MobX ، تتبع التحديث لدينا سريع بما يكفي فقط لتجنب إعادة العرض". لا أعتقد أن هذا صحيح في جميع الحالات (على سبيل المثال ، بغض النظر عن مدى سرعة MobX ، لا يزال يتعين عليك إنشاء عُقد DOM والقيام بالعرض على طرق العرض المثبتة حديثًا). ومع ذلك ، إذا كان هذا صحيحًا ، وإذا قررت بوعي أنك على ما يرام مع التفاف الكائنات دائمًا في مكتبة JavaScript محددة تتعقب عمليات القراءة والكتابة ، فربما لا تستفيد من هذه التحسينات كثيرًا.

لكن العرض غير المتزامن لا يتعلق فقط بتحسينات الأداء.

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

ومع ذلك ، إذا كان التنقل سريعًا بدرجة كافية (في غضون ثانية أو نحو ذلك) ، فإن الوميض وإخفاء القرص الدوار على الفور يتسبب في تدهور تجربة المستخدم. والأسوأ من ذلك ، إذا كان لديك مستويات متعددة من المكونات مع تبعيات مختلفة غير متزامنة (بيانات ، رمز ، صور) ، ينتهي بك الأمر بسلسلة من المغازل التي تومض لفترة وجيزة واحدة تلو الأخرى. يعد هذا أمرًا مزعجًا من الناحية المرئية ويجعل تطبيقك أبطأ من الناحية العملية بسبب كل عمليات إعادة تدفق DOM. وهو أيضًا مصدر الكثير من التعليمات البرمجية المعيارية.

ألن يكون رائعًا إذا كنت تقوم بعمل بسيط setState() يعرض طريقة عرض مختلفة ، فيمكننا "البدء" في عرض العرض المحدّث "في الخلفية"؟ تخيل أنه بدون كتابة أي رمز تنسيق بنفسك ، يمكنك اختيار إظهار زر دوار إذا استغرق التحديث أكثر من حد معين (على سبيل المثال ثانية) ، وإلا دع React ينفذ انتقالًا سلسًا عندما تكون التبعيات غير المتزامنة للشجرة الفرعية الجديدة بالكامل راضية . علاوة على ذلك ، بينما نحن "ننتظر" ، تظل "الشاشة القديمة" تفاعلية (على سبيل المثال ، يمكنك اختيار عنصر مختلف للانتقال إليه) ، وتفرض React أنه إذا استغرق الأمر وقتًا طويلاً ، فيجب عليك إظهار قرص دوار.

اتضح أنه مع نموذج React الحالي وبعض التعديلات على دورات الحياة ، يمكننا في الواقع تنفيذ ذلك! يعملacdlite على هذه الميزة خلال الأسابيع القليلة الماضية ، وسينشر RFC لها قريبًا.

لاحظ أن هذا ممكن فقط لأن this.state لا يتم مسحه على الفور. إذا تم مسحه على الفور ، فلن يكون لدينا أي طريقة لبدء عرض "نسخة جديدة" من العرض في الخلفية بينما لا يزال "الإصدار القديم" مرئيًا وتفاعليًا. سوف تتعارض تحديثات الدولة المستقلة.

لا أريد أن أسرق الرعد من acdlite فيما يتعلق بالإعلان عن كل هذا ولكن آمل أن يبدو هذا مثيرًا بعض الشيء على الأقل. أتفهم أن هذا لا يزال يبدو وكأنه برنامج بخار ، أو أننا لا نعرف حقًا ما نفعله. آمل أن نتمكن من إقناعك بخلاف ذلك في الأشهر المقبلة ، وأنك ستقدر مرونة نموذج React. وبقدر ما أفهم ، فإن هذه المرونة ممكنة جزئيًا على الأقل بفضل عدم مسح تحديثات الحالة على الفور.

تفسير رائع ومتعمق للقرارات وراء بنية React. شكرا.

علامة

شكرا لك دان.

أنا ❤️ هذه المسألة. سؤال رائع وإجابة رائعة. لطالما اعتقدت أن هذا كان قرار تصميم سيئًا ، الآن يجب أن أعيد التفكير

شكرا لك دان.

أسميها مجموعة غير متزامنة: حالة رهيبة: ابتسامة:

أميل إلى الاعتقاد بأنه يجب تنفيذ كل شيء غير متزامن أولاً ، وإذا وجدت حاجة لعملية مزامنة ، فقم بلف العملية غير المتزامنة مع انتظار اكتمالها. من الأسهل بكثير إخراج كود المزامنة من الكود غير المتزامن (كل ما تحتاجه هو غلاف) من العكس (الذي يتطلب في الأساس إعادة كتابة كاملة ، ما لم تصل إلى خيط ، وهو ليس خفيف الوزن على الإطلاق).

gaearon شكرا على الشرح الشامل! لقد كان يزعجني لفترة طويلة ("يجب أن يكون هناك سبب وجيه ، لكن لا أحد يستطيع تحديد أيهما"). ولكن الآن أصبح الأمر منطقيًا تمامًا وأرى كيف أن هذا قرار واع حقًا :). شكرا جزيلا للإجابة الشاملة ، حقا أقدر ذلك!

أو ربما لا يقرؤون كثيرًا من الدعائم وبدلاً من ذلك يقرؤون مباشرةً من الكائنات القابلة للتغيير من MobX بدلاً من ذلك.

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

مرة أخرى ، شكرا جزيلا!

gaearon شكرا على الشرح المفصل
على الرغم من أنه لا يزال هناك شيء مفقود هنا أعتقد أنني أفهمه ولكني أريد أن أتأكد.

عندما يتم تسجيل الحدث "Outside React" ، فهذا يعني أنه ربما من خلال addEventListener على المرجع على سبيل المثال. ثم لا يوجد تجميع.
ضع في اعتبارك هذا الرمز:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    this.refBtn.addEventListener("click", this.onClick);
  }

  componentWillUnmount() {
    this.refBtn.removeEventListener("click", this.onClick);
  }

  onClick = () => {
    console.log("before setState", this.state.count);
    this.setState(state => ({ count: state.count + 1 }));
    console.log("after setState", this.state.count);
  };

  render() {
    return (
      <div>
        <button onClick={this.onClick}>React Event</button>
        <button ref={ref => (this.refBtn = ref)}>Direct DOM event</button>
      </div>
    );
  }
}

عندما نضغط على زر "حدث رد الفعل" ، سنرى في وحدة التحكم:
"before setState" 0
"after setState" 0
عندما يتم النقر على الزر الآخر "حدث DOM المباشر" ، سنرى في وحدة التحكم:
"before setState" 0
"after setState" 1

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

ماهي أفكارك حول هذا؟ : التفكير:

@ sag1v على الرغم من أنه مرتبط قليلاً ، إلا أنه من الواضح على الأرجح فتح عدد جديد للأسئلة الجديدة. فقط استخدم # 11527 في مكان ما في الوصف لربطه بهذا.

لقد أعطاني @ sag1vgaearon ردًا مقتضبًا للغاية هنا https://twitter.com/dan_abramov/status/949992957180104704 . أعتقد أن رأيه في هذا سوف يجيبني أيضًا بشكل ملموس أكثر.

mweststrate فكرت في فتح إصدار جديد ولكن بعد ذلك أدركت أن هذا مرتبط بشكل مباشر بسؤالك "لماذا setState غير متزامن؟".
نظرًا لأن هذه المناقشة تدور حول القرارات التي تم اتخاذها بشأن جعل setState "غير متزامن" ، فقد فكرت في إضافة متى ولماذا جعلها "متزامنة".
لا أمانع في فتح عدد جديد إذا لم أقنعك بأن مشاركتي مرتبطة بهذه المشكلة: wink:

Kaybarax لأن سؤالك كان "_متى يتم مزامنة_" وليس "_ لماذا يتم مزامنة_" ؟.
كما ذكرت في رسالتي ، أعتقد أنني أعرف السبب ، لكني أريد أن أتأكد وأن أحصل على الإجابة الرسمية. :ابتسامة:

رد الفعل لا يمكن أن يتحكم بشكل كامل في تدفق الحدث ولا يمكن أن يكون متأكدًا من موعد وكيفية إطلاق الحدث التالي ، لذلك "في وضع الذعر" ، فإنه سيؤدي فقط إلى تغيير الحالة على الفور

نوعا ما. على الرغم من أن هذا لا يتعلق بالضبط بالسؤال حول تحديث this.state .

ما تطلبه هو الحالات التي تقوم فيها React بتمكين التجميع. تقوم React حاليًا بتجميع التحديثات داخل معالجات الأحداث المُدارة بواسطة React لأن React "تجلس" في أعلى إطار مكدس الاستدعاءات وتعرف متى يتم تشغيل جميع معالجات أحداث React. في هذه المرحلة ، يتدفق التحديث.

إذا لم يتم تعيين معالج الحدث من قبل تتفاعل، حاليا يجعل متزامن التحديث. لأنه لا يعرف ما إذا كان الانتظار أم لا ، وما إذا كانت التحديثات الأخرى ستحدث قريبًا.

سيتغير هذا السلوك في الإصدارات المستقبلية من React. تتمثل الخطة في التعامل مع التحديثات على أنها ذات أولوية منخفضة بشكل افتراضي بحيث ينتهي بها الأمر بدمجها وتجميعها معًا (على سبيل المثال في غضون ثانية) ، مع الاشتراك لمسحها على الفور. يمكنك قراءة المزيد هنا: https://github.com/facebook/react/issues/11171#issuecomment -357945371.

مدهش!

يجب توثيق هذا السؤال والإجابة في مكان ما يسهل الوصول إليه. شكرا لكم يا رفاق منور لنا.

تعلمت الكثير. شكرا

أحاول إضافة وجهة نظري إلى الموضوع. أعمل على تطبيق يعتمد على MobX منذ عدة أشهر ، وكنت أستكشف ClojureScript منذ سنوات وصنعت بديل React الخاص بي (يسمى Respo) ، لقد جربت Redux في الأيام الأولى على الرغم من وقت قصير جدًا ، وأنا أراهن على ReasonML.

الفكرة الأساسية في الجمع بين React و Functional Programming (FP) هي أنك تحصل على جزء من البيانات يمكنك تحويله إلى عرض بأي مهارات لديك وتلتزم بالقوانين في FP. ليس لديك أي آثار جانبية إذا كنت تستخدم وظائف نقية فقط.

رد الفعل ليس وظيفيًا خالصًا. من خلال احتضان الحالات المحلية داخل المكونات ، تتمتع React بقدرة التفاعل مع المكتبات المختلفة المتعلقة بـ DOM وواجهات برمجة تطبيقات المتصفح الأخرى (صديقة أيضًا لـ MobX) ، مما يجعل React في الوقت نفسه غير نقي. ومع ذلك ، حاولت في ClojureScript ، إذا كانت React نقية ، فقد تكون كارثة نظرًا لأنه من الصعب حقًا التفاعل مع العديد من المكتبات الحالية التي لها آثار جانبية فيها.

لذلك في Respo (الحل الخاص بي) ، كان لدي هدفان يبدوان متعارضين ، 1) view = f(store) لذلك لا يُتوقع وجود ولاية محلية ؛ 2) لا أرغب في برمجة جميع حالات واجهة المستخدم المكونة في مخفضات عالمية لأن ذلك قد يكون من الصعب صيانته. في النهاية ، اكتشفت أنني بحاجة إلى سكر تركيبي ، مما يساعدني في الحفاظ على حالات المكونات في متجر عالمي باستخدام paths ، وفي الوقت نفسه ، أكتب تحديثات الحالة داخل المكون باستخدام وحدات ماكرو Clojure.

إذن ما تعلمته: الدول المحلية هي ميزة تجربة مطور ، نرغب في الأسفل في دول عالمية تمكن محركاتنا من إجراء تحسينات في مستويات عميقة. لذا ، يفضل موبكس ​​OOP ، هل هذا لتجربة المطور أم للمحركات؟

بالمناسبة لقد تحدثت عن مستقبل React وميزاتها غير المتزامنة ، في حال فاتتك:
https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html

دان ، أنت مثلي الأعلى ... شكرا جزيلا.

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات