
هل سمعت عن "رفع الدولة"؟ أعتقد أن لديك وهذا هو السبب الدقيق لوجودك هنا. كيف يمكن أن يكون أحد المفاهيم الرئيسية الـ 12 المدرجة في وثائق React الرسمية قد يؤدي إلى ضعف الأداء؟ ضمن هذه المقالة ، سننظر في الموقف عندما يكون الأمر كذلك بالفعل.
الخطوة 1: ارفعها
أقترح عليك إنشاء لعبة بسيطة من تيك تاك تو. لهذه اللعبة سنحتاج:
بعض حالة اللعبة. لا يوجد منطق لعبة حقيقي لمعرفة ما إذا فزنا أو خسرنا. مجرد مجموعة بسيطة ثنائية الأبعاد مليئة إما undefined
، "x"
أو "0".
const size = 10
حاوية الأم لاستضافة حالة لعبتنا.
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 المدهشة. إنه قادر على تسجيل سبب تقديم المكونات. يجب عليك تمكينه يدويًا.

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

كل خلية لديها اثنين من الدعائم: 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)
مما يجعل 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.

ماذا؟ لماذا هيك تقول "الدعائم الأصل تغيرت"؟ وبالتالي فإن الشيء هو أنه في كل مرة يتم تحديث مجالنا يتم إعادة تقديم 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 مرة أخرى.

لا يوجد سوى تطبيق واحد Cell
وكان سريعًا للغاية ، لكن هناك أيضًا تطبيقًا App
استغرق بعض الوقت. الشيء هو أنه مع كل إعادة تقديم App
كل Cell
مقارنة الدعائم الجديدة مع الدعائم السابقة. حتى إذا قررت عدم تقديم (وهو بالضبط حالتنا) ، فإن هذه المقارنة لا تزال تستغرق بعض الوقت. يا (1) ، ولكن هذا (1) يحدث size
* size
مرات!
الخطوة 2: تحريكه لأسفل
ما الذي يمكننا القيام به للتغلب عليه؟ إذا كان تقديم App
يكلفنا كثيرًا ، فيتعين علينا إيقاف تقديم App
. لا يمكن ذلك إذا useState
استضافة حالتنا في App
باستخدام useState
، لأن هذا هو بالضبط ما يعيد useState
. لذلك يتعين علينا تحريك حالتنا لأسفل والسماح لكل Cell
بالاشتراك في الحالة بمفردها.
لنقم بإنشاء فصل مخصص سيكون حاوية لحالتنا.
class Field { constructor(fieldSize) { this.size = fieldSize
ثم قد يبدو تطبيقنا كالتالي:
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) }
الآن يمكننا الاشتراك في التحديثات الميدانية.
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.

كما نرى الآن ، يتم تقديم Cell
فقط وهي سريعة الجنون.
ماذا لو قلت الآن أن رمز Cell
هو سبب محتمل لتسرب الذاكرة؟ كما ترون ، في useEffect
، useEffect
في تحديثات الخلايا ، لكننا لا useEffect
اشتراكك مطلقًا. وهذا يعني أنه حتى عندما يتم تدمير Cell
، يستمر اشتراكها. دعنا نغير ذلك.
أولاً ، نحتاج إلى تعليم Field
ما يعنيه إلغاء الاشتراك.
class Field {
الآن يمكننا تطبيق 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 = () => {
الآن يمكننا أن نستهلك 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
نأمل أن تكون قد وجدت شيئًا مفيدًا لمشروعك. لا تتردد في توصيل ملاحظاتك لي! بالتأكيد أقدر أي نقد وأسئلة.