تفاعل جافا سكريبت: مثال بسيط وبديهي

تحتوي العديد من إطارات 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 ، يحتاج المحرك إلى القيام بثلاثة أشياء:

  1. تحديث قيمة price على صفحة الويب.
  2. إعادة حساب التعبير الذي يتم ضرب price فيه quantity ، وعرض القيمة الناتجة على الصفحة.
  3. استدعاء الدالة totalPriceWithTax ، ومرة ​​أخرى ، ضع ما يعود على الصفحة.

يظهر ما يحدث هنا في الرسم التوضيحي التالي.


كيف تعرف Vue ماذا تفعل عندما يتغير سعر العقار؟

لدينا الآن أسئلة حول كيفية معرفة Vue لما يجب تحديثه بالضبط عندما يتغير price ، وكيف يتتبع المحرك ما يحدث على الصفحة. ما يمكنك ملاحظته هنا لا يبدو وكأنه تطبيق JS عادي.

ربما لم يكن هذا واضحًا بعد ، ولكن المشكلة الرئيسية التي نحتاج إلى حلها هنا هي أن برامج JS لا تعمل عادةً بهذه الطريقة. على سبيل المثال ، لنقم بتشغيل التعليمات البرمجية التالية:

 let price = 5 let quantity = 2 let total = price * quantity //  10 price = 20; console.log(`total is ${total}`) 

ما رأيك سيتم عرضه في وحدة التحكم؟ نظرًا لعدم استخدام أي شيء هنا باستثناء 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 ، ومن ثم استدعاء وظيفة التسجيل. سنتحدث عنه أدناه. أود أيضًا أن أشير إلى أنه يمكن إعادة كتابة الوظيفة 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) // 10 replay() console.log(total) // 40 

هذا ما سيتم عرضه في وحدة تحكم المتصفح بعد بدء تشغيله.


نتيجة الرمز

التحدي: حل موثوق لتخزين الوظائف


يمكننا الاستمرار في تدوين الوظائف التي نحتاجها عندما يصبح ذلك ضروريًا ، ولكن سيكون من الجيد إذا كان لدينا حل أكثر موثوقية يمكن تطويره باستخدام التطبيق. ربما ستكون فئة تحتفظ بقائمة من الوظائف المكتوبة في الأصل إلى المتغير 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    target() //     total console.log(total) // 10 -   price = 20 console.log(total) // 10 -    ,    dep.notify() //   -  console.log(total) // 40 -    

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

الشيء الوحيد الذي يبدو غريباً فيه حتى الآن هو العمل مع وظيفة مخزنة في المتغير 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 خالص ، من خلال فهم الذي يمكنك فهم ميزات عمل هذه الأنظمة المستخدمة في أطر الويب الحديثة.

أعزائي القراء! إذا ، قبل قراءة هذه المادة ، تخيلت بشكل سيئ ميزات آليات أنظمة التفاعل ، أخبرني ، هل تمكنت الآن من التعامل معها؟

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


All Articles