مرحبا بالجميع!
كما تتذكر ، في
شهر أكتوبر ، قمنا بترجمة مقال مثير للاهتمام حول استخدام أجهزة ضبط الوقت في Javascript. تسبب ذلك في نقاش كبير ، وفقًا للنتائج التي طالما أردنا العودة إليها ، ونقدم لك تحليلًا تفصيليًا للبرمجة غير المتزامنة في هذه اللغة. يسعدنا أننا تمكنا من العثور على مواد لائقة ونشرها قبل نهاية العام. هل لديك قراءة لطيفة!
مرت البرمجة غير المتزامنة في جافاسكريبت بتطور متعدد المراحل: من عمليات الاسترجاعات إلى الوعود والمزيد إلى المولدات ، وقريباً
async/await
. في كل مرحلة ، كانت البرمجة غير المتزامنة في جافاسكريبت مبسطة قليلاً بالنسبة لأولئك الذين راكعوا بالفعل في هذه اللغة ، ولكن بالنسبة للمبتدئين أصبح الأمر أكثر إثارة للخوف ، لأنه كان من الضروري فهم الفروق الدقيقة في كل نموذج ، وإتقان تطبيق كل منهم ، وفهم إتقان تطبيقه ، كيف يعمل كل شيء.
في هذه المقالة ، قررنا أن نتذكر بإيجاز كيفية استخدام عمليات الاسترجاعات والوعود ، وتقديم مقدمة مختصرة للمولدات ، ثم نساعدك على فهم حدسي تمامًا كيف يتم ترتيب البرمجة غير المتزامنة "تحت الغطاء" مع المولدات وتزامن / انتظار. نأمل أن تتمكن بهذه الطريقة من تطبيق النماذج المختلفة بثقة أينما كانت مناسبة.
من المفترض أن يكون القارئ قد استخدم بالفعل عمليات الاسترجاعات والوعود والمولدات للبرمجة غير المتزامنة ، كما أنه مألوف تمامًا بالإغلاق والكاري في Javascript.
رد الجحيمفي البداية ، كانت هناك عمليات الاسترجاعات. لا يحتوي Javascript على I / O متزامن (يشار إليه فيما يلي I / O) والحظر غير معتمد على الإطلاق. لذلك ، لتنظيم أي إدخال / إخراج أو لتأجيل أي إجراء ، تم اختيار هذه الاستراتيجية: تم تمرير الكود المطلوب تنفيذه بشكل غير متزامن إلى الوظيفة مع التنفيذ المؤجل ، والذي تم إطلاقه في مكان ما أدناه في حلقة الحدث. رد الاتصال واحد ليس سيئًا للغاية ، ولكن الرمز ينمو ، وعادة ما ينتج عن عمليات رد الاتصال رد اتصال جديد. والنتيجة هي شيء مثل هذا:
getUserData(function doStuff(e, a) { getMoreUserData(function doMoreStuff(e, b) { getEvenMoreUserData(function doEvenMoreStuff(e, c) { getYetMoreUserData(function doYetMoreStuff(e, c) { console.log('Welcome to callback hell!'); }); }); }); })
بصرف النظر عن نتوءات الأوز عند رؤية مثل هذا الرمز النمطي هندسي متكرر ، هناك مشكلة أخرى: لقد قمنا الآن بتفويض التحكم في منطق
do*Stuff
إلى وظائف أخرى (
get*UserData()
) ، والتي قد لا تكون لديك شفرة مصدر ، وقد لا تكون لديك بالتأكيد إذا كانوا يؤدون رد الاتصال الخاص بك. عظيم ، أليس كذلك؟
الوعودتعد الوعود بعكس اتجاه التحكم الذي توفره عمليات الاسترجاعات وتساعد في كشف تشابك عمليات الاسترجاعات في سلسلة سلسة.
الآن يمكن تحويل المثال السابق إلى شيء مثل هذا:
getUserData() .then(getUserData) .then(doMoreStuff) .then(getEvenMoreUserData) .then(doEvenMoreStuff) .then(getYetMoreUserData) .then(doYetMoreStuff);
بالفعل ليست قبيحة جدا ، هاه؟
لكن دعني !!! دعنا ننظر إلى مثال رد اتصال أكثر حيوية (ولكن لا يزال مفتقدًا إلى حد كبير):
لذلك ، نختار ملف تعريف المستخدم ، ثم اهتماماته ، وبناءً على اهتماماته ، نختار التوصيات ، وأخيراً ، بعد أن جمعنا كل التوصيات ، نعرض الصفحة. هذه المجموعة من عمليات الاسترجاعات ، والتي ، على الأرجح ، يمكن أن تفخر بها ، لكنها مع ذلك أشعث إلى حد ما. لا شيء ، قم بتطبيق الوعود هنا - وكل شيء سوف ينجح. صحيح؟
دعونا نغير طريقة
fetchJson()
حتى تُرجع الوعد بدلاً من قبول رد الاتصال. يتم حل الوعد بواسطة هيئة استجابة موزعة بتنسيق JSON.
fetchJson('/api/user/self') .then(function (user) { return fetchJson('/api/user/interests?userId=' + self.id); }) .then(function (interests) { return Promise.all[interests.map(i => fetchJson('/api/recommendations?topic=' + i))]; }) .then(function (recommendations) { render(user, interests, recommendations); });
لطيف ، أليس كذلك؟ ما هو الخطأ في هذا الرمز الآن؟
عفوًا ... ..
ليس لدينا إمكانية الوصول إلى الملف الشخصي أو الاهتمامات في الوظيفة الأخيرة من هذه السلسلة؟ لذلك لا شيء يعمل! ماذا تفعل؟ لنجرب الوعود المتداخلة:
fetchJson('/api/user/self') .then(function (user) { return fetchJson('/api/user/interests?userId=' + self.id) .then(interests => { user: user, interests: interests }); }) .then(function (blob) { return Promise.all[blob.interests.map(i => fetchJson('/api/recommendations?topic=' + i))] .then(recommendations => { user: blob.user, interests: blob.interests, recommendations: recommendations }); }) .then(function (bigBlob) { render(bigBlob.user, bigBlob.interests, bigBlob.recommendations); });
نعم ... الآن يبدو أكثر خرقاء بكثير مما كنا نأمل. هل هذا بسبب دمى التعشيش المجنونة التي سعينا ، أخيرًا وليس آخرًا ، إلى الخروج من جحيم الاسترجاعات؟ ماذا تفعل الآن؟
يمكن تمشيط الكود قليلاً ، وهو يعتمد على عمليات الإغلاق:
نعم ، كل شيء الآن هو عمليا بالطريقة التي أردناها ، ولكن مع نزوة واحدة. لاحظ كيف أطلقنا على الحجج داخل عمليات الاسترجاعات في
fetchedInterests
user
fetchedInterests
، بدلاً من
user
interests
؟ إذا كان الأمر كذلك ، فأنت ملتزم للغاية!
العيب في هذا النهج هو: يجب أن تكون حذراً للغاية في عدم تسمية أي شيء في الوظائف الداخلية وكذلك المتغيرات من ذاكرة التخزين المؤقت التي ستستخدمها في الإغلاق. حتى لو كان لديك موهبة لتجنب التظليل ، فإن الإشارة إلى متغير عالٍ للغاية في الإغلاق لا تزال خطيرة جدًا ، وهذا بالتأكيد ليس جيدًا.
مولدات غير متزامنةسوف مولدات مساعدة! إذا كنت تستخدم المولدات ، فإن كل الإثارة تختفي. فقط سحرية. الحقيقة هي. إلقاء نظرة فقط:
co(function* () { var user = yield fetchJson('/api/user/self'); var interests = yield fetchJson('/api/user/interests?userId=' + self.id); var recommendations = yield Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); });
هذا كل شيء. سوف تعمل. أنت لا تنهمر عندما ترى كم هو جميل المولدات ، هل تشعر بالأسف لأنك قصر النظر وبدأت تتعلم جافا سكريبت حتى قبل ظهور المولدات فيه؟ أعترف ، هذه الفكرة زارتني ذات مرة.
لكن ... كيف يعمل كل هذا؟ حقا السحر؟
بالطبع! ننتقل إلى التعرض.
مولداتفي مثالنا ، يبدو أن المولدات سهلة الاستخدام ، لكن في الواقع هناك الكثير يحدث فيها. لمعرفة المزيد حول المولدات غير المتزامنة ، تحتاج إلى فهم أفضل لكيفية عمل المولدات وكيف توفر تنفيذ غير متزامن ، والذي يبدو متزامنًا.
كما يوحي الاسم ، المولد يجعل القيم:
function* counts(start) { yield start + 1; yield start + 2; yield start + 3; return start + 4; } const counter = counts(0); console.log(counter.next());
الأمر بسيط للغاية ، ولكن على أي حال ، دعونا نتحدث عن ما يحدث هنا:
const counter = counts();
- تهيئة المولد وحفظه في عداد متغير. المولد في طي النسيان ؛ لم يتم تنفيذ أي كود في جسم المولد.console.log(counter.next());
- تفسير المخرجات ( yield
) 1 ، وبعدها يتم إرجاع 1 value
، done
نتائج false
، لأن الناتج لا ينتهي عند هذا الحدconsole.log(counter.next());
- الآن 2!console.log(counter.next());
- الآن 3! انتهى. هل كل شيء على حق؟ رقم تم إيقاف التنفيذ مؤقتًا في الخطوة رقم yield 3;
لإكمال ، تحتاج إلى الاتصال التالي () مرة أخرى.console.log(counter.next());
- الآن 4 ، وتعود ، ولكن لم تصدر ، لذلك نحن الآن الخروج من الوظيفة ، وكل شيء جاهز.console.log(counter.next());
- انتهى المولد العمل! ليس لديه ما يقدمه سوى "كل شيء يتم".
لذلك اكتشفنا كيف تعمل المولدات! لكن انتظر يا لها من حقيقة مروعة: لا يمكن للمولدات أن تبث القيم فحسب ، بل تلتهمها أيضًا!
function* printer() { console.log("We are starting!"); console.log(yield); console.log(yield); console.log(yield); console.log("We are done!"); } const counter = printer(); counter.next(1);
فاهو ، ماذا؟ يستهلك المولد القيم ، بدلاً من التفريخ منها. كيف هذا ممكن؟
السر في الوظيفة
next
. لا تقوم بإرجاع القيم من المولد فحسب ، بل يمكنها أيضًا إعادتها إلى المولد. إذا أخبرت
next()
الوسيطة ، فإن عملية الإنتاج ، التي ينتظرها المولد حاليًا ، تؤدي فعليًا إلى الوسيطة. هذا هو السبب في تسجيل أول عداد. التالي
counter.next(1)
أنه
undefined
. ببساطة لا يوجد تسليم يمكن تسويته.
يبدو الأمر كما لو أن المولد سمح لرمز الاتصال (الإجراء) ورمز المولد (الإجراء) بالشراكة مع بعضهما البعض بحيث يمرران القيم إلى بعضهما البعض كما تم تنفيذها وانتظار بعضهما البعض. الوضع هو نفسه من الناحية العملية ، كما لو كان لمولدي Javascript إمكانية تنفيذ الإجراءات التعاونية المنفذة بشكل تنافسي ، فهم أيضًا "coroutines" ، كان من الممكن التفكير فيها. في الواقع ، إلى حد كبير مثل
co()
، أليس كذلك؟
ولكن دعونا لا نتسرع ، وإلا فإننا سوف نفوز على أنفسنا. في هذه الحالة ، من المهم أن يفهم القارئ بشكل جوهري جوهر المولدات والبرمجة غير المتزامنة ، وأفضل طريقة للقيام بذلك هي تجميع المولد بنفسك. لا تكتب وظيفة المولد ولا تستخدم المولد النهائي ، لكن أعد تصميم وظيفة المولد بنفسك.
الجهاز الداخلي للمولد - نحن نولد مولداتحسنًا ، لا أعرف حقًا كيف تبدو الأجزاء الداخلية للمولد بدقة في أوقات تشغيل JS المختلفة. ولكن هذا ليس مهما جدا. مولدات تتوافق مع الواجهة. "مُنشئ" لإنشاء مثيل للمولد ، الطريقة
next(value? : any)
، التي نطلب بها من المولد مواصلة العمل وإعطائه قيمًا ، طريقة
throw(error)
في حالة إنشاء
throw(error)
بدلاً من القيمة ، وأخيرًا ، طريقة
return()
، والتي لا تزال صامتة. إذا تحقق الامتثال للواجهة ، فكل شيء على ما يرام.
لذلك ، دعونا نحاول إنشاء مولد
counts()
المذكور أعلاه على ES5 الخالص ، بدون
function*
الكلمة الأساسية
function*
. في الوقت الحالي ، يمكنك تجاهل
throw()
وتمرير القيمة إلى
next()
، لأن الطريقة لا تقبل أي إدخال. كيف نفعل ذلك؟
ولكن في Javascript ، هناك آلية أخرى لإيقاف تنفيذ البرنامج واستئنافه: عمليات الإغلاق! هل تبدو مألوفة؟
function makeCounter() { var count = 1; return function () { return count++; } } var counter = makeCounter(); console.log(counter());
إذا كنت تستخدم عمليات الإغلاق من قبل ، فأنا متأكد من أنك كتبت بالفعل شيئًا كهذا. يمكن أن تقوم الدالة التي يتم إرجاعها بواسطة makeCounter بإنشاء تسلسل غير محدود من الأرقام ، تمامًا مثل المولد.
ومع ذلك ، فإن هذه الوظيفة لا تتوافق مع واجهة المولد ، ولا يمكن تطبيقها مباشرة في مثالنا مع
counts()
، والتي تُرجع 4 قيم ومخارج. ما هو المطلوب لنهج عالمي لكتابة وظائف تشبه المولد؟
الإغلاقات ، وأجهزة الدولة ، والعمل الجاد!
function counts(start) { let state = 0; let done = false; function go() { let result; switch (state) { case 0: result = start + 1; state = 1; break; case 1: result = start + 2; state = 2; break; case 2: result = start + 3; state = 3; break; case 3: result = start + 4; done = true; state = -1; break; default: break; } return {done: done, value: result}; } return { next: go } } const counter = counts(0); console.log(counter.next());
عن طريق تشغيل هذا الرمز ، سترى نفس النتائج كما في الإصدار مع المولد. لطيف ، أليس كذلك؟
لذلك ، قمنا بفرز جانب التوليد للمولد ؛ دعنا نحلل المستهلك؟
في الواقع ، لا توجد اختلافات كثيرة.
function printer(start) { let state = 0; let done = false; function go(input) { let result; switch (state) { case 0: console.log("We are starting!"); state = 1; break; case 1: console.log(input); state = 2; break; case 2: console.log(input); state = 3; break; case 3: console.log(input); console.log("We are done!"); done = true; state = -1; break; default: break; return {done: done, value: result}; } } return { next: go } } const counter = printer(); counter.next(1);
كل ما نحتاج إليه هو إضافة
input
كحجة انتقال ، ويتم إخراج القيم. يشبه السحر مرة أخرى؟ تقريبا مثل المولدات؟
الصيحة! لذلك قمنا بإعادة إنشاء المولد كمورد وكمستهلك. لماذا لا تحاول الجمع بين هذه الوظائف في ذلك؟ فيما يلي مثال مصطنع جميل للمولد:
function* adder(initialValue) { let sum = initialValue; while (true) { sum += yield sum; } }
نظرًا لأننا جميعًا متخصصون في المولدات الكهربائية ، فإننا نفهم أن هذا المولد يضيف القيمة المعطاة في
next(value)
إلى
sum
، ثم يُرجع sum. إنه يعمل تمامًا كما توقعنا:
const add = adder(0); console.log(add.next());
رائع الآن دعنا نكتب هذه الواجهة كدالة عادية!
function adder(initialValue) { let state = 'initial'; let done = false; let sum = initialValue; function go(input) { let result; switch (state) { case 'initial': result = initialValue; state = 'loop'; break; case 'loop': sum += input; result = sum; state = 'loop'; break; default: break; } return {done: done, value: result}; } return { next: go } } function runner() { const add = adder(0); console.log(add.next());
نجاح باهر ، قمنا بتنفيذ coroutine كاملة.
لا يزال هناك شيء لمناقشة حول تشغيل المولدات الكهربائية. كيف تعمل الاستثناءات؟ مع الاستثناءات التي تحدث داخل المولدات ، كل شيء بسيط:
next()
سيجعل الاستثناء يصل إلى المتصل وسوف يموت المولد. يتم تمرير استثناء إلى المولد في طريقة
throw()
، والتي حذفتها أعلاه.
دعونا إثراء فاصل لدينا مع ميزة جديدة باردة. إذا قام المتصل بتمرير الاستثناء إلى المولد ، فسيعود إلى القيمة الأخيرة للمجموع.
function* adder(initialValue) { let sum = initialValue; let lastSum = initialValue; let temp; while (true) { try { temp = sum; sum += yield sum; lastSum = temp; } catch (e) { sum = lastSum; } } } const add = adder(0); console.log(add.next());
مشكلة البرمجة - مولد خطأ الاختراقالرفيق ، كيف ننفذ رمي ()؟
سهل الخطأ هو مجرد قيمة أخرى. يمكننا تمريره إلى
go()
كالوسيطة التالية. في الواقع ، هناك حاجة إلى بعض الحذر هنا. عندما
throw(e)
استدعاء
throw(e)
، فإن
yield
سيعمل كما لو أننا كتبنا رمي ه. هذا يعني أنه يجب علينا التحقق من الأخطاء في كل حالة من حالات حالة الجهاز لدينا ، وتعطل البرنامج إذا لم نتمكن من معالجة الخطأ.
لنبدأ مع التنفيذ السابق للفاصل ، نسخ
نمطالحلبوم! لقد طبقنا مجموعة من coroutines التي هي قادرة على تمرير الرسائل والاستثناءات لبعضها البعض ، تماما مثل مولد حقيقي.
لكن الوضع يزداد سوءا ، أليس كذلك؟ تنفيذ جهاز الدولة يتحرك بشكل متزايد بعيدا عن تنفيذ المولد. ليس ذلك فحسب ، نظرًا لمعالجة الأخطاء ، يتم تشويش الشفرة بالقمامة ؛ الكود هو أكثر تعقيدًا بسبب
while
الطويلة التي لدينا هنا. لتحويل
while
تحتاج إلى "فك الارتباط" بها إلى حالات. لذلك ، تتضمن حالتنا 1 فعليًا 2.5 تكرار من
while
، نظرًا لأن
yield
تتكسر في المنتصف. أخيرًا ، يجب عليك إضافة رمز إضافي لدفع الاستثناءات من المتصل والعكس بالعكس إذا لم يكن هناك
try/catch
في المولد لمعالجة هذا الاستثناء.
لقد فعلت ذلك !!! لقد أكملنا تحليلًا تفصيليًا للبدائل المحتملة لتنفيذ المولدات ، وآمل أن تكون قد فهمت بالفعل كيف تعمل المولدات. في المخلفات الجافة:
- يمكن للمولد توليد القيم أو استهلاك القيم أو كليهما.
- يمكن إيقاف حالة المولد (الحالة ، آلة الحالة ، أمسك؟)
- يسمح لك المتصل والمولد بتكوين مجموعة من corutin ، والتفاعل مع بعضهما البعض
- يتم توجيه الاستثناءات في أي اتجاه.
الآن وبعد أن أصبح لدينا فهم أفضل للمولدات ، أقترح طريقة ملائمة محتملة للتفكير حولها: هذه عبارة عن تصميمات بناء جملة يمكنك من خلالها كتابة إجراءات منفذة بطريقة تنافسية تنقل القيم إلى بعضها البعض من خلال قناة تمرر القيم واحدًا تلو الآخر (
yield
). سيكون هذا مفيدًا في القسم التالي ، حيث سننتج تنفيذ
co()
من coroutine.
Corutin تحكم انعكاسالآن وقد أصبحنا ماهرين في العمل مع المولدات الكهربائية ، دعونا نفكر في كيفية استخدامها في البرمجة غير المتزامنة. إذا استطعنا كتابة المولدات على هذا النحو ، فإن هذا لا يعني أن الوعود في المولدات سيتم حلها تلقائيًا. ولكن مهلا ، ليس المقصود من المولدات أن تعمل بمفردها. يجب أن يتفاعلوا مع برنامج آخر ، الإجراء الرئيسي ، الإجراء الذي يستدعي
.next()
و.
.throw()
.
ماذا لو وضعنا منطق أعمالنا ليس في الإجراء الرئيسي ، ولكن في المولد؟ عندما تحدث قيمة غير متزامنة معينة ، مثل الوعد ، لمنطق الأعمال ، سيقول المولد: "لا أريد أن أعبث مع هذا الهراء ، أيقظني عندما يحل" ، وسيتوقف مؤقتًا ويصدر وعدًا بإجراء الإجراء. إجراء الصيانة: "حسنًا ، سأتصل بك لاحقًا." بعد ذلك يسجل رد اتصال مع هذا الوعد ، ويخرج وينتظر حتى يكون من الممكن تشغيل دورة من الأحداث (أي عندما يتم حل الوعد). عندما يحدث هذا ، سيعلن الإجراء "مهلا ، حان دورك" ، ويرسل القيمة عبر
.next()
مولد النوم. سوف تنتظر المولد للقيام بعمله ، وفي الوقت نفسه سوف يقوم بأشياء أخرى غير متزامنة ... وهكذا. لقد استمعت إلى قصة حزينة حول كيفية استمرار الإجراء في خدمة المولد.
لذلك ، العودة إلى الموضوع الرئيسي. الآن بعد أن عرفنا كيف تعمل المولدات والوعود ، لن يكون من الصعب علينا إنشاء مثل "إجراء الخدمة" هذا. سيتم تنفيذ إجراء الخدمة نفسه بشكل تنافسي باعتباره وعدًا ، وإنشاء مثيل للمولد وصيانته ، ثم العودة إلى النتيجة النهائية لإجراءاتنا الرئيسية باستخدام رد الاتصال
.then()
.
بعد ذلك ، دعنا نعود إلى برنامج co () ومناقشته بمزيد من التفاصيل.
co()
هو إجراء خدمة يأخذ عمالة الرقيق بحيث لا يستطيع المولد العمل إلا مع القيم المتزامنة. يبدو بالفعل أكثر منطقية ، أليس كذلك؟
co(function* () { var user = yield fetchJson('/api/user/self'); var interests = yield fetchJson('/api/user/interests?userId=' + self.id); var recommendations = yield Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); });
, ,
co()
, .
— co()عظيم!
co()
, , .
co()
- ,
.next()
, {done: false, value: [a Promise]}
- ( ),
.next()
, - , 4
- -
{done: true, value: ...}
, , co()
, co(), :
نمط function deferred(val) { return new Promise((resolve, reject) => resolve(val)); } co(function* asyncAdds(initialValue) { console.log(yield deferred(initialValue + 1)); console.log(yield deferred(initialValue + 2)); console.log(yield deferred(initialValue + 3)); }); function co(generator) { return new Promise((resolve, reject) => {
, ? - 10
co()
, . , . ?
– co(), , , ,
co()
. ,
.throw()
.
نمط function deferred(val) { return new Promise((resolve, reject) => resolve(val)); } function deferReject(e) { return new Promise((resolve, reject) => reject(e)); } co(function* asyncAdds() { console.log(yield deferred(1)); try { console.log(yield deferredError(new Error('To fail, or to not fail.'))); } catch (e) { console.log('To not fail!'); } console.log(yield deferred(3)); }); function co(generator) { return new Promise((resolve, reject) => {
. , ,
.next()
onResolve()
.
onReject()
,
.throw()
.
try/catch
, ,
try/catch
.
,
co()
! !
co()
, , , . , ?
: async/awaitco()
. - , async/await? — ! ,
async await
.
async ,
await
,
yield
.
await
,
async
.
async
- .
,
async/await
, , -
co()
async
yield
await
,
*
, .
co(function* () { var user = yield fetchJson('/api/user/self'); var interests = yield fetchJson('/api/user/interests?userId=' + self.id); var recommendations = yield Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); });
:
async function () { var user = await fetchJson('/api/user/self'); var interests = await fetchJson('/api/user/interests?userId=' + self.id); var recommendations = await Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }();
, :
co()
. async , . async
co()
co.wrap()
.co()
( yield
) , , . async
( await
) .
Javascript , , « »
co()
, , ,
async/await
. ? صحيح