
هذا هو الجزء الثاني من تحليل المهام من جناحنا في مؤتمر
HolyJS ، الذي عقد في سان بطرسبرج في 24-25 مايو. لسياق أكبر ، يوصى أولاً بقراءة
الجزء الأول من هذه المادة. وإذا كان قد تم بالفعل الانتهاء من
العد التنازلي التعبير ، ثم مرحبا بكم في الخطوة التالية.
على عكس الظلامية في المهمة الأولى ، فإن الاثنين المقبلين لديهما بالفعل بعض التلميح إلى قابلية تطبيق التطبيقات العادية في الحياة. جافا سكريبت لا تزال تتطور بسرعة كبيرة والحلول للمشاكل المقترحة تسلط الضوء على بعض الميزات الجديدة للغة.
المهمة 2 ~ تم بواسطة تلك
كان من المفترض أن يتم تنفيذ التعليمات البرمجية وطباعة الإجابات على وحدة التحكم استجابةً لثلاثة طلبات ، ثم "تم". ولكن حدث خطأ ما ... تصحيح الوضع.
;(function() { let iter = { [Symbol.iterator]: function* iterf() { let fs = ['/1', '/2', '/3']; for (const req in fs) { yield fetch(req); } } }; for (const res of iter) { console.log(res); } console.log('done'); })();
مشكلة البحث
ماذا لدينا هنا؟ هذا هو كائن
iter قابل للتكرار يحتوي على رمز
Symbol.iterator معروف معروف من خلال
دالة منشئ . يتم الإعلان عن صفيف
fs في نص الوظيفة ، حيث تقع عناصره في دالة
الجلب بدورها لإرسال طلب ويتم إرجاع نتيجة كل استدعاء دالة خلال
العائد . ما هي الطلبات التي ترسلها وظيفة
الجلب ؟ جميع عناصر الصفيف
fs هي مسارات نسبية للموارد مع الأرقام 1 و 2 و 3 ، على التوالي. لذلك سيتم الحصول على عنوان URL الكامل عن طريق سَلسَلة site.origin بالرقم التالي ، على سبيل المثال:
GET https://www.example.com/1
بعد ذلك ، نريد تكرار كائن
iter من خلال
for-of ،
من أجل تنفيذ كل طلب بدوره مع إخراج النتيجة ، بعد كل شيء - print "done". لكن هذا لا يعمل! المشكلة هي أن
الجلب شيء غير متزامن ويعيد الوعود ، وليس استجابة. لذلك ، في وحدة التحكم ، سنرى شيئًا مثل هذا:
Promise {pending}
Promise {pending}
Promise {pending}
done
في الواقع ، تكمن المهمة في حل هذه الوعود نفسها.
لدينا متزامن / تنتظر
قد تكون الفكرة الأولى هي اللعب مع
Promise.all : إعطائها
كائننا القابل للتكرار ،
ثم الإخراج إلى وحدة التحكم "تم". لكنه لن يزودنا بتنفيذ متسلسل للطلبات (كما هو مطلوب من قبل الشرط) ، ولكن ببساطة أرسلها جميعًا وانتظر الإجابة الأخيرة قبل القرار العام.
سيكون الحل الأبسط هنا في
انتظار النص
الكامل لانتظار الوعد التالي قبل الإخراج إلى وحدة التحكم:
for (const res of iter) { console.log(await res); }
لكي
تنتظر العمل و "تم الانتهاء" ليتم عرضها في النهاية ، تحتاج إلى جعل الوظيفة الرئيسية غير متزامنة عبر
المزامنة :
;(async function() { let iter = { }; for (const res of iter) { console.log(await res); } console.log('done'); })();
في هذه الحالة ، تم حل المشكلة بالفعل (تقريبًا):
GET 1st
Response 1st
GET 2nd
Response 2nd
GET 3rd
Response 3rd
done
مكرر غير متزامن ومولد
سنترك الوظيفة الرئيسية غير متزامنة ، ولكن في
انتظار وجود مكان أكثر أناقة في هذه المهمة من في
for-of body: هذا هو استخدام التكرار غير المتزامن من خلال
for-wait-of ، وهي:
for await (const res of iter) { console.log(res); }
كل شيء سوف يعمل! ولكن إذا انتقلت إلى وصف
هذا الاقتراح حول التكرار غير المتزامن ، فإليك ما يثير الاهتمام:
نقدم صيغة مختلفة لبيان التكرار الذي يتكرر على الكائنات القابلة للتكرار غير المتزامنة. لا يسمح باستخدام Async for-of statement إلا في وظائف المتزامن ووظائف إنشاء المتزامن
بمعنى ، يجب ألا يكون
كائننا متكرراً فحسب ، بل
" غير
متزامن" من خلال الرمز الجديد
المعروف Symbol.asyncIterator ، وفي حالتنا بالفعل ، وظيفة مولد غير متزامن:
let iter = { [Symbol.asyncIterator]: async function* iterf() { let fs = ['/1', '/2', '/3']; for (const req in fs) { yield await fetch(req); } } };
كيف يعمل بعد ذلك على مكرر ومولد منتظم؟ نعم ، ضمنيًا ، مثل الكثير في هذه اللغة. هذا
الانتظار أمر صعب: إذا كان الكائن
قابلاً للتكرار فقط ، فعند التكرار غير المتزامن ، "يحول" الكائن إلى غير
متزامن عن طريق لف العناصر (إذا لزم الأمر) في
وعد مع توقع الدقة. وتحدث بمزيد من التفصيل
في مقال بقلم أكسل راوشماير .
ربما ، من خلال
Symbol.asyncIterator ،
سيظل الأمر أكثر صحة ، لأننا
صممنا الكائن غير المتزامن بشكل صريح للتكرار غير المتزامن من خلال
انتظار ، مع ترك الفرصة لتكملة الكائن بتكرار منتظم لـ
لـ ، إذا لزم الأمر. إذا كنت تريد أن تقرأ شيئًا مفيدًا وكافيًا في مقال واحد حول التكرارات غير المتزامنة في جافا سكريبت ،
فهذه هي !
لا يزال عدم التزامن
لـ - في مسودة جزئيًا ، لكنه معتمد بالفعل من قبل المتصفحات الحديثة (باستثناء Edge) و Node.js من الإصدار 10.x. إذا كان هذا يزعج شخصًا ما ، فيمكنك دائمًا كتابة polyphile الصغيرة الخاصة بك للحصول على سلسلة من الوعود ، على سبيل المثال ، لكائن قابل
للتكرار :
const chain = (promises, callback) => new Promise(resolve => function next(it) { let i = it.next(); i.done ? resolve() : i.value.then(res => { callback(res); next(it); }); }(promises[Symbol.iterator]()) ); ;(async function() { let iter = { }; await chain(iter, console.log); console.log('done'); })();
وبهذه الطرق وبهذه الطريقة ، اكتشفنا إرسال الطلبات ومعالجة الردود بدورها. ولكن في هذه المشكلة ، هناك مشكلة واحدة صغيرة ولكنها مزعجة ...
اختبار الذهن
لقد انتابنا كل هذا التزامن بحيث ، كما يحدث غالبًا ، فقدنا تفصيلًا صغيرًا واحدًا. هل تم إرسال تلك الطلبات بواسطة برنامجنا النصي؟ دعنا نرى
الشبكة :
GET https://www.example.com/0
GET https://www.example.com/1
GET https://www.example.com/2
لكن أعدادنا هي 1 ، 2 ، 3. كما لو كان الانخفاض قد حدث. لماذا هذا فقط في التعليمات البرمجية المصدر للمهمة هناك مشكلة أخرى مع التكرار ، هنا:
let fs = ['/1', '/2', '/3']; for (const req in fs) { yield fetch(req); }
هنا
يتم استخدام for-in ، والذي بدلاً من قيم الصفيف يتخطى خصائصه المذكورة: وهذه هي مؤشرات العناصر من 0 إلى 2. وظيفة
الجلب لا تزال تقودهم إلى سلاسل ، وعلى الرغم من عدم وجود شرطة مائلة من قبل (لم يعد هذا
مسارًا ) ، فإنه يحل نسبيًا عنوان URL للصفحة الحالية. لإصلاح أسهل بكثير من إشعار. خياران:
let fs = ['/1', '/2', '/3']; for (const req of fs) { yield fetch(req); } let fs = ['/1', '/2', '/3']; for (const req in fs) { yield fetch(fs[req]); }
في الأول ، استخدمنا نفس القيمة
لـ لـ للتكرار على قيم الصفيف ، في الثانية - الوصول إلى عنصر الصفيف حسب الفهرس.
حافز
درسنا 3 حلول: 1) من خلال
انتظار في
for-of- body ، 2) من خلال
for-of-of-of ، و 3) من خلال polyfile لدينا (دالة تكرارية ، خط
أنابيب الأنابيب ، إلخ). من الغريب أن هذه الخيارات قسمت المشاركين في المؤتمر على قدم المساواة تقريبًا ولم يتم الكشف عن أي مفضلات واضحة. في المشروعات الكبيرة ، لمثل هذه المهام الحقيقية ، عادةً ما تستخدم مكتبة تفاعلية (على سبيل المثال ،
RxJS ) ، لكن تجدر الإشارة إلى الميزات الأصلية الحديثة للغة ذات الطبيعة غير المتزامنة.
ما يقرب من نصف المشاركين لم يلاحظوا أخطاء في التكرار على قائمة الموارد ، وهي أيضًا ملاحظة مهمة. بالتركيز على مشكلة غير تافهة ولكن واضحة ، يمكننا بسهولة تخطي هذه التافهة على ما يبدو ، ولكن مع عواقب وخيمة محتملة.
مشكلة 3 ~ عامل 19
كم مرة في سجل الرقم 2019! (مضروب من عام 2019) هل يحدث الرقم 19؟ جنبا إلى جنب مع الجواب ، توفير حل جافا سكريبت.
مشكلة البحث
المشكلة موجودة على السطح: نحتاج إلى سجل عدد كبير جدًا لإيجاد عدد كل الأحداث في السلسلة الفرعية "19". لحل المشكلة على
الرقم ، نواجه بسرعة كبيرة
Infinity (بعد 170) ولم نحصل على أي شيء. بالإضافة إلى ذلك ، يضمن تنسيق تمثيل الأرقام
float64 دقة من 15 إلى 17 حرفًا فقط ، ونحن بحاجة إلى الحصول ليس فقط على سجل كامل ، ولكن أيضًا. وبالتالي ، فإن الصعوبة الرئيسية هي تحديد هيكل لتراكم هذا العدد الكبير.
أعداد كبيرة كبيرة
إذا تابعت ابتكارات اللغة ، فسيتم حل المهمة ببساطة: بدلاً من
رقم الكتابة
، يمكنك استخدام النوع الجديد
BigInt (المرحلة 3) ، والذي يسمح لك بالعمل بأرقام دقة عشوائية. من خلال الوظيفة العودية الكلاسيكية لحساب عوامل
التصفية وإيجاد المطابقات عبر
String.prototype.split ، يبدو الحل الأول كما يلي:
const fn = n => n > 1n ? n * fn(n - 1n) : 1n; console.log(fn(2019n).toString().split('19').length - 1);
ومع ذلك ، يمكن أن
تكون ألفي استدعاءات دالة على المكدس بالفعل
خطيرة . حتى إذا كنت تجلب الحل
لتكرار التكرار ، فلا يزال دعم
مكالمات Tail يدعم Safari فقط. المشكلة الموضعية هنا أكثر متعة لحلها من خلال دورة حسابية أو
Array.prototype.reduce :
console.log([...Array(2019)].reduce((p, _, i) => p * BigInt(i + 1), 1n).toString().match(/19/g).length);
قد يبدو هذا كإجراء طويل بجنون. لكن هذا الانطباع خادع. إذا كنت تقدر ، فإننا نحتاج فقط إلى إنفاق أكثر من ألفي مضاعفة. في i5-4590 3.30 جيجا هرتز في الكروم ، يتم حل المشكلة في المتوسط في 4-5ms (!).
هناك خيار آخر للعثور على التطابقات في سلسلة بنتيجة عملية حسابية وهو
String.prototype.match بواسطة التعبير العادي باستخدام علامة البحث العالمية:
/ 19 / g .
حساب كبير
ولكن ماذا لو لم يكن لدينا
BigInt (
والمكتبات أيضًا) حتى الآن؟ في هذه الحالة ، يمكنك أن تفعل الحساب الطويل نفسك. لحل المشكلة ، يكفي أن ننفذ فقط وظيفة الضرب الكبير بصغير (نضرب بالأرقام من 1 إلى 2019). يمكننا الاحتفاظ بعدد كبير ونتيجة الضرب ، على سبيل المثال ، في السطر:
const mult = (big, int) => { let res = '', carry = 0; for (let i = big.length - 1; i >= 0; i -= 1) { let prod = big[i] * int + carry; res = prod % 10 + res; carry = prod / 10 | 0; } return (carry || '') + res; } console.log([...Array(2019)].reduce((p, _, i) => mult(p, i + 1), '1').match(/19/g).length);
هنا نقوم ببساطة بضرب العمود بالبت من نهاية السطر إلى البداية ، كما تعلمنا في المدرسة. لكن الحل يتطلب بالفعل حوالي 170ms.
يمكننا تحسين الخوارزمية إلى حد ما عن طريق معالجة أكثر من رقم في سجل الأرقام في وقت واحد. للقيام بذلك ، نقوم بتعديل الوظيفة وفي نفس الوقت ننتقل إلى المصفوفات ، حتى لا نلتف حول الخطوط في كل مرة:
const mult = (big, int, digits = 1) => { let res = [], carry = 0, div = 10 ** digits; for (let i = big.length - 1; i >= 0 || carry; i -= 1) { let prod = (i < 0 ? 0 : big[i] * int) + carry; res.push(prod % div); carry = prod / div | 0; } return res.reverse(); }
هنا ، يتم تمثيل الأرقام الكبيرة بمصفوفة ، يقوم كل عنصر بتخزين معلومات حول
أرقام الأرقام من سجل الأرقام ، باستخدام
الرقم . على سبيل المثال ، سيتم تمثيل الرقم 2016201720182019 مع
الأرقام = 3 على النحو التالي:
'2|016|201|720|182|019' => [2,16,201,720,182,19]
عند التحويل إلى سطر قبل صلة ، تحتاج إلى تذكر الأصفار البادئة. تقوم دالة
factor بإرجاع العامل المحسوب بواسطة سلسلة ، باستخدام الدالة
mult مع العدد المحدد من الأرقام المعالجة في وقت واحد في التمثيل "الضخم" للرقم عند الحساب:
const factor = (n, digits = 1) => [...Array(n)].reduce((p, _, i) => mult(p, i + 1, digits), [1]) .map(String) .map(el => '0'.repeat(digits - el.length) + el) .join('') .replace(/^0+/, ''); console.log(factor(2019, 3).match(/19/g).length);
اتضح أن تنفيذ "طول الركبة" من خلال المصفوفات يكون أسرع من خلال السلاسل ، ومع
الأرقام = 1 فإنه يحسب الإجابة بالفعل في المتوسط في 90ms ،
والأرقام = 3 في 35ms ،
والأرقام = 6 في 20ms فقط. ومع ذلك ، تذكر أنه عند زيادة عدد الأرقام ، فإننا نقترب من موقف قد يكون فيه ضرب
الرقم بالرقم "تحت الغطاء" خارج
MAX_SAFE_INTEGER الآمن. يمكنك أن تلعب معها
هنا . ما هي قيمة
الأرقام القصوى التي يمكننا تحملها لهذه المهمة؟
النتائج بالفعل
مؤشّرة تمامًا ،
BigInt سريع جدًا:

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