مساء الخير أيها الأصدقاء!
تم إطلاق الدورة التدريبية
"أمان أنظمة المعلومات" ، فيما يتعلق بهذا ، فإننا نشارك معك الجزء الأخير من المقال "أساسيات محركات جافا سكريبت: تحسين النماذج الأولية" ، والتي يمكن قراءة الجزء الأول منها
هنا .
نذكرك أيضًا بأن المنشور الحالي هو استمرار لهاتين المادتين:
"أساسيات محركات 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();
تتمثل الخطوة الأولى في تحميل الأسلوب ، والذي يعد في الواقع خاصية للنموذج الأولي (قيمته دالة). الخطوة الثانية هي استدعاء دالة ذات مثيل ، على سبيل المثال ، قيمة
this
. دعونا نلقي نظرة فاحصة على الخطوة الأولى حيث يتم
getX
طريقة
getX
من مثيل
foo
.

يبدأ المحرك مثيل
foo
ويدرك أن النموذج
foo
ليس له
'getX'
، لذلك يجب أن يمر عبر سلسلة النموذج الأولي للعثور عليه. نصل إلى
Bar.prototype
، وننظر إلى نموذج النموذج الأولي ، ونرى أنه يحتوي على خاصية
'getX'
عند الإزاحة الصفرية. نحن نبحث عن القيمة في هذا الإزاحة في
Bar.prototype
ونجد
JSFunction getX
الذي كنا نبحث عنه.
تسمح مرونة JavaScript بتغيير روابط سلسلة النماذج الأولية ، على سبيل المثال:
const foo = new Bar(true); foo.getX();
في هذا المثال ، نسميه
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
getAttribute()
طريقة
getAttribute()
. سلسلة هذا العنصر البسيط تتضمن بالفعل 6 نماذج أولية! معظم أساليب DOM التي تهمنا ليست في نموذج
HTMLAnchorElement
، ولكن في مكان ما في السلسلة.

طريقة
getAttribute()
موجودة في
Element.prototype
. هذا يعني أنه في كل مرة نسميها
anchor.getAttribute()
، يحتاج محرك JavaScript إلى:
- تحقق من أن
'getAttribute'
ليس 'getAttribute'
في حد ذاته ؛ - تحقق من أن النموذج الأولي هو
HTMLAnchorElement.prototype
؛ - تأكيد عدم وجود
'getAttribute'
هناك ؛ - تحقق من أن النموذج الأولي التالي هو
HTMLElement.prototype
؛ - تأكيد غياب
'getAttribute'
؛ - تحقق من أن النموذج الأولي التالي هو
Element.prototype
؛ - تحقق من وجود
'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();
يشير التخزين المؤقت المضمن في
loadX
الآن إلى
ValidityCell
لـ
Bar.prototype
. إذا قمت بعد ذلك بتعديل
Object.prototype
(
Object.prototype
) ، والذي هو جذر كل النماذج الأولية في JavaScript ، يصبح
ValidityCell
غير صالح ولن يتم استخدام ذاكرة التخزين المؤقت المضمنة الموجودة في المرة القادمة ، مما يؤدي إلى ضعف الأداء.
يعد تغيير
Object.prototype
دائمًا فكرة سيئة ، لأنه يبطل أي ذاكرة تخزين مؤقت مضمّنة لنماذج تم تحميلها في وقت التغيير. فيما يلي مثال على كيفية عدم القيام بما يلي:
Object.prototype.foo = function() { };
نحن الآن بصدد توسيع
Object.prototype
، مما يؤدي إلى إبطال جميع ذاكرات التخزين المؤقت النموذج الأولي المضمّنة بواسطة المحرك في هذه المرحلة. ثم سنقوم بتشغيل بعض الأكواد التي تستخدم الطريقة التي وصفناها. يجب أن يبدأ المحرك من البداية وتكوين ذاكرة التخزين المؤقت المضمنة لأي وصول إلى خاصية النموذج الأولي. ثم ، أخيرًا ، "نظف" وأزل طريقة النموذج الأولي التي أضفناها سابقًا.
تعتقد أن التنظيف فكرة جيدة ، أليس كذلك؟ حسنًا ، في هذه الحالة ، سيزيد الوضع سوءًا! تؤدي إزالة الخصائص إلى تغيير
Object.prototype
، بحيث يتم تعطيل جميع ذاكرات التخزين المؤقت المضمنة مرة أخرى ، ويجب أن يبدأ المحرك في العمل من البداية مرة أخرى.
لتلخيص . على الرغم من أن النماذج الأولية مجرد كائنات ، إلا أن محركات JavaScript تتم معالجتها بشكل خاص من أجل تحسين أداء عمليات البحث عن طريق النماذج الأولية.
اترك النماذج وحدها! أو إذا كنت بحاجة حقًا إلى التعامل معهم ، فقم بذلك قبل تنفيذ الكود ، لذلك لن تقوم على الأقل بإبطال جميع المحاولات لتحسين الكود أثناء تنفيذه!
نحن التعميم
لقد تعلمنا كيف تقوم JavaScript بتخزين الكائنات والفئات ، وكيف تساعد النماذج والتخزين المؤقت المضمّن وخلايا الصلاحية في تحسين عمليات النموذج الأولي. بناءً على هذه المعرفة ، فهمنا كيفية تحسين الأداء من وجهة نظر عملية: لا تلمس النماذج الأولية! (أو إذا كنت في حاجة إليها حقًا ، فقم بذلك قبل تنفيذ الكود).
←
الجزء الأولهل كانت هذه السلسلة من المنشورات مفيدة لك؟ اكتب في التعليقات.