كيفية تنظيم الحالة العامة في تطبيقات التفاعل دون استخدام المكتبات (ولماذا mobx مطلوب)

على الفور مفسد صغير - لا يختلف تنظيم حالة في mobx عن تنظيم حالة عامة دون استخدام mobx في رد فعل نقي. الجواب على السؤال الطبيعي هو لماذا تحتاج إلى هذا mobx في نهاية المقال ، ولكن في الوقت الحالي ، سيتم تخصيص المقالة لتنظيم الدولة في تطبيق رد فعل نظيف بدون أي مكتبات خارجية.




يوفر التفاعل طريقة لتخزين وتحديث حالة المكونات باستخدام خاصية الحالة على مثيل مكون فئة وأسلوب setState. ومع ذلك ، بين مجتمع التفاعل ، يتم استخدام مجموعة من المكتبات والأساليب الإضافية للعمل مع الدولة (التدفق ، الاختزال ، الاختزال ، المستجيب ، mobx ، الدماغ ، حفنة منهم). ولكن هل من الممكن إنشاء تطبيق كبير بما فيه الكفاية مع مجموعة من منطق الأعمال مع عدد كبير من الكيانات وعلاقات البيانات المعقدة بين المكونات باستخدام setState فقط؟ هل هناك حاجة لمكتبات إضافية للعمل مع الدولة؟ دعونا نكتشف ذلك.

لذلك لدينا setState والذي يقوم بتحديث الحالة ويستدعي أداة تقديم المكون. ولكن ماذا لو كانت البيانات نفسها مطلوبة من قبل العديد من المكونات غير المترابطة؟ في الرصيف الرسمي للتفاعل يوجد قسم "رفع الحالة" مع وصف تفصيلي - نحن ببساطة نرفع الحالة إلى الجد المشترك لهذه المكونات ، ونمر عبر الدعائم (ومن خلال المكونات الوسيطة ، إذا لزم الأمر) البيانات والوظائف لتغييره. بالنسبة إلى الأمثلة الصغيرة ، يبدو هذا معقولًا ، ولكن الحقيقة هي أنه في التطبيقات المعقدة يمكن أن يكون هناك الكثير من التبعيات بين المكونات والميل إلى نقل الحالات إلى مكون مشترك من سلف يؤدي إلى حقيقة أن الحالة بأكملها ستكون أعلى وأعلى وستنتهي في المكون الجذر للتطبيق مع منطق تحديث هذه الحالة لجميع المكونات. ونتيجة لذلك ، سيحدث setState فقط لتحديث مكون البيانات محليًا أو في المكون الجذر للتطبيق ، حيث سيتم تركيز كل المنطق.


ولكن هل من الممكن تخزين العملية وتقديم الحالة في تطبيق رد الفعل دون استخدام إما setState أو أي مكتبات إضافية وتوفير وصول عام إلى هذه البيانات من أي مكونات؟


إن أكثر عناصر جافا سكريبت شيوعًا وقواعد معينة لتنظيمها تأتي لمساعدتنا.


ولكن عليك أولاً معرفة كيفية تحليل التطبيقات إلى أنواع الكيانات وعلاقاتها.


بادئ ذي بدء ، نقدم كائنًا يقوم بتخزين البيانات العالمية التي تنطبق على التطبيق بأكمله ككل - (يمكن أن يكون هذا إعدادات الأنماط ، التعريب ، أحجام النوافذ ، إلخ) في كائن AppState واحد ووضع هذا الكائن في ملف منفصل.


// src/stores/AppState.js export const AppState = { locale: "en", theme: "...", .... } 

الآن في أي مكون يمكنك استيراد واستخدام بيانات متجرنا.


 import AppState from "../stores/AppState.js" const SomeComponent = ()=> ( <div> {AppState.locale === "..." ? ... : ...} </div> ) 

نذهب إلى أبعد من ذلك - فكل تطبيق تقريبًا له جوهر المستخدم الحالي (لا يهم كيف يتم إنشاؤه أو أن يأتي من الخادم ، وما إلى ذلك) ، لذلك سيكون كائن المستخدم الفردي الخاص بنا أيضًا في حالة التطبيق. يمكن أيضًا نقله إلى ملف منفصل واستيراده أيضًا ، أو يمكن تخزينه على الفور داخل كائن AppState. والآن الشيء الرئيسي - تحتاج إلى تحديد الرسم التخطيطي للكيانات التي تشكل التطبيق. من حيث قاعدة البيانات ، ستكون هذه جداول ذات علاقة واحد إلى كثير أو علاقة كثير إلى كثير ، وتبدأ سلسلة العلاقات هذه بالكامل من الجوهر الرئيسي للمستخدم. حسنًا ، في حالتنا ، سيخزن كائن المستخدم ببساطة مصفوفة من كائنات - كيانات - متاجر أخرى ، حيث يقوم كل مخزن كائن ، بدوره ، بتخزين صفائف من متاجر كيانات أخرى.


في ما يلي مثال - هناك منطق تجاري يتم التعبير عنه بأنه "يمكن للمستخدم إنشاء / تعديل / حذف المجلدات ، والمشاريع في كل مجلد ، وفي كل مشروع مهمة وفي كل مهمة مهمة فرعية" (يتحول إلى شيء يشبه مدير المهام) وسيظهر في مخطط الحالة شيء من هذا القبيل:


 export const AppStore = { locale: "en", theme: "...", currentUser: { name: "...", email: "" folders: [ { name: "folder1", projects: [ { name: "project1", tasks: [ { text: "task1", subtasks: [ {text: "subtask1"}, .... ] }, .... ] }, ..... ] }, ..... ] } } 

الآن يمكن للمكون الجذر للتطبيق استيراد هذا الكائن وتقديم بعض المعلومات حول المستخدم ، ومن ثم يمكنه نقل كائن المستخدم إلى مكون لوحة المعلومات


  .... <Dashboard user={appState.user}/> .... 

ويمكنه عرض قائمة المجلدات


  ... <div>{user.folders.map(folder=><Folder folder={folder}/>)}</div> ... 

وسيعرض كل مكون في المجلد قائمة بالمشاريع


  .... <div>{folder.projects.map(project=><Project project={project}/>)}</div> .... 

ويمكن لكل مكون في المشروع سرد المهام


  .... <div>{project.tasks.map(task=><Task task={task}/>)}</div> .... 

وأخيرًا ، يمكن لكل مكون مهمة تقديم قائمة بالمهام الفرعية بتمرير الكائن المطلوب إلى مكون المهمة الفرعية


  .... <div>{task.subtask.map(subtask=><Subtask subtask={subtask}/>)}</div> .... 

بطبيعة الحال ، في صفحة واحدة ، لن يعرض أحد جميع المهام لجميع المشاريع لجميع المجلدات ، وسيتم تقسيمها على لوحات جانبية (على سبيل المثال ، قائمة المجلدات) ، حسب الصفحات ، وما إلى ذلك ، ولكن الهيكل العام هو نفسه تقريبًا - المكون الأصلي يعرض المكون المضمن الذي يمرر كائنًا مع الدعائم البيانات. يجب ملاحظة نقطة مهمة - أي كائن (على سبيل المثال ، كائن مجلد ، مشروع ، مهمة) لا يتم تخزينه داخل حالة أي مكون - يتلقى المكون ببساطة من خلال الدعائم كجزء من كائن أكثر عمومية. وكما هو الحال عندما ينقل عنصر من عناصر المشروع المكون التابعة المهام الكائن المهمة ( <div>{project.tasks.map(task=><Task task={task}/>)}</div> ) يرجع ذلك إلى حقيقة أن يتم تخزين الكائنات في كائن واحد يمكنك دائمًا تغيير كائن المهمة هذا من الخارج - على سبيل المثال ، AppState.currentUser.folders [2] .projects [3] .tasks [4] .text = "edited مهمة" ثم تتسبب في تحديث مكون الجذر (ReactDOM.render (<App /> ) وبهذه الطريقة نحصل على الحالة الحالية للتطبيق.


افترض أيضًا أننا نريد إنشاء مهمة فرعية جديدة عند النقر فوق الزر "+" في مكون المهام. كل شيء بسيط


  onClick = ()=>{ this.props.task.subtasks.push({text: ""}); updateDOM() } 

نظرًا لأن مكوِّن المهام يتلقى كدعم ككائن مهمة ولا يتم تخزين هذا الكائن داخل حالته ولكنه جزء من متجر AppState العام (أي ، يتم تخزين كائن المهمة داخل صفيف المهام لكائن المشروع الأكثر عمومية ، وهذا بدوره جزء من كائن المستخدم ويتم تخزين المستخدم بالفعل داخل AppState ) وبفضل هذا الاتصال ، بعد إضافة كائن مهمة جديد إلى مصفوفة المهام الفرعية ، يمكنك استدعاء تحديث مكون الجذر وبالتالي تحديث المنزل وتحديثه لجميع تغييرات البيانات (بغض النظر عن مكان حدوثها) ببساطة عن طريق استدعاء وظيفة التحديث ateDOM ، والذي بدوره يقوم بتحديث مكون الجذر.


 export function updateDOM(){ ReactDom.render(<App/>, rootElement); } 

ولا يهم ما هي البيانات الخاصة بأجزاء AppState ومن الأماكن التي نقوم بتغييرها (على سبيل المثال ، يمكنك إعادة توجيه كائن مجلد من خلال الدعائم من خلال مكونات المشروع والمهمة الوسيطة إلى مكون المهام الفرعية ، ويمكن فقط تحديث اسم المجلد (this.props.folder.name = "new name ") - نظرًا لحقيقة تلقي المكونات للبيانات من خلال الدعائم ، سيؤدي تحديث المكون الجذر إلى تحديث جميع المكونات المتداخلة وتحديث التطبيق بالكامل.


الآن دعنا نحاول إضافة بعض الراحة للعمل مع الجانب. في المثال أعلاه ، يمكنك ملاحظة أن إنشاء كائن كيان جديد في كل مرة (على سبيل المثال project.tasks.push({text: "", subtasks: [], ...}) إذا كان الكائن يحتوي على العديد من الخصائص مع المعلمات الافتراضية ، ثم في كل مرة لسردها ويمكنك ارتكاب خطأ ونسيان شيء ، وما إلى ذلك. أول شيء يتبادر إلى الذهن هو وضع إنشاء كائن في وظيفة حيث سيتم تعيين الحقول الافتراضية وفي نفس الوقت إعادة تعريفها ببيانات جديدة


 function createTask(data){ return { text: "", subtasks: [], ... //many default fields ...data } } 

ولكن إذا نظرت من الجانب الآخر ، فهذه الوظيفة هي مُنشئ كيان معين وفئات جافا سكريبت رائعة لهذا الدور


 class Task { text: ""; subtasks: []; constructor(data){ Object.assign(this, data) } } 

ثم سيؤدي إنشاء الكائن ببساطة إلى إنشاء مثيل من الفئة مع القدرة على تجاوز بعض الحقول الافتراضية


 onAddTask = ()=>{ this.props.project.tasks.push(new Task({...}) } 

علاوة على ذلك ، يمكنك أن تلاحظ أنه بنفس الطريقة ، بإنشاء فئات لكائنات المشروع ، والمستخدمين ، والمهام الفرعية ، نحصل على تكرار التعليمات البرمجية داخل المُنشئ


 constructor(){ Object.assign(this,data) } 

ولكن يمكننا الاستفادة من الميراث وسحب هذا الرمز إلى مُنشئ الفئة الأساسية.


 class BaseStore { constructor(data){ Object.update(this, data); } } 

علاوة على ذلك ، ستلاحظ أنه في كل مرة نقوم فيها بتحديث حالة ما ، نقوم بتغيير حقول الكائن يدويًا


 user.firstName = "..."; user.lastName = "..."; updateDOM(); 

ويصبح من الصعب تتبع ومساومة وفهم ما يحدث في المكون ، وبالتالي هناك حاجة لتحديد قناة مشتركة تمر من خلالها تحديثات أي بيانات ومن ثم يمكننا إضافة التسجيل وجميع أنواع وسائل الراحة الأخرى. للقيام بذلك ، فإن الحل هو إنشاء طريقة تحديث في الفصل والتي ستأخذ كائنًا مؤقتًا ببيانات جديدة وتحديث نفسها وتؤسس القاعدة القائلة بأنه يمكن تحديث الكائنات فقط من خلال طريقة التحديث وليس عن طريق التعيين المباشر


 class Task { update(newData){ console.log("before update", this); Object.assign(this, data); console.log("after update", this); } } //// user.update({firstName: "...", lastName: "..."}) 

حسنًا ، حتى لا نكرر الرمز في كل فئة ، ننقل أيضًا طريقة التحديث هذه إلى الفئة الأساسية.


يمكنك الآن ملاحظة أنه عندما نقوم بتحديث بعض البيانات ، يتعين علينا استدعاء طريقة updateDOM () يدويًا. ولكن من الممكن بسهولة إجراء هذا التحديث تلقائيًا في كل مرة يتم فيها إجراء استدعاء لتحديث ({...}) طريقة الفئة الأساسية.
اتضح أن الطبقة الأساسية ستبدو مثل هذا


 class BaseStore { constructor(data){ Object.update(this, data); } update(data){ Object.update(this, data); ReactDOM.render(<App/>, rootElement) } } 

حسنًا ، أثناء المكالمة المتتالية لطريقة التحديث () لا توجد تحديثات غير ضرورية ، يمكنك تأخير تحديث المكون إلى الدورة التالية من الأحداث


 let TimerId = 0; class BaseStore { constructor(data){ Object.update(this, data); } update(data){ Object.update(this, data); if(TimerId === 0) { TimerId = setTimeout(()=>{ TimerId = 0; ReactDOM.render(<App/>, rootElement); }) } } } 

علاوة على ذلك ، يمكنك زيادة وظائف الفئة الأساسية تدريجيًا - على سبيل المثال ، حتى لا تضطر إلى إرسال طلب يدويًا إلى الخادم في كل مرة ، بالإضافة إلى تحديث الحالة ، يمكنك إرسال الطلب عن طريق تحديث طريقة ((..) في الخلفية. يمكنك تنظيم قناة تحديث مباشرة لمقابس الويب عن طريق إضافة حساب لكل كائن تم إنشاؤه في خريطة التجزئة العامة دون تغيير المكونات والعمل مع البيانات بأي طريقة.


لا يزال هناك الكثير الذي يتعين القيام به ، ولكن أريد أن أذكر موضوعًا واحدًا مثيرًا للاهتمام - غالبًا ما يمرر كائنًا بالبيانات إلى المكون الضروري (على سبيل المثال ، عندما يعرض أحد مكونات المشروع مكون مهمة -


 <div>{project.tasks.map(task=><Task task={task}/>)}</div> 

قد يحتاج المكون الخاص بالمهمة إلى بعض المعلومات التي لا يتم تخزينها مباشرة داخل المهمة ولكنها موجودة في الكائن الأصل.


افترض أنك تريد تلوين جميع المهام بلون مخزّن في المشروع وهو أمر شائع في جميع المهام. للقيام بذلك ، بالإضافة إلى دعائم المهام ، يجب أن يرسل مكون المشروع أيضًا دعائم المشروع الخاصة به <Task task={task} project={this.props.project}/> . وإذا كنت بحاجة فجأة إلى تلوين المهمة بلون مشترك لجميع المهام في مجلد واحد ، فسيتعين عليك نقل كائن المجلد الحالي من مكون المجلد إلى مكون المهمة عن طريق إعادة توجيهه من خلال مكون المشروع المتوسط.
تظهر تبعية هشة أن المكون يجب أن يعرف ما تتطلبه المكونات المتداخلة. علاوة على ذلك ، فإن إمكانية سياق رد الفعل ، على الرغم من أنها ستبسط النقل من خلال المكونات الوسيطة ، ستظل تتطلب وصفًا للموفر ومعرفة بالبيانات المطلوبة للمكونات الفرعية.


لكن المشكلة الرئيسية هي أنه في كل مرة تقوم فيها بتحرير تصميم أو تغيير قائمة رغبات العميل عندما يحتاج أحد المكونات إلى معلومات جديدة ، سيتعين عليك تغيير المكونات الأعلى إما دعائم إعادة التوجيه أو إنشاء موفري السياق. أود أن يتلقى المكون من خلال الدعائم كائنًا مع بيانات للوصول بطريقة ما إلى أي جزء من حالة التطبيق لدينا. وهنا ، يعد جافا سكريبت مناسبًا تمامًا (على عكس أي لغات وظيفية مثل الدردار أو الأساليب الثابتة مثل redux) - بحيث يمكن للأشياء تخزين الروابط الدائرية مع بعضها البعض. في هذه الحالة ، يجب أن يحتوي كائن المهمة على حقل task.project مع ارتباط إلى كائن المشروع الأصلي الذي يتم تخزينه فيه ، ويجب أن يحتوي كائن المشروع بدوره على ارتباط إلى كائن المجلد ، وما إلى ذلك ، إلى كائن AppState الجذر. وبالتالي ، يمكن للمكون ، بغض النظر عن مدى عمقه ، أن يمر دائمًا بالكائنات الرئيسية من خلال الرابط والحصول على جميع المعلومات اللازمة ولا تحتاج إلى رميها من خلال مجموعة من المكونات الوسيطة. لذلك ، نقدم قاعدة - في كل مرة تقوم بإنشاء كائن تحتاج إلى إضافة ارتباط إلى الكائن الأصل. على سبيل المثال ، سيبدو إنشاء مهمة جديدة الآن على هذا النحو


  ... const {project} = this.props; const newTask = new Task({project: this.props.project}) this.props.project.tasks.push(newTask); 

علاوة على ذلك ، مع زيادة منطق الأعمال ، يمكنك ملاحظة أن لوح الربط مرتبط بدعم الروابط الخلفية (على سبيل المثال ، تعيين رابط للكائن الرئيسي عند إنشاء كائن جديد أو على سبيل المثال ، عند نقل مشروع من مجلد إلى آخر ، لن تحتاج فقط إلى تحديث المشروع. مجلد = newFolder وحذف خاصية تبدأ نفسك من مصفوفة المشروع للمجلد السابق وإضافة مجلد جديد إلى صفيف المشروع) في التكرار ويمكن أيضًا نقله إلى الفئة الأساسية بحيث أنه عند إنشاء الكائن كان كافياً لتحديد الأصل - new Task({project: this.porps.project}) new Task({project: this.porps.project}) الأساسية ستضيف تلقائيًا كائنًا جديدًا إلى مصفوفة project.tasks وأيضًا عند نقل المهمة إلى مشروع آخر سيكون كافيًا فقط لتحديث task.update({project: newProject}) الفئة الأساسية المهمة تلقائيًا من مجموعة من المهام من المشروع السابق وإضافتها إلى مشروع جديد. لكن هذا سيتطلب بالفعل إعلان العلاقات (على سبيل المثال ، في الخصائص أو الأساليب الثابتة) بحيث تعرف الفئة الأساسية الحقول التي يجب تحديثها.


الخلاصة


بهذه الطريقة البسيطة ، باستخدام كائنات js فقط ، توصلنا إلى استنتاج مفاده أنه يمكنك الحصول على كل الراحة للعمل مع الحالة العامة للتطبيق دون إدخال الاعتماد على مكتبة خارجية للعمل مع الحالة.


والسؤال هو ، لماذا نحتاج إذن إلى مكتبات لإدارة الدولة ، وخاصة mobx؟


والحقيقة هي أنه في النهج الموصوف لتنظيم الحالة العامة ، عند استخدام كائنات "الفانيليا" العادية (أو الكائنات الطبقية) ، هناك عيب كبير واحد - عندما يتغير جزء صغير من الحالة أو حتى حقل واحد ، سيتم تحديث المكونات أو "تقديم" التي لا ترتبط بأي شكل من الأشكال ولا تعتمد على هذا الجزء من الدولة.
وفي التطبيقات الكبيرة ذات واجهة المستخدم الغامقة ، سيؤدي هذا إلى المكابح لأن التفاعل ببساطة ليس لديه وقت لمقارنة المنزل الافتراضي للتطبيق بأكمله بشكل متكرر ، نظرًا لأنه بالإضافة إلى مقارنة كل عارض ، سيتم إنشاء شجرة كائن جديدة في كل مرة تصف تخطيط جميع المكونات على الإطلاق.


لكن هذه المشكلة ، على الرغم من أهميتها ، هي تقنية بحتة - هناك مكتبات مشابهة لتفاعل Dom الافتراضي الذي يعمل على تحسين جهاز العرض بشكل أفضل ويمكن أن يزيد من حد المكون.


هناك تقنيات أكثر فاعلية لتجديد المنزل من إنشاء شجرة منزل افتراضية جديدة وتمرير المقارنة العودية اللاحق مع الشجرة السابقة.


وأخيرًا ، هناك مكتبات تحاول حل مشكلة التحديثات البطيئة من خلال نهج مختلف - وبالتحديد ، لتتبع أي أجزاء الولاية متصلة بالمكونات وعند تغيير بعض البيانات ، قم فقط بحساب وتحديث المكونات التي تعتمد على هذه البيانات ولا تلمس المكونات المتبقية. Redux هي أيضًا مكتبة ، لكنها تتطلب نهجًا مختلفًا تمامًا لتنظيم الدولة. لكن مكتبة mobx ، على العكس من ذلك ، لا تجلب أي شيء جديد ويمكننا الحصول على تسريع العارض عمليًا دون تغيير أي شيء في التطبيق - فقط أضف الديكور @observable إلى حقول الفصل @observable decorator للمكونات التي تجعل هذه الحقول ويبقى لقص رمز التحديث غير الضروري فقط للمكون الجذر في طريقة التحديث () لفئتنا الأساسية وسنحصل على تطبيق يعمل بشكل كامل ، ولكن الآن تغيير جزء من الحالة أو حتى حقل واحد سيؤدي إلى تحديث هذه المكونات فقط التي نضجت وقعت (تحول أسلوب داخل تجعل ()) لحقل معين من دولة معينة للكائن.

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


All Articles