الرد: رفع الحالة يقتل تطبيقك

غطاء


هل سمعت عن "رفع الدولة"؟ أعتقد أن لديك وهذا هو السبب الدقيق لوجودك هنا. كيف يمكن أن يكون أحد المفاهيم الرئيسية الـ 12 المدرجة في وثائق React الرسمية قد يؤدي إلى ضعف الأداء؟ ضمن هذه المقالة ، سننظر في الموقف عندما يكون الأمر كذلك بالفعل.


الخطوة 1: ارفعها


أقترح عليك إنشاء لعبة بسيطة من تيك تاك تو. لهذه اللعبة سنحتاج:


  • بعض حالة اللعبة. لا يوجد منطق لعبة حقيقي لمعرفة ما إذا فزنا أو خسرنا. مجرد مجموعة بسيطة ثنائية الأبعاد مليئة إما undefined ، "x" أو "0".


     const size = 10 // Two-dimensional array (size * size) filled with `undefined`. Represents an empty field. const initialField = new Array(size).fill(new Array(size).fill(undefined)) 

  • حاوية الأم لاستضافة حالة لعبتنا.


     const App = () => { const [field, setField] = useState(initialField) return ( <div> {field.map((row, rowI) => ( <div> {row.map((cell, cellI) => ( <Cell content={cell} setContent={ // Update a single cell of a two-dimensional array // and return a new two dimensional array (newContent) => setField([ // Copy rows before our target row ...field.slice(0, rowI), [ // Copy cells before our target cell ...field[rowI].slice(0, cellI), newContent, // Copy cells after our target cell ...field[rowI].slice(cellI + 1), ], // Copy rows after our target row ...field.slice(rowI + 1), ]) } /> ))} </div> ))} </div> ) } 

  • مكون فرعي لعرض حالة خلية واحدة.


     const randomContent = () => (Math.random() > 0.5 ? 'x' : '0') const Cell = ({ content, setContent }) => ( <div onClick={() => setContent(randomContent())}>{content}</div> ) 


عرض توضيحي مباشر # 1


حتى الآن يبدو جيدا. حقل تفاعلي تمامًا يمكنك التفاعل معه بسرعة الضوء :) دعنا نزيد الحجم. قل ، إلى 100. نعم ، لقد حان الوقت للنقر فوق هذا الرابط التجريبي وتغيير متغير size في الأعلى. لا تزال سريعة بالنسبة لك؟ جرب 200 أو استخدم وحدة التحكم في وحدة المعالجة المركزية المدمجة في Chrome . هل ترى الآن فجوة كبيرة بين الوقت الذي تنقر فيه على الخلية والوقت الذي يتغير فيه محتواها؟


دعنا نغير size إلى 10 ونضيف بعض التوصيفات لمعرفة السبب.


 const Cell = ({ content, setContent }) => { console.log('cell rendered') return <div onClick={() => setContent(randomContent())}>{content}</div> } 

عرض حي رقم 2


نعم ، هذا كل شيء. سيكون console.log بسيط يكفي لأنه يعمل على كل تقديم.


إذن ماذا نرى؟ استنادًا إلى الرقم الوارد في عبارات "الخلية المقدمة" (بالنسبة size = يجب أن تكون N) في وحدة التحكم الخاصة بنا ، يبدو أن الحقل بأكمله يتم تقديمه في كل مرة تتغير فيها خلية واحدة.


الأمر الأكثر وضوحًا هو إضافة بعض المفاتيح كما تشير وثائق React .


 <div> {field.map((row, rowI) => ( <div key={rowI}> {row.map((cell, cellI) => ( <Cell key={`row${rowI}cell${cellI}`} content={cell} setContent={(newContent) => setField([ ...field.slice(0, rowI), [ ...field[rowI].slice(0, cellI), newContent, ...field[rowI].slice(cellI + 1), ], ...field.slice(rowI + 1), ]) } /> ))} </div> ))} </div> 

العرض التوضيحي المباشر رقم 3


ومع ذلك ، بعد زيادة size مرة أخرى نرى أن هذه المشكلة لا تزال قائمة. إذا تمكنا من رؤية سبب ظهور أي مكون ... لحسن الحظ ، يمكننا تقديم بعض المساعدة من React DevTools المدهشة. إنه قادر على تسجيل سبب تقديم المكونات. يجب عليك تمكينه يدويًا.


رد إعدادات DevTools


بمجرد تمكينه ، يمكننا أن نرى أن جميع الخلايا أعيد تقديمها لأن الدعائم الخاصة بها تغيرت ، على وجه التحديد ، setContent prop.


الرد على تقرير DevTools رقم 1


كل خلية لديها اثنين من الدعائم: content و setContent . إذا تغيرت الخلية [0] [0] ، فلن يتغير محتوى الخلية [0] [1]. من ناحية أخرى ، يلتقط setContent field و cellI و rowI في إغلاقه. cellI و cellI كما هي ، لكن field يتغير مع كل تغيير لأي خلية.


دعونا refactor setContent والحفاظ على setContent نفسه.


للاحتفاظ بالإشارة إلى setContent ، يجب أن نتخلص من عمليات الإغلاق. يمكننا القضاء على إغلاق rowI و rowI بجعل Cell بنا تمرر cellI و rowI بشكل صريح إلى setContent . بالنسبة إلى field ، يمكننا استخدام ميزة أنيقة من setState - وهي تقبل عمليات الاسترجاعات .


 const [field, setField] = useState(initialField) // `useCallback` keeps reference to `setCell` the same. const setCell = useCallback( (rowI, cellI, newContent) => setField((oldField) => [ ...oldField.slice(0, rowI), [ ...oldField[rowI].slice(0, cellI), newContent, ...oldField[rowI].slice(cellI + 1), ], ...oldField.slice(rowI + 1), ]), [], ) 

مما يجعل App يبدو مثل هذا


 <div> {field.map((row, rowI) => ( <div key={rowI}> {row.map((cell, cellI) => ( <Cell key={`row${rowI}cell${cellI}`} content={cell} rowI={rowI} cellI={cellI} setContent={setCell} /> ))} </div> ))} </div> 

الآن يجب على Cell تمرير cellI و rowI إلى setContent .


 const Cell = ({ content, rowI, cellI, setContent }) => { console.log('cell render') return ( <div onClick={() => setContent(rowI, cellI, randomContent())}> {content} </div> ) } 

عرض حي رقم 4


دعنا نلقي نظرة على تقرير DevTools.


الرد على تقرير DevTools رقم 2


ماذا؟ لماذا هيك تقول "الدعائم الأصل تغيرت"؟ وبالتالي فإن الشيء هو أنه في كل مرة يتم تحديث مجالنا يتم إعادة تقديم App . لذلك يتم إعادة تقديم مكوناته الفرعية. طيب. هل يقول stackoverflow أي شيء مفيد حول تحسين أداء React؟ يقترح الإنترنت استخدام shouldComponentUpdate أو أقربائه المقربين: PureComponent memo .


 const Cell = memo(({ content, rowI, cellI, setContent }) => { console.log('cell render') return ( <div onClick={() => setContent(rowI, cellI, randomContent())}> {content} </div> ) }) 

عرض حي رقم 5


ياي! الآن يتم إعادة تقديم خلية واحدة فقط بمجرد تغيير محتواها. لكن انتظر ... هل كان هناك أي مفاجأة؟ اتبعنا أفضل الممارسات وحصلنا على النتيجة المتوقعة.


كان من المفترض أن تكون الضحك الشرير هنا. بما أنني لست معك ، من فضلك ، حاول أن تتخيلها بأقصى قدر ممكن. تابع وزيادة size في الإصدار التجريبي المباشر رقم 5 . هذه المرة قد تضطر إلى الذهاب مع عدد أكبر قليلا. ومع ذلك ، فإن تأخر لا يزال هناك. لماذا ؟؟


دعنا نلقي نظرة على تقرير DebTools مرة أخرى.


الرد على تقرير DevTools رقم 3


لا يوجد سوى تطبيق واحد Cell وكان سريعًا للغاية ، لكن هناك أيضًا تطبيقًا App استغرق بعض الوقت. الشيء هو أنه مع كل إعادة تقديم App كل Cell مقارنة الدعائم الجديدة مع الدعائم السابقة. حتى إذا قررت عدم تقديم (وهو بالضبط حالتنا) ، فإن هذه المقارنة لا تزال تستغرق بعض الوقت. يا (1) ، ولكن هذا (1) يحدث size * size مرات!


الخطوة 2: تحريكه لأسفل


ما الذي يمكننا القيام به للتغلب عليه؟ إذا كان تقديم App يكلفنا كثيرًا ، فيتعين علينا إيقاف تقديم App . لا يمكن ذلك إذا useState استضافة حالتنا في App باستخدام useState ، لأن هذا هو بالضبط ما يعيد useState . لذلك يتعين علينا تحريك حالتنا لأسفل والسماح لكل Cell بالاشتراك في الحالة بمفردها.


لنقم بإنشاء فصل مخصص سيكون حاوية لحالتنا.


 class Field { constructor(fieldSize) { this.size = fieldSize // Copy-paste from `initialState` this.data = new Array(this.size).fill(new Array(this.size).fill(undefined)) } cellContent(rowI, cellI) { return this.data[rowI][cellI] } // Copy-paste from old `setCell` setCell(rowI, cellI, newContent) { console.log('setCell') this.data = [ ...this.data.slice(0, rowI), [ ...this.data[rowI].slice(0, cellI), newContent, ...this.data[rowI].slice(cellI + 1), ], ...this.data.slice(rowI + 1), ] } map(cb) { return this.data.map(cb) } } const field = new Field(size) 

ثم قد يبدو تطبيقنا كالتالي:


 const App = () => { return ( <div> {// As you can see we still need to iterate over our state to get indexes. field.map((row, rowI) => ( <div key={rowI}> {row.map((cell, cellI) => ( <Cell key={`row${rowI}cell${cellI}`} rowI={rowI} cellI={cellI} /> ))} </div> ))} </div> ) } 

ويمكن Cell عرض المحتوى من field بمفرده:


 const Cell = ({ rowI, cellI }) => { console.log('cell render') const content = field.cellContent(rowI, cellI) return ( <div onClick={() => field.setCell(rowI, cellI, randomContent())}> {content} </div> ) } 

عرض حي # 6


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


كيف يمكننا تشغيل عرض برمجيًا؟


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


إليك كيفية إنشاء ربط مخصص لفرض إعادة تقديمه.


 const useForceRender = () => { const [, setDummy] = useState(0) const forceRender = useCallback(() => setDummy((oldVal) => oldVal + 1), []) return forceRender } 

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


 class Field { constructor(fieldSize) { this.size = fieldSize this.data = new Array(this.size).fill(new Array(this.size).fill(undefined)) this.subscribers = {} } _cellSubscriberId(rowI, cellI) { return `row${rowI}cell${cellI}` } cellContent(rowI, cellI) { return this.data[rowI][cellI] } setCell(rowI, cellI, newContent) { console.log('setCell') this.data = [ ...this.data.slice(0, rowI), [ ...this.data[rowI].slice(0, cellI), newContent, ...this.data[rowI].slice(cellI + 1), ], ...this.data.slice(rowI + 1), ] const cellSubscriber = this.subscribers[this._cellSubscriberId(rowI, cellI)] if (cellSubscriber) { cellSubscriber() } } map(cb) { return this.data.map(cb) } // Note that we subscribe not to updates of the whole filed, but to updates of one cell only subscribeCellUpdates(rowI, cellI, onSetCellCallback) { this.subscribers[this._cellSubscriberId(rowI, cellI)] = onSetCellCallback } } 

الآن يمكننا الاشتراك في التحديثات الميدانية.


 const Cell = ({ rowI, cellI }) => { console.log('cell render') const forceRender = useForceRender() useEffect(() => field.subscribeCellUpdates(rowI, cellI, forceRender), [ forceRender, ]) const content = field.cellContent(rowI, cellI) return ( <div onClick={() => field.setCell(rowI, cellI, randomContent())}> {content} </div> ) } 

عرض حي رقم 7


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


دعنا نلقي نظرة على تقرير DevTools.


الرد على تقرير DevTools رقم 4


كما نرى الآن ، يتم تقديم Cell فقط وهي سريعة الجنون.


ماذا لو قلت الآن أن رمز Cell هو سبب محتمل لتسرب الذاكرة؟ كما ترون ، في useEffect ، useEffect في تحديثات الخلايا ، لكننا لا useEffect اشتراكك مطلقًا. وهذا يعني أنه حتى عندما يتم تدمير Cell ، يستمر اشتراكها. دعنا نغير ذلك.


أولاً ، نحتاج إلى تعليم Field ما يعنيه إلغاء الاشتراك.


 class Field { // ... unsubscribeCellUpdates(rowI, cellI) { delete this.subscribers[this._cellSubscriberId(rowI, cellI)] } } 

الآن يمكننا تطبيق unsubscribeCellUpdates على Cell لدينا.


 const Cell = ({ rowI, cellI }) => { console.log('cell render') const forceRender = useForceRender() useEffect(() => { field.subscribeCellUpdates(rowI, cellI, forceRender) return () => field.unsubscribeCellUpdates(rowI, cellI) }, [forceRender]) const content = field.cellContent(rowI, cellI) return ( <div onClick={() => field.setCell(rowI, cellI, randomContent())}> {content} </div> ) } 

عرض توضيحي مباشر رقم 8


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


خطوة المكافأة: إعادة بيع في العالم الحقيقي


في العرض التوضيحي المباشر رقم 8 ، استخدمنا field العالمي ، والذي لا ينبغي أن يكون هو الحال في تطبيق حقيقي. لحلها ، يمكننا استضافة field في تطبيقنا وتمريره إلى أسفل الشجرة باستخدام [سياق] ().


 const AppContext = createContext() const App = () => { // Note how we used a factory to initialize our state here. // Field creation could be quite expensive for big fields. // So we don't want to create it each time we render and block the event loop. const [field] = useState(() => new Field(size)) return ( <AppContext.Provider value={field}> <div> {field.map((row, rowI) => ( <div key={rowI}> {row.map((cell, cellI) => ( <Cell key={`row${rowI}cell${cellI}`} rowI={rowI} cellI={cellI} /> ))} </div> ))} </div> </AppContext.Provider> ) } 

الآن يمكننا أن نستهلك field من السياق في Cell .


 const Cell = ({ rowI, cellI }) => { console.log('cell render') const forceRender = useForceRender() const field = useContext(AppContext) useEffect(() => { field.subscribeCellUpdates(rowI, cellI, forceRender) return () => field.unsubscribeCellUpdates(rowI, cellI) }, [forceRender]) const content = field.cellContent(rowI, cellI) return ( <div onClick={() => field.setCell(rowI, cellI, randomContent())}> {content} </div> ) } 

عرض توضيحي مباشر رقم 9


نأمل أن تكون قد وجدت شيئًا مفيدًا لمشروعك. لا تتردد في توصيل ملاحظاتك لي! بالتأكيد أقدر أي نقد وأسئلة.

Source: https://habr.com/ru/post/ar471300/


All Articles