التوقف عن إصدار شيء آخر بمثابة تسرب للذاكرة

حتى الآن ، تمت كتابة الكثير من المقالات التي تحتاج إلى إلغاء الاشتراك من اشتراكات RxJS القابلة للملاحظة ، وإلا فسيحدث تسرب للذاكرة . بالنسبة لمعظم قراء مثل هذه المقالات ، فإن القاعدة الصارمة "تم التوقيع؟ - تسجيل!" ولكن لسوء الحظ ، غالبًا ما تكون المعلومات مشوهة في مثل هذه المقالات أو لا يتم التفاوض حول شيء ما ، بل والأسوأ من ذلك عندما يتم استبدال المفاهيم. سنتحدث عن هذا.



خذ على سبيل المثال هذه المقالة: https://medium.com/ngx/why-do-you-need-unsubscribe-ee0c62b5d21f



عندما يقولون لي عن "الفرصة المحتملة للحصول على تراجع في الأداء" ، أفكر على الفور في التحسين السابق لأوانه .




نستمر في قراءة المقال من قبل الرجل تحت لقب رد الفعل فوكس :



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


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


قام مؤلف المقال بتقديم طلب تجريبي ، حيث حاول إثبات أفكاره:
https://stackblitz.com/edit/why-you-have-to-unsubscribe-from-observable-material


في الواقع ، على موقفه يمكنك أن ترى كيف يعمل المعالج بشكل غير ضروري (عندما لا أقوم بالنقر فوق أي شيء) وكيف يزيد استهلاك الذاكرة (تغيير بسيط):



كتأكيد لحقيقة أنك تحتاج دائمًا إلى إلغاء الاشتراك في اشتراكات طلبات HttpClient التي يمكن ملاحظتها ، أضاف مُعترض طلب يعرض "لا يزال حيا ... لا يزال حيا ... لا يزال حيا ..." في وحدة التحكم:

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


StackBlitz ليست مناسبة للغاية لقياس أداء التطبيق ، كما هناك التزامن التلقائي أثناء الترقية ويستغرق الموارد. لذلك قدمت طلب الاختبار الخاص بي: https://github.com/andchir/test-angular-app


هناك نوعان من النوافذ هناك. عند فتح كل منهما ، يتم إرسال طلب إلى action.php ، حيث يوجد تأخير لمدة 3 ثوانٍ كتقليد لعملية كثيفة الاستخدام للموارد. أيضا action.php يسجل جميع الطلبات إلى ملف log.txt .


رمز Action.php
<?php header('Content-Type: application/json'); function logging($str, $fileName = 'log.txt') { if (is_array($str)) { $str = json_encode($str); } $rootPath = __DIR__; $logFilePath = $rootPath . DIRECTORY_SEPARATOR . $fileName; $options = [ 'max_log_size' => 200 * 1024 ]; if (!is_dir(dirname($logFilePath))) { mkdir(dirname($logFilePath)); } if (file_exists($logFilePath) && filesize($logFilePath) >= $options['max_log_size']) { unlink($logFilePath); } $fp = fopen( $logFilePath, 'a' ); $dateFormat = 'd/m/YH:i:s'; $str = PHP_EOL . PHP_EOL . date($dateFormat) . PHP_EOL . $str; fwrite( $fp, $str ); fclose( $fp ); return true; } $actionName = isset($_GET['a']) && !is_array($_GET['a']) ? $_GET['a'] : '1'; logging("STARTED-{$actionName}"); sleep(3);// Very resource-intensive operation that takes 3 seconds logging("COMPLETED-{$actionName}"); echo json_encode([ 'success' => true, 'data' => ['name' => 'test', 'title' => 'This is a test'] ]); 

لكن أولا ، استطرادا صغيرا. في الصورة أدناه (القابلة للنقر) ، يمكنك رؤية مثال بسيط لكيفية عمل أداة تجميع مجمعي البيانات المهملة في JavaScript في متصفح Chrome. حدث PUSH ، لكن setTimeout لم يمنع جامع البيانات المهملة من مسح الذاكرة.


لا تنس الاتصال بمجمع أداة تجميع البيانات المهملة بلمسة زر واحدة عند التجربة.


دعنا نعود إلى تطبيق الاختبار. هنا هو رمز لكلا النوافذ:


BadModalComponent Code
 @Component({ selector: 'app-bad-modal', templateUrl: './bad-modal.component.html', styleUrls: ['./bad-modal.component.css'], providers: [HttpClient] }) export class BadModalComponent implements OnInit, OnDestroy { loading = false; largeData: number[] = (new Array(1000000)).fill(1); destroyed$ = new Subject<void>(); data: DataInterface; constructor( private http: HttpClient, private bsModalRef: BsModalRef ) { } ngOnInit() { this.loadData(); } loadData(): void { // For example only, not for production. this.loading = true; const subscription = this.http.get<DataInterface>('/action.php?a=2').pipe( takeUntil(this.destroyed$), catchError((err) => throwError(err.message)), finalize(() => console.log('FINALIZE')) ) .subscribe({ next: (res) => { setTimeout(() => { console.log(subscription.closed ? 'SUBSCRIPTION IS CLOSED' : 'SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('LOADED'); this.data = res; this.loading = false; }, error: (error) => { setTimeout(() => { console.log(subscription.closed ? 'ERROR - SUBSCRIPTION IS CLOSED' : 'ERROR - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('ERROR', error); }, complete: () => { setTimeout(() => { console.log(subscription.closed ? 'COMPLETED - SUBSCRIPTION IS CLOSED' : 'COMPLETED - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('COMPLETED'); } }); } close(event?: MouseEvent): void { if (event) { event.preventDefault(); } this.bsModalRef.hide(); } ngOnDestroy() { console.log('DESTROY'); this.destroyed$.next(); this.destroyed$.complete(); } } 

كما ترون ، هناك إلغاء الاشتراك (takeUntil). كل شيء كما "المعلم" نصحت لنا. هناك أيضا مجموعة كبيرة.


GoodModalComponent Code
 @Component({ selector: 'app-good-modal', templateUrl: './good-modal.component.html', styleUrls: ['./good-modal.component.css'] }) export class GoodModalComponent implements OnInit, OnDestroy { loading = false; largeData: number[] = (new Array(1000000)).fill(1); data: DataInterface; constructor( private http: HttpClient, private bsModalRef: BsModalRef ) { } ngOnInit() { this.loadData(); } loadData(): void { // For example only, not for production. this.loading = true; const subscription = this.http.get<DataInterface>('/action.php?a=1').pipe( catchError((err) => throwError(err.message)), finalize(() => console.log('FINALIZE')) ) .subscribe({ next: (res) => { setTimeout(() => { console.log(subscription.closed ? 'SUBSCRIPTION IS CLOSED' : 'SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('LOADED'); this.data = res; this.loading = false; }, error: (error) => { setTimeout(() => { console.log(subscription.closed ? 'ERROR - SUBSCRIPTION IS CLOSED' : 'ERROR - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('ERROR', error); }, complete: () => { setTimeout(() => { console.log(subscription.closed ? 'COMPLETED - SUBSCRIPTION IS CLOSED' : 'COMPLETED - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('COMPLETED'); } }); } close(event?: MouseEvent): void { if (event) { event.preventDefault(); } this.bsModalRef.hide(); } ngOnDestroy() { console.log('DESTROY'); } } 

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


شاهد الفيديو:



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


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

نعم ، إنه يتحدث عن تسرب "محتمل" . ولكن ، إذا كان الدفق محدودًا ، فلن يكون هناك تسرب للذاكرة.


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


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


شاهد الفيديو التالي:



لقد جعلت المستخدم ينتظر إجابة. في معظم الحالات ، ستأتي الإجابة بسرعة ولن يتعرض المستخدم لأي إزعاج. ولكن بهذه الطريقة ، سوف ننقذ الخادم من القيام بعمليات ثقيلة متكررة ، إن وجدت.


النتائج:


  • أنا لا أقول أنك لست بحاجة إلى إلغاء الاشتراك من اشتراكات RxJS لطلبات HttpClient. أنا فقط أقول أن هناك أوقات عندما يكون هذا غير ضروري. لا حاجة لاستبدال المفاهيم. إذا كنت تتحدث عن تسرب للذاكرة ، فقم بإظهار هذا التسرب. ليس لديك console.log التي لا نهاية لها ، وهي تسرب. ما هي الذاكرة تقاس؟ ما هو وقت العملية تقاس؟ هذا هو ما يجب أن يظهر.
  • أنا لا أتصل بحلتي ، والذي تقدمت به في تطبيق الاختبار ، "رصاصة فضية". على العكس من ذلك ، أحث الجهة الأمامية على منح المزيد من الحرية. دعه يقرر كيف يكتب رمزه. لا حاجة لتخويفه وفرض أسلوبه الخاص في التنمية.
  • أنا ضد التعصب والتحسين السابق لأوانه. لقد رأيت الكثير من هذا في الآونة الأخيرة.
  • يحتوي المتصفح على طرق أكثر تطوراً للعثور على تسرب الذاكرة مقارنة بالطريقة التي عرضتها. أعتقد في حالتي أن تطبيق هذه الطريقة البسيطة يكفي. لكنني أوصي بأن تتعرف على الموضوع بمزيد من التفاصيل ، على سبيل المثال ، في هذه المقالة: https://habr.com/en/post/309318/ .

UPD # 1
في الوقت الحالي ، تراجع المنشور لمدة يوم تقريبًا. في البداية ذهب إلى إيجابيات وسلبيات ، ثم توقف التقييم عند مستوى الصفر. هذا يعني أن الجمهور تم تقسيمه إلى معسكرين. لا أعرف إذا كان هذا جيدًا أم سيئًا.


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


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


UPD # 4


على العكس من ذلك ، أحث الجهة الأمامية على منح المزيد من الحرية. دعه يقرر كيف يكتب رمزه.

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


UPD # 5
تعليق مهم مع إجابات من سمعة الناس:
https://habr.com/ru/post/479732/#comment_21012620
توجد توصية بتنفيذ عقد "توقيع إلغاء الاشتراك" من مطور RxJS ، ولكن بشكل غير رسمي.

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


All Articles