أساسيات محرك جافا سكريبت: تحسين النموذج الأولي. الجزء 2

مساء الخير أيها الأصدقاء! تم إطلاق الدورة التدريبية "أمان أنظمة المعلومات" ، فيما يتعلق بهذا ، فإننا نشارك معك الجزء الأخير من المقال "أساسيات محركات جافا سكريبت: تحسين النماذج الأولية" ، والتي يمكن قراءة الجزء الأول منها هنا .

نذكرك أيضًا بأن المنشور الحالي هو استمرار لهاتين المادتين: "أساسيات محركات JavaScript: النماذج العامة والتخزين المؤقت المضمّن. الجزء 1 " ، " أساسيات محركات جافا سكريبت: النماذج العامة والتخزين المؤقت المضمّن. الجزء 2 " .



فصول البرمجة والنموذج

الآن بعد أن عرفنا كيفية الوصول السريع إلى خصائص كائنات JavaScript ، يمكننا إلقاء نظرة على البنية الأكثر تعقيدًا لفئات JavaScript. هذا ما يشبه بناء جملة الفصل في JavaScript:

class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } 

على الرغم من أن هذا يبدو وكأنه مفهوم جديد نسبيًا لجافا سكريبت ، إلا أنه مجرد "سكر نحوي" لبرمجة النموذج الأولي الذي تم استخدامه دائمًا في JavaScript:

 function Bar(x) { this.x = x; } Bar.prototype.getX = function getX() { return this.x; }; 

نحن هنا getX خاصية getX لكائن getX . سيعمل هذا تمامًا مثل أي كائن آخر ، نظرًا لأن النماذج الأولية في JavaScript هي نفس الكائنات. في لغات برمجة النماذج الأولية مثل JavaScript ، يتم الوصول إلى الطرق من خلال النماذج الأولية ، بينما يتم تخزين الحقول في حالات محددة.

دعونا نلقي نظرة فاحصة على ما يحدث عندما نخلق مثيل جديد من Bar ، والذي foo عليه foo .

 const foo = new Bar(true); 

يحتوي مثيل تم إنشاؤه باستخدام هذا الرمز على نموذج به خاصية 'x' واحدة. النموذج foo هو Bar.prototype ، الذي ينتمي إلى فئة Bar .



يحتوي Bar.prototype هذا على شكله نفسه ، حيث يحتوي على الخاصية الوحيدة 'getX' ، التي يتم تحديد قيمتها بواسطة الدالة 'getX' ، والتي عندما تُرجع this.x النموذج الأولي Bar.prototype هو Object.prototype ، وهو جزء من لغة JavaScript. Object.prototype هو جذر شجرة النموذج الأولي ، بينما يكون النموذج الأولي الخاص به null .



عندما تنشئ مثيلًا جديدًا من نفس الفئة ، يكون لكلتا الحالتين نفس الشكل ، كما فهمنا بالفعل. Bar.prototype كلتا الحالتين إلى نفس الكائن Bar.prototype .

خصائص الوصول إلى النموذج الأولي

حسنًا ، نحن نعرف الآن ماذا يحدث عندما نحدد فئة ونخلق مثيلًا جديدًا. ولكن ماذا يحدث إذا نسميها الطريقة في المثال ، كما فعلنا في المثال التالي؟

 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); // ^^^^^^^^^^ 

يمكنك اعتبار أي طريقة استدعاء كخطوتين منفصلتين:

 const x = foo.getX(); // is actually two steps: const $getX = foo.getX; const x = $getX.call(foo); 

تتمثل الخطوة الأولى في تحميل الأسلوب ، والذي يعد في الواقع خاصية للنموذج الأولي (قيمته دالة). الخطوة الثانية هي استدعاء دالة ذات مثيل ، على سبيل المثال ، قيمة this . دعونا نلقي نظرة فاحصة على الخطوة الأولى حيث يتم getX طريقة getX من مثيل foo .



يبدأ المحرك مثيل foo ويدرك أن النموذج foo ليس له 'getX' ، لذلك يجب أن يمر عبر سلسلة النموذج الأولي للعثور عليه. نصل إلى Bar.prototype ، وننظر إلى نموذج النموذج الأولي ، ونرى أنه يحتوي على خاصية 'getX' عند الإزاحة الصفرية. نحن نبحث عن القيمة في هذا الإزاحة في Bar.prototype ونجد JSFunction getX الذي كنا نبحث عنه.

تسمح مرونة JavaScript بتغيير روابط سلسلة النماذج الأولية ، على سبيل المثال:

 const foo = new Bar(true); foo.getX(); // → true Object.setPrototypeOf(foo, null); foo.getX(); // → Uncaught TypeError: foo.getX is not a function 

في هذا المثال ، نسميه
 foo.getX() 
مرتين ، لكن في كل مرة يكون لها معاني ونتائج مختلفة تمامًا. هذا هو السبب ، على الرغم من أن النماذج الأولية هي مجرد كائنات في JavaScript ، فإن تسريع الوصول إلى خصائص النموذج الأولي يعد مهمة أكثر أهمية لمحركات JavaScript من تسريع وصولها إلى خصائص الكائنات العادية.

في الممارسة اليومية ، يعد تحميل خصائص النموذج الأولي عملية شائعة إلى حد ما: يحدث هذا في كل مرة تتصل فيها بأسلوب ما!

 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); // ^^^^^^^^^^ 

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



للقيام بذلك بسرعة مع التنزيلات المتكررة في هذه الحالة بالذات ، تحتاج إلى معرفة الأشياء الثلاثة التالية:

  • النموذج foo لا يحتوي على 'getX' ولم يتغير. هذا يعني أنه لم يقم أي شخص بتغيير كائن foo عن طريق إضافة أو إزالة خاصية أو تغيير إحدى سمات الخصائص.
  • النموذج foo لا يزال Bar.prototype الأصلي. لذلك لم Object.setPrototypeOf() أي شخص بتغيير النموذج الأولي باستخدام Object.setPrototypeOf() أو تعيينه إلى خاصية _proto_ الخاصة.
  • يحتوي نموذج Bar.prototype على 'getX' ولم يتغير. هذا يعني أن أحدا لم يغير Bar.prototype عن طريق إضافة أو إزالة خاصية أو تغيير إحدى خصائص الخصائص.

في الحالة العامة ، يعني هذا أنك بحاجة إلى إجراء فحص واحد للمثيل نفسه وإجراء فحصين إضافيين لكل نموذج أولي يصل إلى النموذج الأولي الذي يحتوي على الخاصية المطلوبة. 1 + 2N الاختبارات ، حيث N هو عدد النماذج الأولية المستخدمة ، لا يبدو سيئا للغاية في هذه الحالة ، لأن سلسلة النموذج الأولي ضحلة نسبيا. ومع ذلك ، يتعين على المحركات في كثير من الأحيان التعامل مع سلاسل النموذج الأولي الطويلة ، كما هو الحال مع فئات DOM المعتادة. على سبيل المثال:

 const anchor = document.createElement('a'); // → HTMLAnchorElement const title = anchor.getAttribute('title'); 

لدينا HTMLAnchorElement getAttribute() طريقة getAttribute() . سلسلة هذا العنصر البسيط تتضمن بالفعل 6 نماذج أولية! معظم أساليب DOM التي تهمنا ليست في نموذج HTMLAnchorElement ، ولكن في مكان ما في السلسلة.



طريقة getAttribute() موجودة في Element.prototype . هذا يعني أنه في كل مرة نسميها anchor.getAttribute() ، يحتاج محرك JavaScript إلى:

  1. تحقق من أن 'getAttribute' ليس 'getAttribute' في حد ذاته ؛
  2. تحقق من أن النموذج الأولي هو HTMLAnchorElement.prototype ؛
  3. تأكيد عدم وجود 'getAttribute' هناك ؛
  4. تحقق من أن النموذج الأولي التالي هو HTMLElement.prototype ؛
  5. تأكيد غياب 'getAttribute' ؛
  6. تحقق من أن النموذج الأولي التالي هو Element.prototype ؛
  7. تحقق من وجود 'getAttribute' فيه.

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

بالعودة إلى مثال سابق قمنا فيه بثلاثة عمليات تحقق فقط عند طلب 'getX' لـ foo :

 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const $getX = foo.getX; 

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



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

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

خلايا الصلاحية

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



ValidityCell تعطيل ValidityCell كل مرة يقوم فيها شخص بتغيير النموذج الأولي المرتبط به أو أي نموذج أولي آخر فوقه. دعونا نرى كيف يعمل.
لتسريع تنزيلات النماذج الأولية اللاحقة ، يضع V8 ذاكرة التخزين المؤقت في السطر في موقع من أربعة مجالات:



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

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



عند تغيير النموذج الأولي ، يتم تمييز نموذج جديد ، ويتم تعطيل خلية ValidityCell السابقة. لهذا السبب ، يتم تخطي ذاكرة التخزين المؤقت المضمنة في المرة التالية التي يبدأ فيها ، مما يؤدي إلى ضعف الأداء.

دعنا نعود إلى المثال مع عنصر DOM. لا يؤدي كل تغيير في Object.prototype إبطال التخزين المؤقت المضمن فقط لـ Object.prototype ، ولكن أيضًا لأي نموذج أولي في السلسلة أدناه ، بما في ذلك EventTarget.prototype و Node.prototype و Element.prototype وما إلى ذلك ، حتى HTMLAnchorElement.prototype نفسه.



في الواقع ، يعد تعديل Object.prototype أثناء تنفيذ التعليمات البرمجية خسارة فظيعة في الأداء. لا تفعل هذا!

دعونا نلقي نظرة على مثال محدد لفهم كيفية عمل هذا بشكل أفضل. لنفترض أن لدينا فئة Bar و loadX تستدعي طريقة على كائنات من النوع Bar . ندعو وظيفة loadX عدة مرات مع مثيلات من نفس الفئة.

 class Bar { /* … */ } function loadX(bar) { return bar.getX(); // IC for 'getX' on `Bar` instances. } loadX(new Bar(true)); loadX(new Bar(false)); // IC in `loadX` now links the `ValidityCell` for // `Bar.prototype`. Object.prototype.newMethod = y => y; // The `ValidityCell` in the `loadX` IC is invalid // now, because `Object.prototype` changed. 

يشير التخزين المؤقت المضمن في loadX الآن إلى ValidityCell لـ Bar.prototype . إذا قمت بعد ذلك بتعديل Object.prototype ( Object.prototype ) ، والذي هو جذر كل النماذج الأولية في JavaScript ، يصبح ValidityCell غير صالح ولن يتم استخدام ذاكرة التخزين المؤقت المضمنة الموجودة في المرة القادمة ، مما يؤدي إلى ضعف الأداء.

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

 Object.prototype.foo = function() { /* … */ }; // Run critical code: someObject.foo(); // End of critical code. delete Object.prototype.foo; 

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

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

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

نحن التعميم

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

الجزء الأول

هل كانت هذه السلسلة من المنشورات مفيدة لك؟ اكتب في التعليقات.

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


All Articles