تحسين معرفتك جافا سكريبت عن طريق قراءة شفرة المصدر
نشرت: 2022-03-10هل تتذكر المرة الأولى التي بحثت فيها بعمق في الكود المصدري لمكتبة أو إطار عمل تستخدمه كثيرًا؟ بالنسبة لي ، جاءت تلك اللحظة خلال وظيفتي الأولى كمطور واجهة منذ ثلاث سنوات.
لقد انتهينا للتو من إعادة كتابة إطار عمل داخلي قديم استخدمناه لإنشاء دورات تعليم إلكتروني. في بداية إعادة الكتابة ، قضينا وقتًا في التحقيق في عدد من الحلول المختلفة بما في ذلك Mithril و Inferno و Angular و React و Aurelia و Vue و Polymer. نظرًا لأنني كنت مبتدئًا جدًا (كنت قد تحولت للتو من الصحافة إلى تطوير الويب) ، أتذكر أنني شعرت بالخوف من تعقيد كل إطار وعدم فهم كيفية عمل كل منها.
نما فهمي عندما بدأت التحقيق في إطار العمل الذي اخترناه ، Mithril ، بعمق أكبر. منذ ذلك الحين ، ساعدت معرفتي بجافا سكريبت - والبرمجة بشكل عام - بشكل كبير من خلال الساعات التي أمضيتها في البحث بعمق في أحشاء المكتبات التي أستخدمها يوميًا إما في العمل أو في مشاريعي الخاصة. في هذا المنشور ، سوف أشارك بعض الطرق التي يمكنك من خلالها أخذ مكتبتك أو إطار العمل المفضل لديك واستخدامه كأداة تعليمية.
فوائد قراءة شفرة المصدر
واحدة من الفوائد الرئيسية لقراءة التعليمات البرمجية المصدر هو عدد الأشياء التي يمكنك تعلمها. عندما نظرت لأول مرة في قاعدة بيانات Mithril ، كانت لدي فكرة غامضة عن ماهية DOM الافتراضي. عندما انتهيت ، توصلت إلى معرفة أن DOM الظاهري هو أسلوب يتضمن إنشاء شجرة من الكائنات التي تصف الشكل الذي يجب أن تبدو عليه واجهة المستخدم الخاصة بك. ثم يتم تحويل هذه الشجرة إلى عناصر DOM باستخدام واجهات برمجة تطبيقات DOM مثل document.createElement
. يتم إجراء التحديثات عن طريق إنشاء شجرة جديدة تصف الحالة المستقبلية لواجهة المستخدم ثم مقارنتها بكائنات من الشجرة القديمة.
لقد قرأت عن كل هذا في العديد من المقالات والبرامج التعليمية ، وبينما كان من المفيد ، أن تكون قادرًا على ملاحظته في العمل في سياق تطبيق قمنا بشحنه كان مفيدًا جدًا بالنسبة لي. علمتني أيضًا الأسئلة التي يجب طرحها عند مقارنة الأطر المختلفة. بدلاً من النظر إلى نجوم GitHub ، على سبيل المثال ، عرفت الآن أن أطرح أسئلة مثل ، "كيف تؤثر طريقة تنفيذ كل إطار للتحديثات على الأداء وتجربة المستخدم؟"
فائدة أخرى هي زيادة تقديرك وفهمك لبنية التطبيق الجيدة. في حين أن معظم المشاريع مفتوحة المصدر تتبع عمومًا نفس الهيكل مع مستودعاتها ، فإن كل منها يحتوي على اختلافات. هيكل Mithril مسطح جدًا وإذا كنت معتادًا على واجهة برمجة التطبيقات الخاصة به ، فيمكنك إجراء تخمينات مستنيرة حول الكود في مجلدات مثل render
router
request
. من ناحية أخرى ، تعكس بنية React بنيتها الجديدة. قام المشرفون بفصل الوحدة المسؤولة عن تحديثات واجهة المستخدم ( react-reconciler
) عن الوحدة المسؤولة عن تقديم عناصر DOM ( react-dom
).
تتمثل إحدى فوائد ذلك في أنه أصبح من السهل الآن على المطورين كتابة برامج العارض المخصصة الخاصة بهم من خلال ربطها بحزمة react-reconciler
. يحتوي Parcel ، وهو مجمع وحدة كنت أدرسه مؤخرًا ، على مجلد packages
مثل React. تسمى الوحدة الرئيسية parcel-bundler
وهي تحتوي على الكود المسؤول عن إنشاء الحزم وتدوير خادم الوحدة النمطية الساخنة وأداة سطر الأوامر.
ميزة أخرى - والتي كانت مفاجأة سارة بالنسبة لي - هي أنك أصبحت أكثر راحة في قراءة مواصفات JavaScript الرسمية التي تحدد كيفية عمل اللغة. كانت المرة الأولى التي قرأت فيها المواصفات عندما كنت أقوم بالتحقيق في الفرق بين throw Error
throw new Error
(تنبيه المفسد - لا يوجد شيء). لقد نظرت في هذا لأنني لاحظت أن Mithril استخدم throw Error
في تنفيذ وظيفته m
وتساءلت عما إذا كانت هناك فائدة من استخدامه على throw new Error
. منذ ذلك الحين ، تعلمت أيضًا أن العوامل المنطقية &&
و ||
لا تُرجع بالضرورة القيم المنطقية ، فقد تم العثور على القواعد التي تحكم كيفية قيام عامل المساواة ==
بفرض القيم والسبب في إرجاع Object.prototype.toString.call({})
'[object Object]'
.
تقنيات لقراءة شفرة المصدر
هناك العديد من الطرق للتعامل مع التعليمات البرمجية المصدر. لقد وجدت أن أسهل طريقة للبدء هي تحديد طريقة من المكتبة التي اخترتها وتوثيق ما يحدث عند تسميتها. لا تقم بتوثيق كل خطوة واحدة ولكن حاول تحديد تدفقها العام وهيكلها.
لقد فعلت ذلك مؤخرًا مع ReactDOM.render
وبالتالي تعلمت الكثير عن React Fiber وبعض الأسباب الكامنة وراء تنفيذها. لحسن الحظ ، نظرًا لأن React هو إطار عمل شائع ، فقد صادفت الكثير من المقالات التي كتبها مطورون آخرون حول نفس المشكلة مما أدى إلى تسريع العملية.
قدمني هذا التعمق أيضًا إلى مفاهيم الجدولة التعاونية وطريقة window.requestIdleCallback
حقيقي للقوائم المرتبطة (يعالج React التحديثات من خلال وضعها في قائمة انتظار وهي قائمة مرتبطة بالتحديثات ذات الأولوية). عند القيام بذلك ، يُنصح بإنشاء تطبيق أساسي للغاية باستخدام المكتبة. هذا يجعل الأمر أسهل عند التصحيح لأنك لست مضطرًا للتعامل مع تتبعات المكدس التي تسببها مكتبات أخرى.
إذا لم أكن أقوم بمراجعة متعمقة ، فسأفتح مجلد /node_modules
في مشروع أعمل عليه أو سأنتقل إلى مستودع GitHub. يحدث هذا عادة عندما أواجه خطأ أو ميزة مثيرة للاهتمام. عند قراءة الكود على GitHub ، تأكد من أنك تقرأ من أحدث إصدار. يمكنك عرض الكود من خلال علامة الإصدار الأحدث بالنقر فوق الزر المستخدم لتغيير الفروع واختيار "العلامات". تخضع المكتبات والأطر لتغييرات إلى الأبد ، لذا لا ترغب في التعرف على شيء قد يتم إسقاطه في الإصدار التالي.
هناك طريقة أخرى أقل انخراطًا في قراءة التعليمات البرمجية المصدر وهي ما أحب أن أسميه طريقة "النظرة السريعة". في وقت مبكر عندما بدأت في قراءة الكود ، قمت بتثبيت express.js ، وفتحت مجلد /node_modules
على تبعياته. إذا لم يقدم لي برنامج README
شرحًا مرضيًا ، فقد قرأت المصدر. قادني القيام بذلك إلى هذه النتائج المثيرة للاهتمام:
- يعتمد Express على وحدتين تدمج كلتاهما الكائنات ولكنهما يقومان بذلك بطرق مختلفة جدًا. تضيف
merge-descriptors
تم العثور عليها مباشرة على الكائن المصدر ، كما أنها تدمج أيضًا الخصائص غير القابلة للتعداد بينما تتكرّرutils-merge
فقط عبر خصائص الكائن التي يمكن تعدادها وكذلك تلك الموجودة في سلسلة النموذج الأولي الخاص بها. تستخدمmerge-descriptors
Object.getOwnPropertyNames()
وObject.getOwnPropertyDescriptor()
بينما تستخدمutils-merge
for..in
؛ - توفر الوحدة النمطية
setprototypeof
طريقة عبر النظام الأساسي لتعيين النموذج الأولي لكائن تم إنشاء مثيل له ؛ -
escape-html
عبارة عن وحدة مكونة من 78 سطرًا للهروب من سلسلة من المحتوى بحيث يمكن إقحامها في محتوى HTML.
في حين أنه من غير المحتمل أن تكون النتائج مفيدة على الفور ، فإن الحصول على فهم عام للتبعيات التي تستخدمها مكتبتك أو إطار العمل الخاص بك أمر مفيد.
عندما يتعلق الأمر بتصحيح كود الواجهة الأمامية ، فإن أدوات تصحيح الأخطاء في متصفحك هي أفضل صديق لك. من بين أشياء أخرى ، تسمح لك بإيقاف البرنامج في أي وقت وفحص حالته أو تخطي تنفيذ الوظيفة أو الدخول إليها أو الخروج منها. في بعض الأحيان لن يكون هذا ممكنًا على الفور لأنه تم تصغير الرمز. أميل إلى إلغاء تصغيره ونسخ الكود غير المصغر إلى الملف ذي الصلة في المجلد /node_modules
.
دراسة حالة: وظيفة Redux's Connect
React-Redux هي مكتبة تستخدم لإدارة حالة تطبيقات React. عند التعامل مع المكتبات الشعبية مثل هذه ، أبدأ بالبحث عن المقالات التي تم كتابتها حول تنفيذها. أثناء القيام بذلك من أجل دراسة الحالة هذه ، صادفت هذا المقال. هذا شيء جيد آخر حول قراءة الكود المصدري. عادة ما تقودك مرحلة البحث إلى مقالات إعلامية مثل هذه التي تعمل فقط على تحسين تفكيرك وفهمك.
connect
هي وظيفة React-Redux التي تربط مكونات React بمتجر Redux للتطبيق. كيف؟ حسنًا ، وفقًا للمستندات ، يقوم بما يلي:
"... يعيد فئة مكون جديدة متصلة تغلف المكون الذي مررته."
بعد قراءة هذا ، أود أن أطرح الأسئلة التالية:
- هل أعرف أي أنماط أو مفاهيم تأخذ فيها الوظائف مدخلاً ثم تعيد نفس الإدخال مغلفًا بوظائف إضافية؟
- إذا كنت أعرف أيًا من هذه الأنماط ، فكيف يمكنني تنفيذها بناءً على الشرح الوارد في المستندات؟
عادةً ما تكون الخطوة التالية هي إنشاء مثال أساسي لتطبيق يستخدم connect
. ومع ذلك ، اخترت في هذه المناسبة استخدام تطبيق React الجديد الذي نقوم ببنائه في Limejump لأنني أردت أن أفهم connect
ضمن سياق تطبيق سينتقل في النهاية إلى بيئة إنتاج.
المكون الذي أركز عليه يبدو كالتالي:
class MarketContainer extends Component { // code omitted for brevity } const mapDispatchToProps = dispatch => { return { updateSummary: (summary, start, today) => dispatch(updateSummary(summary, start, today)) } } export default connect(null, mapDispatchToProps)(MarketContainer);
إنه مكون حاوية يلتف بأربعة مكونات متصلة أصغر. من أول الأشياء التي تصادفها في الملف الذي يصدر طريقة connect
هذا التعليق: connect هو واجهة عبر connectAdvanced . بدون الذهاب بعيدًا ، لدينا أول لحظة تعلم لدينا: فرصة لمراقبة نمط تصميم الواجهة أثناء العمل . في نهاية الملف ، نرى أن connect
يصدر استدعاءًا لوظيفة تسمى createConnect
. معلماته عبارة عن مجموعة من القيم الافتراضية التي تم تدميرها على النحو التالي:
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {})
مرة أخرى ، نواجه لحظة تعلم أخرى: تصدير الوظائف المستدعاة وتدمير وسيطات الوظيفة الافتراضية . جزء التدمير هو لحظة تعلم لأنه قد تمت كتابة الكود على النحو التالي:
export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory })
كان من الممكن أن يؤدي إلى هذا الخطأ خطأ Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'.
هذا لأن الوظيفة ليس لديها وسيطة افتراضية للرجوع إليها.
ملاحظة : لمزيد من المعلومات حول هذا الموضوع ، يمكنك قراءة مقالة ديفيد والش. قد تبدو بعض لحظات التعلم تافهة ، اعتمادًا على معرفتك باللغة ، ولذلك قد يكون من الأفضل التركيز على الأشياء التي لم ترها من قبل أو تحتاج إلى معرفة المزيد عنها.
createConnect
نفسه لا يفعل شيئًا في جسم وظيفته. تقوم بإرجاع وظيفة تسمى connect
، تلك التي استخدمتها هنا:
export default connect(null, mapDispatchToProps)(MarketContainer)
يتطلب الأمر أربع وسيطات ، جميعها اختيارية ، ويمر كل من الوسيطات الثلاث الأولى عبر دالة match
تساعد في تحديد سلوكها وفقًا لما إذا كانت الوسيطات موجودة ونوع قيمتها. الآن ، نظرًا لأن الوسيطة الثانية المقدمة match
هي واحدة من ثلاث وظائف تم استيرادها إلى connect
، فلا بد لي من تحديد الخيط الذي يجب اتباعه.
هناك لحظات تعلم باستخدام وظيفة الوكيل المستخدمة في التفاف الوسيطة الأولى connect
إذا كانت هذه الوسائط عبارة عن وظائف ، أو الأداة المساعدة isPlainObject
المستخدمة للتحقق من الكائنات العادية أو الوحدة النمطية warning
التي تكشف عن كيفية ضبط مصحح الأخطاء الخاص بك لكسر جميع الاستثناءات. بعد وظائف المطابقة ، نصل إلى connectHOC
، وهي الوظيفة التي تأخذ مكون React الخاص بنا وتربطه بـ Redux. إنها استدعاء دالة أخرى تقوم بإرجاع wrapWithConnect
، وهي الوظيفة التي تتعامل بالفعل مع توصيل المكون بالمخزن.
بالنظر إلى تنفيذ connectHOC
، يمكنني أن أدرك سبب حاجته إلى connect
لإخفاء تفاصيل التنفيذ الخاصة به. إنه قلب React-Redux ويحتوي على منطق لا يحتاج إلى كشفه عبر connect
. على الرغم من أنني سأنهي الغوص العميق هنا ، فلو تابعت ، كان هذا هو الوقت المثالي لاستشارة المواد المرجعية التي وجدتها سابقًا لأنها تحتوي على شرح مفصل بشكل لا يصدق لقاعدة الشفرات.
ملخص
قراءة الكود المصدري صعبة في البداية ولكن كما هو الحال مع أي شيء ، تصبح أسهل بمرور الوقت. الهدف ليس فهم كل شيء ولكن الخروج بمنظور مختلف ومعرفة جديدة. المفتاح هو أن تكون متداولًا بشأن العملية برمتها وأن تكون مهتمًا بشدة بكل شيء.
على سبيل المثال ، وجدت أن دالة isPlainObject
مثيرة للاهتمام لأنها تستخدم هذا if (typeof obj !== 'object' || obj === null) return false
للتأكد من أن الوسيطة المعطاة هي كائن عادي. عندما قرأت تطبيقه لأول مرة ، تساءلت عن سبب عدم استخدامه Object.prototype.toString.call(opts) !== '[object Object]'
، وهو رمز أقل ويميز بين الكائنات وأنواع الكائنات الفرعية مثل التاريخ هدف. ومع ذلك ، كشفت قراءة السطر التالي أنه في حالة احتمال قيام مطور باستخدام connect
بإرجاع كائن تاريخ ، على سبيل المثال ، سيتم التعامل مع هذا بواسطة Object.getPrototypeOf(obj) === null
.
جزء آخر من المؤامرات في isPlainObject
هو هذا الكود:
while (Object.getPrototypeOf(baseProto) !== null) { baseProto = Object.getPrototypeOf(baseProto) }
قادني بعض عمليات البحث في Google إلى موضوع StackOverflow هذا ومسألة Redux التي تشرح كيفية معالجة هذا الرمز لحالات مثل التحقق من الكائنات التي تنشأ من iFrame.
روابط مفيدة لقراءة شفرة المصدر
- "كيفية عكس هندسة الأطر ،" ماكس كوريتسكي ، متوسط
- "كيف تقرأ الكود" ، أريا ستيوارت ، جيثب