تعد إدارة الحالة أحد أهم الأجزاء في تطبيق الويب. بدءًا من استخدام المتغيرات العامة وخطافات React وحتى استخدام مكتبات الطرف الثالث مثل MobX أو Redux أو XState لتسمية هذه المكتبات الثلاثة فقط، يعد هذا أحد الموضوعات التي تغذي معظم المناقشات لأنه من المهم إتقانها لتصميم تطبيق موثوق وفعال.
اليوم، أقترح إنشاء مكتبة إدارة حالة مصغرة في أقل من 50 سطرًا من JavaScript استنادًا إلى مفهوم الأشياء القابلة للملاحظة. يمكن بالتأكيد استخدام هذا الحل كما هو في المشاريع الصغيرة، ولكن بعيدًا عن هذا التمرين التعليمي، ما زلت أوصيك باللجوء إلى حلول أكثر توحيدًا لمشاريعك الحقيقية.
عند بدء مشروع مكتبة جديد، من المهم تحديد الشكل الذي يمكن أن تبدو عليه واجهة برمجة التطبيقات (API) الخاصة به من البداية من أجل تجميد مفهومه وتوجيه تطويره قبل حتى التفكير في تفاصيل التنفيذ الفني. بالنسبة لمشروع حقيقي، من الممكن أيضًا البدء في كتابة الاختبارات في هذا الوقت للتحقق من صحة تنفيذ المكتبة كما هي مكتوبة وفقًا لمنهج TDD.
نريد هنا تصدير فئة واحدة سنسميها الحالة والتي سيتم إنشاء مثيل لها بكائن يحتوي على الحالة الأولية وطريقة مراقبة واحدة تسمح لنا بالاشتراك في تغييرات الحالة مع المراقبين. يجب أن يتم تنفيذ هؤلاء المراقبين فقط إذا تغيرت إحدى تبعياتهم.
لتغيير الحالة، نريد استخدام خصائص الفئة مباشرةً بدلاً من اتباع طريقة مثل setState.
نظرًا لأن مقتطف الشفرة يساوي ألف كلمة، فإليك الشكل الذي قد يبدو عليه التنفيذ النهائي عند الاستخدام:
const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.count = 1; state.text = 'Hello, world!'; state.count = 1; // Output: // Count changed 1 // Text changed Hello, world! // Count changed 2
لنبدأ بإنشاء فئة حالة تقبل الحالة الأولية في مُنشئها وتكشف عن طريقة المراقبة التي سننفذها لاحقًا.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; } observe(observer) { this.observers.push(observer); } }
نختار هنا استخدام كائن الحالة المتوسطة الداخلي الذي سيسمح لنا بالحفاظ على قيم الحالة. نقوم أيضًا بتخزين المراقبين في مصفوفة مراقبين داخليين والتي ستكون مفيدة عندما نكمل هذا التنفيذ.
نظرًا لأن هاتين الخاصيتين لن يتم استخدامهما إلا داخل هذه الفئة، فيمكننا إعلانهما على أنهما خاصتان مع القليل من السكر النحوي عن طريق وضع بادئة لهما بـ # وإضافة إعلان أولي عن الفئة:
class State { #state = {}; #observers = []; constructor(initialState = {}) { this.#state = initialState; this.#observers = []; } observe(observer) { this.#observers.push(observer); } }
من حيث المبدأ، ستكون هذه ممارسة جيدة، لكننا سنستخدم الوكلاء في الخطوة التالية وهم غير متوافقين مع الممتلكات الخاصة. بدون الخوض في التفاصيل ولتسهيل هذا التنفيذ، سنستخدم الخصائص العامة في الوقت الحالي.
عندما حددنا مواصفات هذا المشروع، أردنا الوصول إلى قيم الحالة مباشرة على مثيل الفئة وليس كمدخل لكائن الحالة الداخلية الخاص به.
لهذا، سوف نستخدم كائن الوكيل الذي سيتم إرجاعه عند تهيئة الفصل.
كما يوحي اسمه، يتيح لك الوكيل إنشاء وسيط لكائن ما لاعتراض عمليات معينة، بما في ذلك الحروف والمحددات الخاصة به. في حالتنا، نقوم بإنشاء وكيل يعرض أول حرف يسمح لنا بكشف مدخلات كائن الحالة كما لو كانت تنتمي مباشرة إلى مثيل الحالة.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); console.log(state.count); // 0
الآن يمكننا تحديد كائن الحالة الأولية عند إنشاء حالة الحالة ثم استرداد قيمها مباشرة من هذا المثيل. الآن دعونا نرى كيفية التعامل مع بياناته.
لقد أضفنا أداة getter، لذا فإن الخطوة المنطقية التالية هي إضافة أداة ضبط تسمح لنا بمعالجة كائن الحالة.
نتحقق أولاً من أن المفتاح ينتمي إلى هذا الكائن، ثم نتحقق من أن القيمة قد تغيرت بالفعل لمنع التحديثات غير الضرورية، وأخيرًا نقوم بتحديث الكائن بالقيمة الجديدة.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; } } else { target[prop] = value; } }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); console.log(state.count); // 0 state.count = 1; console.log(state.count); // 1
لقد انتهينا الآن من جزء قراءة البيانات وكتابتها. يمكننا تغيير قيمة الحالة ثم استرداد هذا التغيير. حتى الآن، تنفيذنا ليس مفيدًا جدًا، لذا دعونا ننفذ المراقبين الآن.
لدينا بالفعل مصفوفة تحتوي على وظائف المراقبين المعلنة في مثيلنا، لذلك كل ما يتعين علينا فعله هو استدعاءهم واحدًا تلو الآخر كلما تغيرت القيمة.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; this.observers.forEach((observer) => { observer(this.state); }); } } else { target[prop] = value; } }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.count = 1; state.text = 'Hello, world!'; // Output: // Count changed 1 // Text changed // Count changed 1 // Text changed Hello, world!
رائع، نحن الآن نتفاعل مع تغييرات البيانات!
مشكلة صغيرة بالرغم من ذلك. إذا كنت منتبهًا إلى هذا الحد، فقد أردنا في الأصل تشغيل المراقبين فقط إذا تغيرت إحدى تبعياتهم. ومع ذلك، إذا قمنا بتشغيل هذا الكود، فسنرى أن كل مراقب يعمل في كل مرة يتم فيها تغيير جزء من الحالة.
ولكن كيف يمكننا بعد ذلك تحديد تبعيات هذه الوظائف؟
مرة أخرى، يأتي الوكلاء لإنقاذنا. لتحديد تبعيات وظائف المراقب لدينا، يمكننا إنشاء وكيل لكائن الحالة الخاص بنا، وتشغيله كوسيطة، وملاحظة الخصائص التي تم الوصول إليها.
بسيطة ولكنها فعالة.
عند استدعاء المراقبين، كل ما يتعين علينا فعله هو التحقق مما إذا كان لديهم تبعية للخاصية المحدثة وتشغيلهم فقط إذا كان الأمر كذلك.
هنا هو التنفيذ النهائي لمكتبتنا المصغرة مع إضافة هذا الجزء الأخير. ستلاحظ أن مصفوفة المراقبين تحتوي الآن على كائنات تسمح بالاحتفاظ بتبعيات كل مراقب.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; this.observers.forEach(({ observer, dependencies }) => { if (dependencies.has(prop)) { observer(this.state); } }); } } else { target[prop] = value; } }, }); } observe(observer) { const dependencies = new Set(); const proxy = new Proxy(this.state, { get: (target, prop) => { dependencies.add(prop); return target[prop]; }, }); observer(proxy); this.observers.push({ observer, dependencies }); } } const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.observe((state) => { console.log('Count or text changed', state.count, state.text); }); state.count = 1; state.text = 'Hello, world!'; state.count = 1; // Output: // Count changed 0 // Text changed // Count or text changed 0 // Count changed 1 // Count or text changed 1 // Text changed Hello, world! // Count or text changed 1 Hello, world! // Count changed 2 // Count or text changed 2 Hello, world!
وهكذا، في 45 سطرًا من التعليمات البرمجية، قمنا بتنفيذ مكتبة إدارة حالة مصغرة في JavaScript.
إذا أردنا المضي قدمًا، فيمكننا إضافة اقتراحات النوع باستخدام JSDoc أو إعادة كتابة هذا الاقتراح في TypeScript للحصول على اقتراحات بشأن خصائص مثيل الحالة.
يمكننا أيضًا إضافة طريقة غير ملحوظة والتي سيتم كشفها على كائن تم إرجاعه بواسطة State.observe.
قد يكون من المفيد أيضًا تجريد سلوك الضبط في طريقة setState التي تسمح لنا بتعديل خصائص متعددة في وقت واحد. حاليًا، يتعين علينا تعديل كل خاصية في حالتنا واحدة تلو الأخرى، مما قد يؤدي إلى تشغيل مراقبين متعددين إذا كان بعضهم يشترك في التبعيات.
على أية حال، أتمنى أن تكون قد استمتعت بهذا التمرين الصغير بقدر ما استمتعت به، وأنه سمح لك بالتعمق أكثر في مفهوم الوكيل في JavaScript.
تنصل: جميع الموارد المقدمة هي جزئيًا من الإنترنت. إذا كان هناك أي انتهاك لحقوق الطبع والنشر الخاصة بك أو الحقوق والمصالح الأخرى، فيرجى توضيح الأسباب التفصيلية وتقديم دليل على حقوق الطبع والنشر أو الحقوق والمصالح ثم إرسالها إلى البريد الإلكتروني: [email protected]. سوف نتعامل مع الأمر لك في أقرب وقت ممكن.
Copyright© 2022 湘ICP备2022001581号-3