جافاسكريبت - الفرق بين `return await promise` و `return promise`
بالنظر إلى عينات الكود أدناه، هل هناك أي اختلاف في السلوك، وإذا كان الأمر كذلك، ما هي تلك الاختلافات؟
اعد النتائج من الوعد
async function delay1Second() {
return (await delay(1000));
}
إرجاع الوعد
async function delay1Second() {
return delay(1000);
}
كما أفهمه، سيكون الأول لديه معالجة للأخطاء داخل الدالة غير المتزامنة، وستندفع الأخطاء خارج وعد الدالة غير المتزامنة. ومع ذلك، يحتاج الثاني لنبضة واحدة أقل. هل هذا صحيح؟
هذه القطعة هي مجرد دالة شائعة لإرجاع وعد كمثال للرجوع إليه.
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
الإجابة رقم 1
في معظم الأحيان، لا يوجد فرق ملحوظ بين return و return await. كلا النسختين من delay1Second لهما نفس السلوك القابل للمراقبة بالضبط (ولكن اعتمادًا على التنفيذ، قد تستخدم نسخة return await ذاكرة أكثر قليلاً لأن كائن Promise وسطي قد يتم إنشاؤه).
ومع ذلك، كما أشار @PitaJ، هناك حالة واحدة يوجد فيها فرق: إذا كان return أو return await متضمناً في كتلة try-catch. ضع في اعتبارك هذا المثال
async function rejectionWithReturnAwait () {
try {
return await Promise.reject(new Error())
} catch (e) {
return 'Saved!'
}
}
async function rejectionWithReturn () {
try {
return Promise.reject(new Error())
} catch (e) {
return 'Saved!'
}
}
في النسخة الأولى، تنتظر الدالة غير المتزامنة الوعد المرفوض قبل إرجاع نتيجته، مما يتسبب في تحويل الرفض إلى استثناء والوصول إلى عبارة catch؛ وبالتالي ستُرجع الدالة وعدًا يحل إلى السلسلة "تم الحفظ!".
ومع ذلك، فإن النسخة الثانية من الدالة تُعيد الوعد المرفوض مباشرة دون الانتظار له داخل الدالة غير المتزامنة، مما يعني أن حالة catch لا تُستدعى ويحصل المستدعي على الرفض بدلاً من ذلك.
الإجابة رقم 2
كما ذكرته الإجابات الأخرى، من المحتمل أن يكون هناك فائدة طفيفة في الأداء عند السماح للوعد بالارتقاع عن طريق إرجاعه مباشرة — ببساطة لأنك لا تحتاج إلى انتظار النتيجة أولاً ثم تغليفها مرة أخرى بوعد آخر. ومع ذلك، لم يتحدث أحد عن تحسين استدعاء الذيل بعد.
تحسين استدعاء الذيل، أو "استدعاءات الذيل الصحيحة"، هي تقنية يستخدمها المترجم لتحسين مكدس الاستدعاءات. حاليًا، لا تدعمها العديد من بيئات التشغيل بعد — على الرغم من أنها تقنيًا جزء من معيار ES6 — لكن من الممكن أن يتم إضافة الدعم في المستقبل، لذا يمكنك التحضير لذلك عن طريق كتابة كود جيد في الوقت الحاضر.
باختصار، التحسين بناءً على تحسين الاستدعاء الكلي (TCO) أو تحسين الاستدعاء النهائي (PTC) يحسن مكدس الاستدعاء عن طريق عدم فتح إطار جديد لدالة يتم إرجاعها مباشرة بواسطة دالة أخرى. بدلاً من ذلك، يعيد استخدام نفس الإطار.
async function delay1Second() {
return delay(1000);
}
نظرًا لأن delay() يتم إرجاعه مباشرة بواسطة delay1Second()، فإن البيئات التي تدعم PTC ستفتح أولاً إطارًا لـ delay1Second() (الدالة الخارجية)، ولكن بدلاً من فتح إطار آخر لـ delay() (الدالة الداخلية)، ستعيد استخدام نفس الإطار الذي تم فتحه للدالة الخارجية. هذا يُحسن الذاكرة المكدسة لأنه يمكن أن يمنع فائض المكدس (ههه) مع الدوال الغير محدودة للغاية، على سبيل المثال fibonacci(5e+25). في الأساس، تتحول إلى حلقة، وهي أسرع بكثير.
يتم تمكين PTC فقط عندما يتم إرجاع الدالة الداخلية مباشرة. لا يتم استخدامه عندما يتم تعديل نتيجة الدالة قبل إرجاعها، على سبيل المثال، إذا كان لديك return (delay(1000) || null)، أو return await delay(1000).
لكن كما قلت، معظم بيئات التشغيل والمتصفحات لا تدعم PTC بعد، لذلك ربما لا يحدث فرقًا كبيرًا الآن، لكن لا بأس من تجهيز كودك للمستقبل.
اقرأ المزيد في هذا السؤال: Node.js: هل هناك تحسينات لاستدعاءات الذيل في الدوال غير المتزامنة؟
الإجابة رقم 3
فرق ملحوظ: يتم التعامل مع رفض الوعد في أماكن مختلفة
return somePromiseسيمرر somePromise إلى موقع الاستدعاء، وawaitsomePromise ليتم حله في موقع الاستدعاء (إذا كان هناك أي). لذلك، إذا تم رفض somePromise، فلن يتم التعامل معه بواسطة كتلة catch المحلية، ولكن بواسطة كتلة catch الخاصة بموقع الاستدعاء.
async function foo () {
try {
return Promise.reject();
} catch (e) {
console.log('IN');
}
}
(async function main () {
try {
let a = await foo();
} catch (e) {
console.log('OUT');
}
})();
// 'OUT'return await somePromiseسيقوم أولاً بالانتظار حتى يتم تسوية somePromise محليًا. لذلك، سيتم التعامل أولاً مع القيمة أو الاستثناء محليًا. => سيتم تنفيذ كتلة catch المحلية إذا تم رفضsomePromise.
async function foo () {
try {
return await Promise.reject();
} catch (e) {
console.log('IN');
}
}
(async function main () {
try {
let a = await foo();
} catch (e) {
console.log('OUT');
}
})();
// 'IN'السبب: return await Promise ينتظر كل من السياق المحلي والخارجي، return Promise ينتظر فقط الخارج
الخطوات التفصيلية:
إرجاع الوعد
async function delay1Second() {
return delay(1000);
}
- استدعِ
delay1Second();
const result = await delay1Second();
- داخل
delay1Second()، تُعيد الدالةdelay(1000)وعدًا على الفور مع[[PromiseStatus]]: 'pending'. لنطلق عليهdelayPromise.
async function delay1Second() {
return delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
- ستقوم الدوال غير المتزامنة بتغليف قيمة الإرجاع الخاصة بها داخل
Promise.resolve()(المصدر). وبما أنdelay1Secondهي دالة غير متزامنة، نحصل على:
const result = await Promise.resolve(delayPromise);
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
Promise.resolve(delayPromise)يُعيدdelayPromiseدون القيام بأي شيء لأن الإدخال هو بالفعل وعد (انظر MDN Promise.resolve):
const result = await delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
awaitينتظر حتى يتم تسويةdelayPromise.
- إذا تم الوفاء بـ
delayPromiseمع PromiseValue=1:
const result = 1;
- وإلا إذا تم رفض
delayPromise:
// jump to catch block if there is any
ارجع انتظر الوعد
async function delay1Second() {
return await delay(1000);
}
- استدعِ
delay1Second();
const result = await delay1Second();
- داخل
delay1Second()، تُعيد الدالةdelay(1000)وعدًا على الفور مع[[PromiseStatus]]: 'pending'. لنطلق عليهdelayPromise.
async function delay1Second() {
return await delayPromise;
// delayPromise.[[PromiseStatus]]: 'pending'
// delayPromise.[[PromiseValue]]: undefined
}
- الانتظار المحلي سينتظر حتى يتم تسوية
delayPromise.
- الحالة 1:
delayPromiseيتم الوفاء بها بالقيمة PromiseValue=1:
async function delay1Second() {
return 1;
}
const result = await Promise.resolve(1); // let's call it "newPromise"
const result = await newPromise;
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: 1
const result = 1;
- الحالة 2:
delayPromiseمرفوض:
// jump to catch block inside `delay1Second` if there is any
// let's say a value -1 is returned in the end
const result = await Promise.resolve(-1); // call it newPromise
const result = await newPromise;
// newPromise.[[PromiseStatus]]: 'resolved'
// newPromise.[[PromiseValue]]: -1
const result = -1;
قاموس المصطلحات:
- يتم التسوية:
Promise.[[PromiseStatus]]يتغير منpendingإلىresolvedأوrejected
الإجابة رقم 4
في مشروعنا، قررنا دائمًا استخدام 'return await'. الحجة هي أن 'خطر نسيان إضافة الـ 'await' عندما يتم لاحقًا وضع كتلة try-catch حول تعبير الإرجاع يبرر وجود الـ 'await' الزائد الآن.'
