تمرير الدوال إلى المكونات
كيف يمكنني تمرير مُعالِج أحداث (مثل onClick
) إلى المكوّن؟
مرِّر مُعالِجات الأحداث والدوال الأخرى كخاصيّات props
إلى المكوّنات الأبناء:
<button onClick={this.handleClick}>
إن احتجت إلى الوصول إلى المكوّن الأب في مُعالِج الأحداث فستحتاج إلى ربط الدالة إلى نسخة المكوّن (مشروحة بالتفصيل في القسم التالي).
كيف أربط الدالة إلى نسخة المكوّن؟
هنالك عدة طرق للتأكّد من أنّ الدوال تستطيع الوصول إلى خاصيّات المكوّن مثل this.props
و this.state
، بناءً على الصياغة وخطوات البناء التي تستخدمها.
الربط في الدالة البانية (ES2015)
class Foo extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('حدثت نقرة');
}
render() {
return <button onClick={this.handleClick}>انقر هنا</button>;
}
}
خاصيّات الصنف (اقتراح المرحلة 3)
class Foo extends Component {
// ملاحظة: هذه الصياغة تجريبية وليست معيارية بعد
handleClick = () => {
console.log('حدثت نقرة');
}
render() {
return <button onClick={this.handleClick}>انقر هنا</button>;
}
}
الربط في تابع التصيير render
class Foo extends Component {
handleClick() {
console.log('حدثت نقرة');
}
render() {
return <button onClick={this.handleClick.bind(this)}>انقر هنا</button>;
}
}
ملاحظة: يُؤدّي استخدام Function.prototype.bind
في التابع render
إلى إنشاء دالة جديدة في كل مرّة يُصيَّر فيها المكوّن، ممّا قد يؤثر على الأداء (للمزيد تابع في الأسفل).
استخدام الدوال السهميّة في تابع التصيير render
class Foo extends Component {
handleClick() {
console.log('حدثت نقرة');
}
render() {
return <button onClick={() => this.handleClick()}>انقر هنا</button>;
}
}
ملاحظة: يُؤدّي استخدام الدوال السهميّة في التابع render
إلى إنشاء دالة جديدة في كل مرّة يُصيَّر فيها المكوّن، ممّا قد يؤثر على الأداء (للمزيد تابع في الأسفل).
هل من الجيّد استخدام الدوال السهميّة في توابع التصيير؟
بشكلٍ عام نعم، لا مشكلة في ذلك، وهي عادةً الطريقة الأسهل لتمرير المُعامِلات إلى دوال ردود النداء.
إن كانت لديك مشاكل في الأداء، فحاول تحسينه عن طريق الطرق المشروحة في توثيق React.
لماذا من الضروري إجراء الربط أساسًا؟
في JavaScript لا تكون الشيفرتان التاليتان متساويتين:
obj.method();
var method = obj.method;
method();
تضمن توابع الربط عمل الشيفرة الثانية بنفس طريقة عمل الشيفرة الأولى.
تحتاج باستخدام React فقط إلى ربط التوابع التي تُمرِّرها إلى المكوّنات الأخرى. على سبيل المثال يُمرِّر <button onClick={this.handleClick}>
التابع this.handleClick
لذا تحتاج إلى ربطه، ولكن من غير الضروري ربط التابع render
أو توابع دورة حياة المكوّن، فنحن لا نُمرّرها إلى المكوّنات الأخرى.
يشرح هذا المنشور مفهوم الربط (binding) وكيفيّة عمل الدوال في JavaScript بالتفصيل.
لماذا تُستدعى الدالة لدي في كل مرّة يُصيَّر فيها المكوّن؟
تأكّد من عدم استدعاء الدالة عند تمريرها إلى المكوّن:
render() {
// خطأ: يُستدعى handleClick بدلًا من تمريره كمرجع
return <button onClick={this.handleClick()}>انقر هنا</button>
}
بدلًا من فعل ذلك مرِّر الدالة نفسها بدون أقواس:
render() {
// صحيح: تُمرّر الدالة handleClick كمرجع هنا
return <button onClick={this.handleClick}>انقر هنا</button>
}
كيف أمرّر مُعامِل إلى مُعالِج الأحداث أو رد النداء؟
تستطيع استخدام الدوال السهميّة من أجل الالتفاف حول مُعالِجات الأحداث وتمرير المُعامِلات:
<button onClick={() => this.handleClick(id)} />
يُكافِئ هذا استدعاء .bind
:
<button onClick={this.handleClick.bind(this, id)} />
مثال: تمرير المُعامِلات باستخدام الدوال السهميّة
const A = 65 // ASCII حرف
class Alphabet extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = {
justClicked: null,
letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
};
}
handleClick(letter) {
this.setState({ justClicked: letter });
}
render() {
return (
<div>
Just clicked: {this.state.justClicked}
<ul>
{this.state.letters.map(letter =>
<li key={letter} onClick={() => this.handleClick(letter)}>
{letter}
</li>
)}
</ul>
</div>
)
}
}
مثال: تمرير المُعامِلات باستخدام خاصيّات البيانات
بإمكانك بشكل بديل استخدام واجهات برمجة التطبيق في DOM لتزين البيانات التي تحتاجها من أجل مُعالِجات الأحداث. اتبع هذه الطريقة إن احتجت لضبط عدد كبير من العناصر أو كنتَ تمتلك شجرة تصيير تعتمد على اختبارات التساوي الخاصّة بالصنف React.PureComponent
:
const A = 65 // ASCII حرف
class Alphabet extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = {
justClicked: null,
letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
};
}
handleClick(e) {
this.setState({
justClicked: e.target.dataset.letter
});
}
render() {
return (
<div>
Just clicked: {this.state.justClicked}
<ul>
{this.state.letters.map(letter =>
<li key={letter} data-letter={letter} onClick={this.handleClick}>
{letter}
</li>
)}
</ul>
</div>
)
}
}
كيف أستطيع منع استدعاء الدالة بسرعة كبيرة أو مرات عديدة؟
إن كان لديك مُعالِج أحداث مثل onClick
أو onScroll
وكنتَ ترغب في منع إطلاق رد النداء بسرعة كبيرة، فتستطيع تحديد معدّل تنفيذ رد النداء. يُمكِن فعل ذلك باستخدام:
- تقنية الخنق (throttling): معاينة التغييرات بناءً على تردد معتمد على الوقت (باستخدام
_.throttle
). - منع الارتداد (debouncing): نشر التغييرات بعد مدّة زمنيّة معينة من عدم الفاعليّة (باستخدام
_.debounce
). - الخنق باستخدام
requestAnimationFrame
: معاينة التغييرات بناءً علىrequestAnimationFrame
(باستخدامraf-schd
).
انظر إلى هذا المخطط للمقارنة بين الدالتين throttle
و debounce
.
ملاحظة: تُزوّدنا الدوال _.debounce
، و _.throttle
، و raf-schd
بتابع للإلغاء cancel
لإلغاء ردود النداء المتأخرة. يجب إمّا استدعاء هذا التابع من خلال التابع componentWillUnmount
أو التحقق من أنّ المكون لا يزال موصولًا ضمن دالة التأخير.
الخنق (Throttle)
يمنع الخنق استدعاء الدالة أكثر من مرّة ضمن النافذة الزمنيّة المُعطاة. يخنق المثال التالي مُعالِج الأحداث click
لمنع استدعائه أكثر من مرّة في الثانية:
import throttle from 'lodash.throttle';
class LoadMoreButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleClickThrottled = throttle(this.handleClick, 1000);
}
componentWillUnmount() {
this.handleClickThrottled.cancel();
}
render() {
return <button onClick={this.handleClickThrottled}>تحميل المزيد</button>;
}
handleClick() {
this.props.loadMore();
}
}
منع الارتداد (Debounce)
يضمن منع الارتداد عدم تنفيذ الدالة حتى مرور فترة معينة من الوقت منذ آخر استدعاء لها. يكون هذا مفيدًا عندما يتوجب عليك إجراء بعض الحسابات المكلفة استجابةً لحدث قد ينتهي بسرعة (مثل النزول بالصفحة scroll أو أحداث لوحة المفاتيح). يمنع المثال التالي الارتداد في حقل إدخال نصي مع تأخير 250 ميلي ثانية:
import debounce from 'lodash.debounce';
class Searchbox extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.emitChangeDebounced = debounce(this.emitChange, 250);
}
componentWillUnmount() {
this.emitChangeDebounced.cancel();
}
render() {
return (
<input
type="text"
onChange={this.handleChange}
placeholder="ابحث..."
defaultValue={this.props.value}
/>
);
}
handleChange(e) {
// تجري React pooling على الأحداث
// لذا نقرأ القيمة قبل منع الارتداد
// بإمكاننا بشكل بديل استدعاء event.persist() وتمرير كامل الحدث
// للمزيد من المعلومات انظر https://wiki.hsoub.com/React/handling_events
this.emitChangeDebounced(e.target.value);
}
emitChange(value) {
this.props.onChange(value);
}
}
الخنق باستخدام requestAnimationFrame
إنّ requestAnimationFrame
هو عبارة عن طريقة لوضع الدالة في طابور لتنفيذه في المتصفح في الوقت المثالي لتحسين أداء التصيير. تُطلَق الدالة التي توضع في الطابور باستخدام requestAnimationFrame
في الإطار الزمني التالي. سيعمل المتصفح بجد لضمان الحصول على 60 إطار في الثانية (60 fps). إن لم يكن المتصفح قادرًا على ذلك فسيحدد عدد الإطارات في الثانية. على سبيل المثال قد يكون جهازك قادر على التعامل فقط مع 30 إطار بالثانية لذا ستحصل على 30 إطار بالثانية فقط. إنّ استخدام requestAnimationFrame
للخنق هو تقنية مفيدة تمنع من إجراء أكثر من 60 تحديث في الثانية. إن كنت تجري 100 تحديث في الثانية فسيؤدي ذلك إلى إنشاء عمل إضافي على المتصفح والذي لن يراه المستخدم على أية حال.
ملاحظة: استخدام هذه التقنية سيلتقط فقط آخر قيمة منشورة في الإطار. بإمكانك رؤية مثال حول كيفية عمل هذا الضبط من هنا.
import rafSchedule from 'raf-schd';
class ScrollListener extends React.Component {
constructor(props) {
super(props);
this.handleScroll = this.handleScroll.bind(this);
// إنشاء دالة جديدة لجدولة التحديثات
this.scheduleUpdate = rafSchedule(
point => this.props.onScroll(point)
);
}
handleScroll(e) {
// عند استقبال حدث scroll جدول تحديثًا
// إن استقبلنا الكثير من التحديثات ضمن الإطار فسننشر آخر قيمة فقط
this.scheduleUpdate({ x: e.clientX, y: e.clientY });
}
componentWillUnmount() {
// إلغاء أي تحديثات منتظرة بما أننا سنفصل المكون
this.scheduleUpdate.cancel();
}
render() {
return (
<div
style={{ overflow: 'scroll' }}
onScroll={this.handleScroll}
>
<img src="/my-huge-image.jpg" />
</div>
);
}
}
اختبار حدود معدل تحديث الإطار لديك
عند اختبار حدود معدل تحديث الإطار من المفيد امتلاك القدرة على تمرير الزمن بسرعة. إن كنت تستخدم jest بإمكانك استخدام محاكيات الوقت لتمرير الوقت بسرعة. إن كنت تستخدم الخنق عن طريق requestAnimationFrame
فهنالك الأداة raf-stub
مفيدة للتحكم بضبط تحريك الإطارات.