تحتوي العديد من إطارات JavaScript الأمامية (مثل Angular و React و Vue) على أنظمة تفاعلية خاصة بها. إن فهم ميزات هذه الأنظمة سيكون مفيدًا لأي مطور ، وسوف يساعده على استخدام أطر JS الحديثة بكفاءة أكبر.

تُظهر المادة ، التي ننشر ترجمتها اليوم ، مثالًا خطوة بخطوة لتطوير نظام تفاعل في JavaScript خالص. يطبق هذا النظام نفس الآليات المستخدمة في Vue.
نظام رد الفعل
بالنسبة لشخص واجه لأول مرة نظام تفاعل Vue ، قد يبدو وكأنه صندوق أسود غامض. فكر في تطبيق Vue بسيط. إليك الترميز:
<div id="app"> <div>Price: ${{ price }}</div> <div>Total: ${{ price*quantity }}</div> <div>Taxes: ${{ totalPriceWithTax }}</div> </div>
هنا هو أمر اتصال الإطار ورمز التطبيق.
<script src="https://cdn.jsdelivr.net/npm/vue"></script> <script> var vm = new Vue({ el: '#app', data: { price: 5.00, quantity: 2 }, computed: { totalPriceWithTax() { return this.price * this.quantity * 1.03 } } }) </script>
بطريقة ما ، اكتشف Vue أنه عندما يتغير
price
، يحتاج المحرك إلى القيام بثلاثة أشياء:
- تحديث قيمة
price
على صفحة الويب. - إعادة حساب التعبير الذي يتم ضرب
price
فيه quantity
، وعرض القيمة الناتجة على الصفحة. - استدعاء الدالة
totalPriceWithTax
، ومرة أخرى ، ضع ما يعود على الصفحة.
يظهر ما يحدث هنا في الرسم التوضيحي التالي.
كيف تعرف Vue ماذا تفعل عندما يتغير سعر العقار؟لدينا الآن أسئلة حول كيفية معرفة Vue لما يجب تحديثه بالضبط عندما يتغير
price
، وكيف يتتبع المحرك ما يحدث على الصفحة. ما يمكنك ملاحظته هنا لا يبدو وكأنه تطبيق JS عادي.
ربما لم يكن هذا واضحًا بعد ، ولكن المشكلة الرئيسية التي نحتاج إلى حلها هنا هي أن برامج JS لا تعمل عادةً بهذه الطريقة. على سبيل المثال ، لنقم بتشغيل التعليمات البرمجية التالية:
let price = 5 let quantity = 2 let total = price * quantity
ما رأيك سيتم عرضه في وحدة التحكم؟ نظرًا لعدم استخدام أي شيء هنا باستثناء JS العادي ، فسيصل
10
إلى وحدة التحكم.
نتيجة البرنامجوعند استخدام إمكانات Vue ، في وضع مماثل ، يمكننا تنفيذ سيناريو يتم فيه إعادة حساب القيمة
total
عند تغير متغيرات
price
أو
quantity
. أي أنه إذا تم استخدام نظام التفاعل في تنفيذ الرمز أعلاه ، فلن يتم عرض 10 ، ولكن 40 على وحدة التحكم:
خرج وحدة التحكم عن طريق كود افتراضي باستخدام نظام تفاعلجافا سكريبت هي لغة يمكن أن تعمل على حد سواء من الناحية الإجرائية والموجهة للكائنات ، ولكنها لا تحتوي على نظام تفاعل مدمج ، لذلك فإن الرمز الذي أخذنا بعين الاعتبار عند تغيير
price
لن ينتج الرقم 40 إلى وحدة التحكم. من أجل إعادة حساب المؤشر
total
عندما يتغير
price
أو
quantity
، سنحتاج إلى إنشاء نظام تفاعل بمفردنا وبالتالي تحقيق السلوك الذي نحتاجه. سنكسر الطريق إلى هذا الهدف إلى عدة خطوات صغيرة.
المهمة: تخزين قواعد حساب المؤشرات
نحتاج إلى مكان ما لحفظ المعلومات حول كيفية حساب المؤشر
total
، مما سيسمح لنا بإعادة حسابه عند تغيير قيم متغيرات
price
أو
quantity
.
▍حل
أولاً ، نحتاج إلى إخبار التطبيق بما يلي: "هذا هو الرمز الذي سأقوم بتشغيله ، احفظه ، قد أحتاج إلى تنفيذه مرة أخرى." ثم سنحتاج إلى تشغيل التعليمات البرمجية. في وقت لاحق ، إذا تغيرت مؤشرات
price
أو
quantity
، فستحتاج إلى الاتصال بالرمز المحفوظ لإعادة حساب
total
. يبدو هذا:
يجب حفظ رمز الحساب الإجمالي في مكان ما حتى تتمكن من الوصول إليه لاحقًايتم تنسيق الرمز الذي يمكنك استدعاؤه في جافا سكريبت لتنفيذ بعض الإجراءات كوظائف. لذلك ، سنكتب دالة تتعامل مع حساب
total
، وسننشئ أيضًا آلية لتخزين الوظائف التي قد نحتاجها لاحقًا.
let price = 5 let quantity = 2 let total = 0 let target = null target = function () { total = price * quantity } record()
لاحظ أننا نقوم بتخزين الوظيفة المجهولة في المتغير
target
، ومن ثم استدعاء وظيفة التسجيل. سنتحدث عنه أدناه. أود أيضًا أن أشير إلى أنه يمكن إعادة كتابة الوظيفة
target
باستخدام صيغة دالات السهم ES6 على النحو التالي:
target = () => { total = price * quantity }
فيما يلي تعريف وظيفة التسجيل وهيكل البيانات المستخدم لتخزين الوظائف:
let storage = [] // target function record () { // target = () => { total = price * quantity } storage.push(target) }
باستخدام وظيفة التسجيل ، نقوم بحفظ الوظيفة
target
(في حالتنا
{ total = price * quantity }
) في مصفوفة
storage
، مما يسمح لنا باستدعاء هذه الوظيفة لاحقًا ، ربما باستخدام وظيفة
replay
، والتي يظهر رمزها أدناه. سيسمح لنا هذا باستدعاء جميع الوظائف المخزنة في
storage
.
function replay () { storage.forEach(run => run()) }
هنا نراجع جميع الوظائف المجهولة المخزنة في مصفوفة
storage
وننفذ كل واحدة منها.
ثم في الكود الخاص بنا يمكننا القيام بما يلي:
price = 20 console.log(total) // 10 replay() console.log(total) // 40
لا يبدو الأمر بهذه الصعوبة ، أليس كذلك؟ إليك الشفرة بأكملها ، التي ناقشنا أجزاء منها أعلاه ، في حالة أنه من الملائم لك التعامل معها أخيرًا. بالمناسبة ، لا تتم كتابة هذا الرمز عن طريق الخطأ بهذه الطريقة.
let price = 5 let quantity = 2 let total = 0 let target = null let storage = [] function record () { storage.push(target) } function replay () { storage.forEach(run => run()) } target = () => { total = price * quantity } record() target() price = 20 console.log(total)
هذا ما سيتم عرضه في وحدة تحكم المتصفح بعد بدء تشغيله.
نتيجة الرمزالتحدي: حل موثوق لتخزين الوظائف
يمكننا الاستمرار في تدوين الوظائف التي نحتاجها عندما يصبح ذلك ضروريًا ، ولكن سيكون من الجيد إذا كان لدينا حل أكثر موثوقية يمكن تطويره باستخدام التطبيق. ربما ستكون فئة تحتفظ بقائمة من الوظائف المكتوبة في الأصل إلى المتغير
target
، والتي تتلقى إشعارات إذا كنا بحاجة إلى إعادة تنفيذ هذه الوظائف.
olutionالحل: فئة التبعية
أحد الطرق لحل المشكلة المذكورة أعلاه هو تغليف السلوك الذي نحتاجه في الفصل ، والذي يمكن أن يسمى التبعية. ستطبق هذه الفئة نمط برمجة المراقب القياسي.
ونتيجة لذلك ، إذا أنشأنا فئة JS تُستخدم لإدارة تبعياتنا (والتي ستكون قريبة من كيفية تطبيق آليات مماثلة في Vue) ، فقد تبدو كما يلي:
class Dep { // Dep - Dependency constructor () { this.subscribers = [] // , // notify() } depend () { // record if (target && !this.subscribers.includes(target)){ // target // this.subscribers.push(target) } } notify () { // replay this.subscribers.forEach(sub => sub()) // - } }
يرجى ملاحظة أنه بدلاً من صفيف
storage
، نقوم الآن بتخزين وظائفنا المجهولة في صفيف
subscribers
. بدلاً من وظيفة التسجيل ، تسمى الطريقة التابعة الآن. أيضًا هنا ، بدلاً من وظيفة
replay
، يتم
notify
وظيفة الإعلام. إليك كيفية تشغيل الكود الخاص بنا باستخدام فئة
Dep
:
const dep = new Dep() let price = 5 let quantity = 2 let total = 0 let target = () => { total = price * quantity } dep.depend()
يعمل رمزنا الجديد بالطريقة نفسها كما كان من قبل ، ولكنه الآن مصمم بشكل أفضل ، ويبدو أنه أفضل لإعادة الاستخدام.
الشيء الوحيد الذي يبدو غريباً فيه حتى الآن هو العمل مع وظيفة مخزنة في المتغير
target
.
المهمة: آلية لإنشاء وظائف مجهولة
في المستقبل ، سنحتاج إلى إنشاء كائن من فئة
Dep
لكل متغير. بالإضافة إلى ذلك ، سيكون من اللطيف تغليف سلوك إنشاء وظائف مجهولة في مكان ما ، والذي يجب استدعاؤه عند تحديث البيانات ذات الصلة. ربما سيساعدنا ذلك في وظيفة إضافية ، والتي
watcher
عليها اسم
watcher
. سيؤدي هذا إلى حقيقة أنه يمكننا استبدال هذا البناء من المثال السابق بوظيفة جديدة:
let target = () => { total = price * quantity } dep.depend() target()
في الواقع ، سيبدو استدعاء وظيفة
watcher
الذي يحل محل هذا الرمز كما يلي:
watcher(() => { total = price * quantity })
▍ الحل: وظيفة مراقب
داخل وظيفة
watcher
، التي يتم تقديم رمزها أدناه ، يمكننا تنفيذ العديد من الإجراءات البسيطة:
function watcher(myFunc) { target = myFunc // target myFunc dep.depend() // target target() // target = null // target }
كما ترى ، تأخذ وظيفة
watcher
، كوسيطة ، وظيفة
myFunc
،
myFunc
إلى المتغير العام
target
، وتستدعي
dep.depend()
لإضافة هذه الوظيفة إلى قائمة المشتركين ، وتستدعي هذه الوظيفة
dep.depend()
تعيين المتغير
target
.
الآن نحصل على نفس القيم 10 و 40 إذا قمنا بتنفيذ الكود التالي:
price = 20 console.log(total) dep.notify() console.log(total)
ربما تتساءل لماذا قمنا بتنفيذ
target
كمتغير عالمي ، بدلاً من تمرير هذا المتغير إلى وظائفنا ، إذا لزم الأمر. لدينا سبب وجيه للقيام بذلك ، وبعد ذلك سوف تفهم.
المهمة: كائن تابع خاص لكل متغير
لدينا كائن واحد من فئة الدرجة. ماذا لو احتجنا إلى أن يكون لكل متغير لدينا كائن فئة
Dep
الخاص به؟ قبل المتابعة ، دعنا ننقل البيانات التي نعمل معها إلى خصائص الكائن:
let data = { price: 5, quantity: 2 }
تخيل للحظة أن كل من خصائصنا (
price
quantity
) لها كائن فئة تابع خاص بها.
خصائص السعر والكميةالآن يمكننا استدعاء وظيفة
watcher
النحو التالي:
watcher(() => { total = data.price * data.quantity })
نظرًا لأننا نعمل هنا مع قيمة خاصية
data.price
، فنحن بحاجة إلى كائن فئة
Dep
لخاصية
price
لوضع دالة مجهولة (مخزنة في
target
) في صفيف المشتركين (عن طريق استدعاء
dep.depend()
). بالإضافة إلى ذلك ، نظرًا لأننا نعمل مع
data.quantity
، فإننا نحتاج إلى كائن
Dep
الخاص
data.quantity
quantity
لوضع دالة مجهولة (مرة أخرى ، مخزنة في
target
) في صفيف المشتركين.
إذا كنت تصور هذا في شكل رسم بياني ، ستحصل على ما يلي.
تقع الدالات في صفائف المشتركين لكائنات فئة Dep المقابلة لخصائص مختلفةإذا كان لدينا وظيفة أخرى مجهولة تعمل فيها فقط مع خاصية
data.price
، فيجب أن تنتقل الوظيفة المجهولة المقابلة فقط إلى مستودع كائن هذه الخاصية.
يمكن إضافة مراقبين إضافيين إلى واحدة فقط من الخصائص المتاحة.متى قد تحتاج إلى استدعاء
dep.notify()
للوظائف المشتركة في التغييرات في خاصية
price
؟ ستكون هناك حاجة عند تغيير
price
. هذا يعني أنه عندما يكون مثالنا جاهزًا تمامًا ، يجب أن تعمل الشفرة التالية بالنسبة لنا.
هنا ، عند تغيير السعر ، تحتاج إلى استدعاء dep.notify () لجميع الوظائف المشتركة في تغيير السعرلكي يعمل كل شيء بهذه الطريقة ، نحتاج إلى طريقة لاعتراض أحداث الوصول إلى الممتلكات (في حالتنا ،
price
أو
quantity
). سيسمح هذا ، عند حدوث ذلك ، بحفظ الوظيفة
target
في صفيف من المشتركين ، وعندما يتغير المتغير المقابل ، لتنفيذ الوظيفة المخزنة في هذا الصفيف.
olution الحل: Object.defineProperty ()
الآن نحن بحاجة للتعرف على طريقة ES5 القياسية
Object.defineProperty (). يسمح لك بتعيين الحروف والمستوطنين لخصائص الأشياء. اسمحوا لي ، قبل أن ننتقل إلى استخدامها العملي ، أن أوضح تشغيل هذه الآليات بمثال بسيط.
let data = { price: 5, quantity: 2 } Object.defineProperty(data, 'price', { // price get() { // console.log(`I was accessed`) }, set(newVal) { // console.log(`I was changed`) } }) data.price // data.price = 20 //
إذا قمت بتشغيل هذا الرمز في وحدة تحكم المستعرض ، فسيعرض النص التالي.
نتائج أفضل وأكثركما ترى ، يطبع مثالنا ببساطة سطرين من النص إلى وحدة التحكم. ومع ذلك ، فإنه لا يقرأ القيم أو يعينها ، حيث قمنا بإعادة تعريف الوظائف القياسية للرسائل والمستوطنين. سنستعيد وظيفة هذه الأساليب. وبالتحديد ، من المتوقع أن ترجع الحروف قيم الأساليب المقابلة ، ويقوم المستوطنون بتعيينها. لذلك ، سنضيف متغيرًا جديدًا ، القيمة
internalValue
، إلى الرمز ، والذي سنستخدمه لتخزين قيمة
price
الحالية.
let data = { price: 5, quantity: 2 } let internalValue = data.price // Object.defineProperty(data, 'price', { // price get() { // console.log(`Getting price: ${internalValue}`) return internalValue }, set(newVal) { console.log(`Setting price to: ${newVal}`) internalValue = newVal } }) total = data.price * data.quantity // data.price = 20 //
الآن بعد أن بدأ المبتدئ والمضخم في العمل بالطريقة التي يجب أن يعملوا بها ، ما الذي تعتقد أنه سيدخل إلى وحدة التحكم عند تنفيذ هذا الرمز؟ نلقي نظرة على الشكل التالي.
إخراج البيانات إلى وحدة التحكملذلك ، لدينا الآن آلية تسمح لك بتلقي الإخطارات عند قراءة قيم الملكية وعندما تكتب لها قيم جديدة. الآن ، بعد إعادة صياغة الكود قليلاً ، يمكننا تجهيز المحارف والمستوطنين بكل خصائص كائن
data
. سنستخدم هنا طريقة
Object.keys()
، التي تُرجع مجموعة من مفاتيح الكائن التي تم تمريرها إليها.
let data = { price: 5, quantity: 2 } Object.keys(data).forEach(key => { // data let internalValue = data[key] Object.defineProperty(data, key, { get() { console.log(`Getting ${key}: ${internalValue}`) return internalValue }, set(newVal) { console.log(`Setting ${key} to: ${newVal}`) internalValue = newVal } }) }) let total = data.price * data.quantity data.price = 20
الآن كل خصائص كائن
data
لها gems والمستوطنين. هذا ما يظهر في وحدة التحكم بعد تشغيل هذا الرمز.
إخراج البيانات إلى وحدة التحكم عن طريق getters والمستوطنينتجميع نظام التفاعل
عندما يتم تنفيذ جزء التعليمات البرمجية مثل
total = data.price * data.quantity
قيمة خاصية
price
، نحتاج إلى خاصية
price
"لتذكر" الوظيفة المجهولة المقابلة (
target
في حالتنا). ونتيجة لذلك ، إذا تم تغيير خاصية
price
، أي تعيينها إلى قيمة جديدة ، فسيؤدي ذلك إلى استدعاء هذه الوظيفة لتكرار العمليات التي تقوم بها ، لأنها تعرف أن سطرًا معينًا من التعليمات البرمجية يعتمد عليها. ونتيجة لذلك ، يمكن تخيل العمليات التي يتم إجراؤها في المراسلات والمستوطنين على النحو التالي:
- الأفضل - تحتاج إلى تذكر الوظيفة المجهولة ، والتي سنستدعيها مرة أخرى عندما تتغير القيمة.
- Setter - من الضروري تنفيذ الوظيفة المجهولة المخزنة ، مما سيؤدي إلى تغيير في القيمة الناتجة المقابلة.
إذا كنت تستخدم فئة
Dep
المعروفة لك بالفعل في هذا الوصف ، فستحصل على ما يلي:
- عند قراءة قيمة خاصية ، يتم استدعاء
dep.depend()
لحفظ دالة target
الحالية. - عندما تتم كتابة قيمة إلى خاصية ، يتم استدعاء
dep.notify()
لإعادة تشغيل كافة الوظائف المخزنة.
سنقوم الآن بدمج هذين الفكرتين ، وأخيرًا ، سنصل إلى الشفرة التي تسمح لنا بتحقيق هدفنا.
let data = { price: 5, quantity: 2 } let target = null // - , class Dep { constructor () { this.subscribers = [] } depend () { if (target && !this.subscribers.includes(target)){ this.subscribers.push(target) } } notify () { this.subscribers.forEach(sub => sub()) } } // , // Object.keys(data).forEach(key => { let internalValue = data[key] // // Dep const dep = new Dep() Object.defineProperty(data, key, { get() { dep.depend() // target return internalValue }, set(newVal) { internalValue = newVal dep.notify() // } }) }) // watcher dep.depend(), // function watcher(myFunc){ target = myFunc target() target = null } watcher(() => { data.total = data.price * data.quantity })
لنجرب هذا الرمز في وحدة تحكم المتصفح.
تجارب كود جاهزةكما ترون ، إنه يعمل تمامًا كما نحتاج! أصبحت خصائص
price
quantity
تفاعلية! كل الكود المسؤول عن توليد
total
عندما يتم تنفيذ تغييرات
price
أو
quantity
بشكل متكرر.
الآن ، بعد أن قمنا بكتابة نظام التفاعل الخاص بنا ، سيبدو هذا الرسم التوضيحي من وثائق Vue مألوفًا ومفهومًا لك.
نظام تفاعل Vueشاهد هذه الدائرة الارجوانية الجميلة التي تقول
التي تحتوي على الحروف والمستوطنين؟ الآن يجب أن يكون مألوفًا لك. يحتوي كل مثيل للمكون على مثيل لطريقة المراقبة (الدائرة الزرقاء) ، التي تجمع التبعيات على الحروف (الخط الأحمر). عندما يتم استدعاء أداة الضبط لاحقًا ، فإنها تخطر طريقة المراقبة ، مما يؤدي إلى إعادة عرض المكون. هنا نفس المخطط ، مزود بشروح تربطه بتطورنا.
مخطط تفاعلية فيو مع التفسيراتنعتقد أنه الآن ، بعد أن قمنا بكتابة نظام التفاعل الخاص بنا ، فإن هذا المخطط لا يحتاج إلى تفسيرات إضافية.
بالطبع ، في Vue ، كل هذا أكثر تعقيدًا ، ولكن الآن يجب أن تفهم الآلية الكامنة وراء أنظمة التفاعل.
الملخص
بعد قراءة هذه المادة ، تعلمت ما يلي:
- كيفية إنشاء فئة
Dep
تجمع الوظائف باستخدام الطريقة التابعة ، وإذا لزم الأمر ، تستدعيهم مرة أخرى باستخدام طريقة الإعلام. - كيفية إنشاء وظيفة
watcher
تسمح لك بالتحكم في التعليمات البرمجية التي نقوم بتشغيلها (هذه هي الوظيفة target
) ، والتي قد تحتاج إلى حفظها في كائن فئة Dep
. - كيفية استخدام طريقة
Object.defineProperty()
لإنشاء Object.defineProperty()
.
كل هذا ، تم جمعه في مثال واحد ، أدى إلى إنشاء نظام استجابة في JavaScript خالص ، من خلال فهم الذي يمكنك فهم ميزات عمل هذه الأنظمة المستخدمة في أطر الويب الحديثة.
أعزائي القراء! إذا ، قبل قراءة هذه المادة ، تخيلت بشكل سيئ ميزات آليات أنظمة التفاعل ، أخبرني ، هل تمكنت الآن من التعامل معها؟