خلال عملي ، صادفت بشكل دوري حقيقة أن المطورين لا يفهمون دائمًا كيفية آلية نقل البيانات عبر الدعائم ، ولا سيما عمليات الاسترجاعات ، ولماذا يتم تحديث منتجات PureComponents الخاصة بهم كثيرًا.
لذلك ، في هذه المقالة سوف نفهم كيف يتم تمرير عمليات الاسترجاعات إلى React ، وكذلك مناقشة ميزات معالجات الأحداث.
TL ؛ د
- لا تتداخل مع JSX ومنطق العمل - هذا سيعقد تصور الكود.
- للحصول على تحسينات صغيرة ، يعمل معالج التخزين المؤقت في شكل classProperties للفئات أو استخدام useCallback للوظائف - فلن يتم عرض المكونات النقية باستمرار. يمكن أن يكون التخزين المؤقت الخاص برد الاتصال مفيدًا بحيث لا يتم إجراء دورات تحديث غير ضرورية عند نقلها إلى PureComponent.
- لا تنس أنك لم تحصل على حدث حقيقي في رد الاتصال ، ولكن حدث Syntetic. إذا خرجت من الوظيفة الحالية ، فلن تتمكن من الوصول إلى حقول هذا الحدث. ذاكرة التخزين المؤقت الحقول التي تحتاج إليها إذا كان لديك إغلاق غير متزامن.
الجزء 1. معالجات الأحداث ، التخزين المؤقت وتصور التعليمات البرمجية
يوفر React طريقة مناسبة إلى حد ما لإضافة معالجات الأحداث لعناصر html.
هذه واحدة من الأشياء الأساسية التي يمكن لأي مطور معرفتها عند بدء الكتابة في React:
class MyComponent extends Component { render() { return <button onClick={() => console.log('Hello world!')}>Click me</button>; } }
بسيطة بما فيه الكفاية؟ من هذا الرمز ، يصبح من الواضح على الفور ما سيحدث عندما ينقر المستخدم على الزر.
ولكن ماذا لو أصبح الكود الموجود في المعالج أكثر وأكثر؟
لنفترض أننا user.team === 'search-team'
على الزر لتحميل وتصفية كل شخص ليس في فريق معين ( user.team === 'search-team'
) ، ثم user.team === 'search-team'
حسب العمر.
class MyComponent extends Component { constructor(props) { super(props); this.state = { users: [] }; } render() { return ( <div> <ul> {this.state.users.map(user => ( <li>{user.name}</li> ))} </ul> <button onClick={() => { console.log('Hello world!'); window .fetch('/usersList') .then(result => result.json()) .then(data => { const users = data .filter(user => user.team === 'search-team') .sort((a, b) => { if (a.age > b.age) { return 1; } if (a.age < b.age) { return -1; } return 0; }); this.setState({ users: users, }); }); }} > Load users </button> </div> ); } }
هذا الرمز هو من الصعب جدا معرفة. يتم خلط رمز منطق العمل بالتخطيط الذي يراه المستخدم.
أسهل طريقة للتخلص من ذلك هي نقل الوظيفة إلى مستوى أساليب الفصل:
class MyComponent extends Component { fetchUsers() {
هنا نقلنا منطق العمل من كود JSX إلى حقل منفصل في فئتنا. لجعل هذا الوصول داخل الوظيفة ، حددنا رد الاتصال بهذه الطريقة: onClick={() => this.fetchUsers()}
بالإضافة إلى ذلك ، عند وصف الفصل ، يمكننا أن نعلن أن الحقل يمثل وظيفة السهم:
class MyComponent extends Component { fetchUsers = () => {
سيتيح لنا ذلك إعلان رد الاتصال كـ onClick={this.fetchUsers}
ما هو الفرق بين هاتين الطريقتين؟
onClick={this.fetchUsers}
- هنا ، مع كل مكالمة إلى وظيفة التجسيد في الدعائم ، سيتم دائمًا إرسال button
بنفس الرابط.
في حالة onClick={() => this.fetchUsers()}
، في كل مرة يتم استدعاء وظيفة التجسيد ، يقوم JavaScript بتهيئة وظيفة جديدة () => this.fetchUsers()
على onClick
prop. هذا يعني أن nextProp.onClick
و prop.onClick
على button
في هذه الحالة لن تكون دائمًا متساوية ، وحتى إذا تم تمييز المكون على أنه نظيف ، فسيتم تقديمه.
ماذا يهدد هذا بالتنمية؟
في معظم الحالات ، لن تلاحظ انخفاضًا في الأداء بصريًا ، لأن DOM الظاهري التي سيتم إنشاؤها بواسطة المكون لن تختلف عن سابقتها ، ولن تكون هناك تغييرات في DOM الخاص بك.
ومع ذلك ، إذا قمت بتقديم قوائم كبيرة من المكونات أو الجداول ، فستلاحظ "الفرامل" على كمية كبيرة من البيانات.
لماذا فهم كيف يتم نقل وظيفة إلى رد الاتصال المهم؟
في الغالب على تويتر أو على stackoverflow ، يمكنك الاطلاع على هذه النصائح:
"إذا كانت لديك مشكلات في الأداء مع تطبيقات React ، فحاول استبدال الوراثة من Component بـ PureComponent. وتذكر أيضًا أنه بالنسبة للمكون ، يمكنك دائمًا تعريف shouldComponentUpdate للتخلص من حلقات التحديث غير الضرورية."
إذا قمنا بتعريف أحد المكونات على أنه Pure ، فهذا يعني أنه يحتوي بالفعل على دالة shouldComponentUpdate
التي تقوم بالضحلة بين الدعائم و nextProps.
بتمرير وظيفة رد اتصال جديدة إلى مثل هذا المكون في كل مرة ، نفقد جميع مزايا وتحسينات PureComponent
.
لنلقِ نظرة على مثال.
قم بإنشاء مكون إدخال يعرض أيضًا معلومات عن عدد المرات التي تم تحديثه فيها:
class Input extends PureComponent { renderedCount = 0; render() { this.renderedCount++; return ( <div> <input onChange={this.props.onChange} /> <p>Input component was rerendered {this.renderedCount} times</p> </div> ); } }
لنقم بإنشاء مكونين من شأنه أن يجعل الإدخال داخليًا:
class A extends Component { state = { value: '' }; onChange = e => { this.setState({ value: e.target.value }); }; render() { return ( <div> <Input onChange={this.onChange} /> <p>The value is: {this.state.value} </p> </div> ); } }
والثاني:
class B extends Component { state = { value: '' }; onChange(e) { this.setState({ value: e.target.value }); } render() { return ( <div> <Input onChange={e => this.onChange(e)} /> <p>The value is: {this.state.value} </p> </div> ); } }
يمكنك تجربة المثال بيديك هنا: https://codesandbox.io/s/2vwz6kjjkr
يوضح هذا المثال كيف يمكنك فقد كافة فوائد PureComponent إذا قمت بتمرير وظيفة رد اتصال جديدة إلى PureComponent في كل مرة.
الجزء 2. استخدام معالجات الأحداث في مكونات الوظيفة
في الإصدار الجديد من React (16.8) ، تم الإعلان عن آلية React hooks ، والتي تسمح لك بكتابة مكونات وظيفية كاملة ، مع دورة حياة واضحة يمكن أن تغطي جميع حالات المستخدمين تقريبًا حتى الآن فقط الطبقات المغطاة.
نقوم بتعديل المثال باستخدام مكون الإدخال بحيث يتم تمثيل جميع المكونات بواسطة دالة والعمل مع خطافات التفاعل.
يجب أن يخزن الإدخال داخل نفسه معلومات حول عدد المرات التي تم تغييرها. إذا استخدمنا حقلًا في مثيلنا في حالة الفئات ، تم تنفيذ الوصول إليه من خلال ذلك ، ثم في حالة دالة لن نتمكن من إعلان متغير من خلال هذا.
يوفر React خطاف useRef الذي يمكن استخدامه لحفظ مرجع إلى HtmlElement في شجرة DOM ، ولكنه مثير أيضًا لأنه يمكن استخدامه للبيانات المنتظمة التي يحتاجها مكوننا:
import React, { useRef } from 'react'; export default function Input({ onChange }) { const componentRerenderedTimes = useRef(0); componentRerenderedTimes.current++; return ( <> <input onChange={onChange} /> <p>Input component was rerendered {componentRerenderedTimes.current} times</p> </> ); }
نحتاج أيضًا إلى أن يكون المكون "نظيفًا" ، أي أنه يتم تحديثه فقط إذا تغيرت الدعائم التي تم تمريرها إلى المكون.
لهذا الغرض ، هناك مكتبات مختلفة توفر HOC ، لكن من الأفضل استخدام وظيفة المذكرة ، التي تم إنشاؤها بالفعل في React ، لأنها تعمل بشكل أسرع وأكثر كفاءة:
import React, { useRef, memo } from 'react'; export default memo(function Input({ onChange }) { const componentRerenderedTimes = useRef(0); componentRerenderedTimes.current++; return ( <> <input onChange={onChange} /> <p>Input component was rerendered {componentRerenderedTimes.current} times</p> </> ); });
مكون الإدخال جاهز ، والآن نعيد كتابة المكونين A و B.
في حالة المكون B ، من السهل القيام بذلك:
import React, { useState } from 'react'; function B() { const [value, setValue] = useState(''); return ( <div> <Input onChange={e => setValue(e.target.value)} /> <p>The value is: {value} </p> </div> ); }
استخدمنا هنا useState
ربط useState
، والذي يسمح لك بالحفظ والعمل مع حالة المكون ، في حالة تمثيل المكون بواسطة دالة.
كيف يمكننا تخزين وظيفة رد الاتصال؟ لا يمكننا إزالته من المكون ، لأنه في هذه الحالة سيكون مشتركًا في مثيلات مختلفة للمكون.
بالنسبة لمثل هذه المهام ، يحتوي React على مجموعة من السنانير للتخزين المؤقت والذاكرة ، والتي يعد useCallback
هو الأنسب useCallback
https://reactjs.org/docs/hooks-reference.html
أضف هذا الخطاف إلى المكون A
:
import React, { useState, useCallback } from 'react'; function A() { const [value, setValue] = useState(''); const onChange = useCallback(e => setValue(e.target.value), []); return ( <div> <Input onChange={onChange} /> <p>The value is: {value} </p> </div> ); }
قمنا بالتخزين المؤقت للوظيفة ، مما يعني أنه لن يتم تحديث مكون الإدخال في كل مرة.
كيف يعمل useCallback
هوك useCallback
؟
إرجاع هذا الخطاف دالة مخزنة مؤقتًا (على سبيل المثال ، لا يتغير الارتباط من التجسيد إلى التجسيد).
بالإضافة إلى الوظيفة التي سيتم تخزينها مؤقتًا ، يتم تمرير وسيطة ثانية إليها - صفيف فارغ.
يسمح لك هذا الصفيف بنقل قائمة من الحقول ، عند تغيير ما تحتاجه لتغيير الوظيفة ، أي إرجاع رابط جديد.
useCallback
أن ترى الفرق بين الطريقة المعتادة لنقل وظيفة إلى رد useCallback
واستخدام useCallback
هنا: https://codesandbox.io/s/0y7wm3pp1w
لماذا نحتاج إلى مجموعة؟
لنفترض أننا بحاجة إلى تخزين وظيفة تعتمد على بعض القيمة من خلال الإغلاق:
import React, { useCallback } from 'react'; import ReactDOM from 'react-dom'; import './styles.css'; function App({ a, text }) { const onClick = useCallback(e => alert(a), [ ]); return <button onClick={onClick}>{text}</button>; } const rootElement = document.getElementById('root'); ReactDOM.render(<App text={'Click me'} a={1} />, rootElement);
هنا ، يعتمد مكون التطبيق على prop a
. إذا قمت بتشغيل المثال ، فسيعمل كل شيء بشكل صحيح حتى اللحظة التي نضيفها إلى النهاية:
setTimeout(() => ReactDOM.render(<App text={'Next A'} a={2} />, rootElement), 5000);
بعد تشغيل المهلة ، عند النقر فوق الزر في حالة تأهب ، سيتم عرض 1
. يحدث هذا لأننا حفظنا الوظيفة السابقة ، والتي أغلقت متغير. ونظرًا لأن المتغير متغير ، وهو في حالتنا نوع قيمة ، ونوع القيمة غير قابل للتغيير ، فقد حصلنا على هذا الخطأ. إذا أزلنا التعليق /*a*/
، فسيعمل الرمز بشكل صحيح. سيؤدي الرد على التجسيد الثاني إلى التحقق من اختلاف البيانات التي تم تمريرها في الصفيف وإرجاع وظيفة جديدة.
يمكنك تجربة هذا المثال بنفسك هنا: https://codesandbox.io/s/6vo8jny1ln
يوفر React العديد من الوظائف التي تسمح لك بتدوين البيانات ، مثل useRef
و useCallback
و useMemo
.
إذا كانت هذه الأخيرة ضرورية لتدوين قيمة الوظيفة ، وكانت متشابهة تمامًا مع useRef
، فإن useRef
يسمح لك بتخزين ليس فقط مراجع لعناصر DOM ، ولكن أيضًا كحقل مثيل.
للوهلة الأولى ، يمكن استخدامه لوظائف التخزين المؤقت ، لأن useRef
أيضًا بتخزين البيانات مؤقتًا بين تحديثات مكونات منفصلة.
ومع ذلك ، استخدام useRef
إلى وظائف التخزين المؤقت غير مرغوب فيه. إذا كانت وظيفتنا تستخدم الإغلاق ، في أي تجسيد ، يمكن أن تتغير القيمة المغلقة ، وستعمل الوظيفة المخبأة لدينا مع القيمة القديمة. هذا يعني أننا سنحتاج إلى كتابة منطق تحديث الوظيفة أو مجرد استخدام useCallback
، والذي يتم تنفيذه بسبب آلية التبعية.
https://codesandbox.io/s/p70pprpvvx هنا يمكنك أن ترى تحفيظ الوظائف مع useCallback
الصحيح ، مع الخطأ ومع useRef
.
الجزء 3. الأحداث النحوية
لقد توصلنا بالفعل إلى كيفية استخدام معالجات الأحداث وكيفية العمل بشكل صحيح مع عمليات الإغلاق في عمليات الاسترجاعات ، ولكن في React يوجد فرق مهم آخر عند العمل معهم:
ملاحظة: الآن Input
، الذي عملنا أعلاه ، متزامن تمامًا ، ولكن في بعض الحالات قد يكون من الضروري أن يحدث رد الاتصال مع تأخير ، وفقًا لنمط الرفض أو الاختناق . لذلك ، debounce ، على سبيل المثال ، مناسب جدًا للاستخدام لإدخال سلسلة البحث - لن يحدث البحث إلا عندما يتوقف المستخدم عن كتابة الأحرف.
قم بإنشاء مكون داخليًا يؤدي إلى تغيير الحالة:
function SearchInput() { const [value, setValue] = useState(''); const timerHandler = useRef(); return ( <> <input defaultValue={value} onChange={e => { clearTimeout(timerHandler.current); timerHandler.current = setTimeout(() => { setValue(e.target.value); }, 300);
هذا الكود لن يعمل. والحقيقة هي أن React يقوم بتوكيل الأحداث داخل نفسه ، ويحدث ما يسمى Syntetic Event في رد الاتصال onChange الخاص بنا ، والذي بعد "سيتم مسح" وظائفنا (ستكون الحقول خالية). لأسباب تتعلق بالأداء ، يقوم React بذلك لاستخدام كائن واحد ، بدلاً من إنشاء كائن جديد في كل مرة.
إذا احتجنا أن نأخذ قيمة ، كما في هذا المثال ، فهذا يكفي لتخزين الحقول الضرورية قبل الخروج من الوظيفة:
function SearchInput() { const [value, setValue] = useState(''); const timerHandler = useRef(); return ( <> <input defaultValue={value} onChange={e => { clearTimeout(timerHandler.current); const pendingValue = e.target.value; // cached! timerHandler.current = setTimeout(() => { setValue(pendingValue); }, 300);
يمكنك مشاهدة مثال هنا: https://codesandbox.io/s/oj6p8opq0z
في حالات نادرة جدًا ، يصبح من الضروري الحفاظ على مثيل الحدث بأكمله. للقيام بذلك ، يمكنك استدعاء event.persist()
، الذي يزيل
هذا مثيل الحدث Syntetic من تجمع الأحداث من أحداث رد الفعل.
الخلاصة:
تعتبر معالجات الأحداث التفاعلية مريحة للغاية لأنها:
- أتمتة الاشتراك وإلغاء الاشتراك (مع مكون إلغاء التثبيت) ؛
- تبسيط تصور الكود ، من السهل تتبع معظم الاشتراكات في كود JSX.
ولكن في نفس الوقت ، عند تطوير التطبيقات ، قد تواجه بعض الصعوبات:
- التغلب على عمليات الاسترجاعات في الدعائم ؛
- الأحداث النحوية التي يتم محوها بعد تنفيذ الوظيفة الحالية.
عادةً ما لا يتم تجاوز عمليات الاستدعاء المرتدة ، نظرًا لأن vDOM لا يتغير ، لكن تجدر الإشارة إلى أنه إذا قمت بتقديم تحسينات أو استبدال مكونات Pure من خلال الميراث من PureComponent
أو باستخدام memo
، فيجب عليك العناية بالتخزين المؤقت لها ، وإلا فلن تكون فوائد تقديم PureComponents أو المذكرة ملحوظة. للتخزين المؤقت ، يمكنك استخدام classProperties (عند العمل مع فصل useCallback
) أو useCallback
hook (عند العمل مع الوظائف).
للتشغيل غير المتزامن الصحيح ، إذا كنت بحاجة إلى بيانات من حدث ما ، فقم أيضًا بتخزين الحقول التي تحتاج إليها.