نظرة متعمقة في إدارة تدفقات البيانات في JavaScript. تعلم كيفية منع التحميل الزائد للنظام وتسرب الذاكرة باستخدام آلية الضغط الخلفي الأنيقة لمولدات async.
الضغط الخلفي لمولد JavaScript Async: الدليل النهائي للتحكم في تدفق البيانات
في عالم التطبيقات كثيفة البيانات، غالبًا ما نواجه مشكلة كلاسيكية: مصدر بيانات سريع ينتج معلومات أسرع بكثير مما يمكن للمستهلك معالجته. تخيل خرطوم حريق متصل برشاش حديقة. بدون صمام للتحكم في التدفق، سيكون لديك فوضى مغمورة. في البرامج، تؤدي هذه الفيضانات إلى ذاكرة مرهقة، وتطبيقات لا تستجيب، وفي النهاية أعطال. يتم إدارة هذا التحدي الأساسي من خلال مفهوم يسمى الضغط الخلفي، ويوفر JavaScript الحديث حلاً أنيقًا وفريدًا: مولدات Async.
سيرشدك هذا الدليل الشامل في رحلة استكشاف متعمقة في عالم معالجة التدفق والتحكم في التدفق في JavaScript. سنستكشف ما هو الضغط الخلفي، ولماذا هو أمر بالغ الأهمية لبناء أنظمة قوية، وكيف توفر مولدات async آلية بديهية ومضمنة للتعامل معه. سواء كنت تقوم بمعالجة ملفات كبيرة، أو استهلاك واجهات برمجة التطبيقات في الوقت الفعلي، أو بناء خطوط أنابيب بيانات معقدة، فإن فهم هذا النمط سيغير بشكل أساسي كيفية كتابة التعليمات البرمجية غير المتزامنة.
1. تفكيك المفاهيم الأساسية
قبل أن نتمكن من بناء حل، يجب أولاً أن نفهم القطع الأساسية للغز. دعنا نوضح المصطلحات الرئيسية: التدفقات، والضغط الخلفي، وسحر مولدات async.
ما هو التدفق؟
التدفق ليس قطعة من البيانات؛ إنه تسلسل من البيانات المتاحة بمرور الوقت. بدلاً من قراءة ملف بحجم 10 جيجابايت بالكامل في الذاكرة مرة واحدة (مما قد يؤدي على الأرجح إلى تعطل التطبيق)، يمكنك قراءته كتدفق، قطعة قطعة. هذا المفهوم عالمي في الحوسبة:
- إدخال/إخراج الملف: قراءة ملف سجل كبير أو كتابة بيانات الفيديو.
- الشبكات: تنزيل ملف، أو استقبال بيانات من WebSocket، أو دفق محتوى الفيديو.
- الاتصال بين العمليات: توجيه إخراج برنامج واحد إلى إدخال برنامج آخر.
تعتبر التدفقات ضرورية لتحقيق الكفاءة، مما يسمح لنا بمعالجة كميات كبيرة من البيانات مع الحد الأدنى من مساحة الذاكرة.
ما هو الضغط الخلفي؟
الضغط الخلفي هو المقاومة أو القوة التي تعارض التدفق المطلوب للبيانات. إنها آلية ردود فعل تسمح للمستهلك البطيء بالإشارة إلى منتج سريع، "مهلاً، أبطئ! لا يمكنني مواكبة ذلك."
دعنا نستخدم تشبيهًا كلاسيكيًا: خط تجميع المصنع.
- المنتج هو المحطة الأولى، حيث يضع الأجزاء على الحزام الناقل بسرعة عالية.
- المستهلك هو المحطة النهائية، التي تحتاج إلى أداء تجميع تفصيلي وبطيء لكل جزء.
إذا كان المنتج سريعًا جدًا، فسوف تتراكم الأجزاء وستسقط في النهاية من الحزام قبل الوصول إلى المستهلك. هذا هو فقدان البيانات وتعطل النظام. الضغط الخلفي هو الإشارة التي يرسلها المستهلك مرة أخرى إلى الخط، ويطلب من المنتج التوقف مؤقتًا حتى يلحق بالركب. إنه يضمن أن النظام بأكمله يعمل بوتيرة أبطأ مكوناته، مما يمنع التحميل الزائد.
بدون ضغط خلفي، فإنك تخاطر بما يلي:
- التخزين المؤقت غير المحدود: تتراكم البيانات في الذاكرة، مما يؤدي إلى ارتفاع استخدام ذاكرة الوصول العشوائي واحتمال حدوث أعطال.
- فقدان البيانات: إذا فاضت المخازن المؤقتة، فقد يتم إسقاط البيانات.
- حظر حلقة الأحداث: في Node.js، يمكن للنظام المحمّل أن يحظر حلقة الأحداث، مما يجعل التطبيق غير مستجيب.
منعش سريع: المولدات والمكررات غير المتزامنة
يكمن الحل للضغط الخلفي في JavaScript الحديث في الميزات التي تسمح لنا بإيقاف التنفيذ واستئنافه لاحقًا. دعنا نراجعها بسرعة.
المولدات (`function*`): هذه وظائف خاصة يمكن الخروج منها وإعادة إدخالها لاحقًا. يستخدمون الكلمة الأساسية `yield` لـ "التوقف مؤقتًا" وإرجاع قيمة. يمكن للمتصل بعد ذلك أن يقرر متى يستأنف تنفيذ الوظيفة للحصول على القيمة التالية. يؤدي هذا إلى إنشاء نظام يعتمد على السحب عند الطلب للبيانات المتزامنة.
المكررات غير المتزامنة (`Symbol.asyncIterator`): هذا بروتوكول يحدد كيفية التكرار فوق مصادر البيانات غير المتزامنة. الكائن هو كائن قابل للتكرار غير متزامن إذا كان لديه أسلوب بالمفتاح `Symbol.asyncIterator` الذي يرجع كائنًا له أسلوب `next()`. يعيد أسلوب `next()` هذا Promise يحل إلى `{ value, done }`.
المولدات غير المتزامنة (`async function*`): هذا هو المكان الذي يجتمع فيه كل شيء معًا. تجمع المولدات غير المتزامنة بين سلوك الإيقاف المؤقت للمولدات والطبيعة غير المتزامنة للـPromises. إنها الأداة المثالية لتمثيل تدفق البيانات الذي يصل بمرور الوقت.
يمكنك استهلاك مولد غير متزامن باستخدام حلقة `for await...of` القوية، والتي تجرد التعقيد المتمثل في استدعاء `.next()` وانتظار حل الوعود.
async function* countToThree() {
yield 1; // Pause and yield 1
await new Promise(resolve => setTimeout(resolve, 1000)); // Asynchronously wait
yield 2; // Pause and yield 2
await new Promise(resolve => setTimeout(resolve, 1000));
yield 3; // Pause and yield 3
}
async function main() {
console.log("Starting consumption...");
for await (const number of countToThree()) {
console.log(number); // This will log 1, then 2 after 1s, then 3 after another 1s
}
console.log("Finished consumption.");
}
main();
الفكرة الأساسية هي أن حلقة `for await...of` *تسحب* القيم من المولد. لن تطلب القيمة التالية حتى ينتهي تنفيذ التعليمات البرمجية الموجودة داخل الحلقة للقيمة الحالية. هذه الطبيعة الأساسية المستندة إلى السحب هي سر الضغط الخلفي التلقائي.
2. المشكلة موضحة: التدفق بدون ضغط خلفي
لتقدير الحل حقًا، دعنا نلقي نظرة على نمط شائع ولكنه معيب. تخيل أن لدينا مصدر بيانات سريعًا جدًا (منتج) ومعالج بيانات بطيء (مستهلك)، ربما يقوم بكتابة إلى قاعدة بيانات بطيئة أو استدعاء واجهة برمجة تطبيقات محدودة المعدل.
إليك محاكاة باستخدام نهج تقليدي لمصدر الأحداث أو نمط رد الاتصال، وهو نظام يعتمد على الدفع.
// Represents a very fast data source
class FastProducer {
constructor() {
this.listeners = [];
}
onData(listener) {
this.listeners.push(listener);
}
start() {
let id = 0;
// Produce data every 10 milliseconds
this.interval = setInterval(() => {
const data = { id: id++, timestamp: Date.now() };
console.log(`PRODUCER: Emitting item ${data.id}`);
this.listeners.forEach(listener => listener(data));
}, 10);
}
stop() {
clearInterval(this.interval);
}
}
// Represents a slow consumer (e.g., writing to a slow network service)
async function slowConsumer(data) {
console.log(` CONSUMER: Starting to process item ${data.id}...`);
// Simulate a slow I/O operation taking 500 milliseconds
await new Promise(resolve => setTimeout(resolve, 500));
console.log(` CONSUMER: ...Finished processing item ${data.id}`);
}
// --- Let's run the simulation ---
const producer = new FastProducer();
const dataBuffer = [];
producer.onData(data => {
console.log(`Received item ${data.id}, adding to buffer.`);
dataBuffer.push(data);
// A naive attempt to process
// slowConsumer(data); // This would block new events if we awaited it
});
producer.start();
// Let's inspect the buffer after a short time
setTimeout(() => {
producer.stop();
console.log(`
--- After 2 seconds ---`);
console.log(`Buffer size is: ${dataBuffer.length}`);
console.log(`Producer created around 200 items, but the consumer would have only processed 4.`);
console.log(`The other 196 items are sitting in memory, waiting.`);
}, 2000);
ماذا يحدث هنا؟
يقوم المنتج بإطلاق البيانات كل 10 مللي ثانية. يستغرق المستهلك 500 مللي ثانية لمعالجة عنصر واحد. المنتج أسرع 50 مرة من المستهلك!
في هذا النموذج المعتمد على الدفع، لا يدرك المنتج على الإطلاق حالة المستهلك. يستمر في دفع البيانات فقط. تضيف التعليمات البرمجية الخاصة بنا ببساطة البيانات الواردة إلى مصفوفة، `dataBuffer`. في غضون ثانيتين فقط، تحتوي هذه المخزن المؤقت على ما يقرب من 200 عنصر. في تطبيق حقيقي يعمل لساعات، سينمو هذا المخزن المؤقت إلى أجل غير مسمى، ويستهلك كل الذاكرة المتاحة ويتسبب في تعطل العملية. هذه هي مشكلة الضغط الخلفي في أخطر أشكالها.
3. الحل: الضغط الخلفي المتأصل مع مولدات Async
الآن، دعنا نعيد صياغة نفس السيناريو باستخدام مولد async. سنقوم بتحويل المنتج من "دافع" إلى شيء يمكن "سحبه" منه.
الفكرة الأساسية هي تغليف مصدر البيانات في `async function*`. سيستخدم المستهلك بعد ذلك حلقة `for await...of` لسحب البيانات فقط عندما يكون جاهزًا للمزيد.
// PRODUCER: A data source wrapped in an async generator
async function* createFastProducer() {
let id = 0;
while (true) {
// Simulate a fast data source creating an item
await new Promise(resolve => setTimeout(resolve, 10));
const data = { id: id++, timestamp: Date.now() };
console.log(`PRODUCER: Yielding item ${data.id}`);
yield data; // Pause until the consumer requests the next item
}
}
// CONSUMER: A slow process, just like before
async function slowConsumer(data) {
console.log(` CONSUMER: Starting to process item ${data.id}...`);
// Simulate a slow I/O operation taking 500 milliseconds
await new Promise(resolve => setTimeout(resolve, 500));
console.log(` CONSUMER: ...Finished processing item ${data.id}`);
}
// --- The main execution logic ---
async function main() {
const producer = createFastProducer();
// The magic of `for await...of`
for await (const data of producer) {
await slowConsumer(data);
}
}
main();
دعنا نحلل تدفق التنفيذ
إذا قمت بتشغيل هذا الرمز، فسترى إخراجًا مختلفًا بشكل كبير. سيبدو شيئًا كهذا:
PRODUCER: Yielding item 0 CONSUMER: Starting to process item 0... CONSUMER: ...Finished processing item 0 PRODUCER: Yielding item 1 CONSUMER: Starting to process item 1... CONSUMER: ...Finished processing item 1 PRODUCER: Yielding item 2 CONSUMER: Starting to process item 2... ...
لاحظ التزامن التام. لا ينتج المنتج عنصرًا جديدًا إلا *بعد* أن ينهي المستهلك معالجة العنصر السابق تمامًا. لا يوجد مخزن مؤقت متزايد ولا تسرب للذاكرة. يتم تحقيق الضغط الخلفي تلقائيًا.
إليك الخطوة خطوة شرحًا سبب عمل هذا:
- تبدأ حلقة `for await...of` وتستدعي `producer.next()` خلف الكواليس لطلب العنصر الأول.
- تبدأ الدالة `createFastProducer` في التنفيذ. تنتظر 10 مللي ثانية، وتنشئ `data` للعنصر 0، ثم تضرب `yield data`.
- يقوم المولد بإيقاف تنفيذه مؤقتًا وإرجاع Promise الذي يحل بالقيمة الناتجة (`{ value: data, done: false }`).
- تتلقى حلقة `for await...of` القيمة. تبدأ كتلة الحلقة في التنفيذ مع عنصر البيانات الأول هذا.
- يستدعي `await slowConsumer(data)`. يستغرق هذا 500 مللي ثانية حتى يكتمل.
- هذا هو الجزء الأكثر أهمية: حلقة `for await...of` لا تستدعي `producer.next()` مرة أخرى حتى يحل Promise `await slowConsumer(data)`. يظل المنتج متوقفًا مؤقتًا في عبارة `yield` الخاصة به.
- بعد 500 مللي ثانية، تنتهي `slowConsumer`. تكتمل كتلة الحلقة لهذه التكرار.
- الآن، وفقط الآن، تستدعي حلقة `for await...of` `producer.next()` مرة أخرى لطلب العنصر التالي.
- تتم استعادة `createFastProducer` من حيث توقف وتستمر حلقة `while` الخاصة بها، بدءًا من الدورة الخاصة بالعنصر 1.
يتحكم معدل معالجة المستهلك مباشرة في معدل إنتاج المنتج. هذا هو النظام المستند إلى السحب، وهو أساس التحكم في التدفق الأنيق في JavaScript الحديث.
4. الأنماط المتقدمة وحالات الاستخدام الواقعية
يتألق القوة الحقيقية للمولدات غير المتزامنة عندما تبدأ في تركيبها في خطوط أنابيب لأداء تحويلات بيانات معقدة.
تدفق وتحويل التدفقات
تمامًا كما يمكنك توجيه الأوامر على سطر أوامر Unix (على سبيل المثال، `cat log.txt | grep 'ERROR' | wc -l`)، يمكنك ربط مولدات async. المحول هو ببساطة مولد async يقبل كائنًا قابلاً للتكرار غير متزامن آخر كمدخلاته وينتج بيانات محولة.
دعنا نتخيل أننا نعالج ملف CSV كبير لبيانات المبيعات. نريد قراءة الملف، وتحليل كل سطر، والتصفية لمعاملات عالية القيمة، ثم حفظها في قاعدة بيانات.
const fs = require('fs');
const { once } = require('events');
// PRODUCER: Reads a large file line by line
async function* readFileLines(filePath) {
const readable = fs.createReadStream(filePath, { encoding: 'utf8' });
let buffer = '';
readable.on('data', chunk => {
buffer += chunk;
let newlineIndex;
while ((newlineIndex = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, newlineIndex);
buffer = buffer.slice(newlineIndex + 1);
readable.pause(); // Explicitly pause Node.js stream for backpressure
yield line;
}
});
readable.on('end', () => {
if (buffer.length > 0) {
yield buffer; // Yield the last line if no trailing newline
}
});
// A simplified way to wait for the stream to finish or error
await once(readable, 'close');
}
// TRANSFORMER 1: Parses CSV lines into objects
async function* parseCSV(lines) {
for await (const line of lines) {
const [id, product, amount] = line.split(',');
if (id && product && amount) {
yield { id, product, amount: parseFloat(amount) };
}
}
}
// TRANSFORMER 2: Filters for high-value transactions
async function* filterHighValue(transactions, minValue) {
for await (const tx of transactions) {
if (tx.amount >= minValue) {
yield tx;
}
}
}
// CONSUMER: Saves the final data to a slow database
async function saveToDatabase(transaction) {
console.log(`Saving transaction ${transaction.id} with amount ${transaction.amount} to DB...`);
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate slow DB write
}
// --- The Composed Pipeline ---
async function processSalesFile(filePath) {
const lines = readFileLines(filePath);
const transactions = parseCSV(lines);
const highValueTxs = filterHighValue(transactions, 1000);
console.log("Starting ETL pipeline...");
for await (const tx of highValueTxs) {
await saveToDatabase(tx);
}
console.log("Pipeline finished.");
}
// Create a dummy large CSV file for testing
// fs.writeFileSync('sales.csv', ...);
// processSalesFile('sales.csv');
في هذا المثال، ينتشر الضغط الخلفي على طول السلسلة. `saveToDatabase` هو الجزء الأبطأ. يؤدي `await` الخاص به إلى إيقاف حلقة `for await...of` النهائية مؤقتًا. يؤدي هذا إلى إيقاف `filterHighValue` مؤقتًا، مما يمنع طلب العناصر من `parseCSV`، مما يمنع طلب العناصر من `readFileLines`، مما يؤدي في النهاية إلى إخبار تدفق ملف Node.js بـ `pause()` فعليًا للقراءة من القرص. يتحرك النظام بأكمله في خطوة واحدة، باستخدام الحد الأدنى من الذاكرة، وكلها منسقة بواسطة آلية السحب البسيطة للتكرار غير المتزامن.
التعامل مع الأخطاء بأناقة
التعامل مع الأخطاء أمر مباشر. يمكنك إحاطة حلقة المستهلك الخاصة بك بكتلة `try...catch`. إذا تم طرح خطأ في أي من المولدات الأولية، فسوف ينتشر لأسفل ويتم التقاطه بواسطة المستهلك.
async function* errorProneGenerator() {
yield 1;
yield 2;
throw new Error("Something went wrong in the generator!");
yield 3; // This will never be reached
}
async function main() {
try {
for await (const value of errorProneGenerator()) {
console.log("Received:", value);
}
} catch (err) {
console.error("Caught an error:", err.message);
}
}
main();
// Output:
// Received: 1
// Received: 2
// Caught an error: Something went wrong in the generator!
تنظيف الموارد باستخدام `try...finally`
ماذا لو قرر المستهلك إيقاف المعالجة مبكرًا (على سبيل المثال، باستخدام عبارة `break`)؟ قد يُترك المولد وهو يحتفظ بموارد مفتوحة مثل مقابض الملفات أو اتصالات قاعدة البيانات. تعد كتلة `finally` داخل المولد هي المكان المثالي للتنظيف.
عند الخروج من حلقة `for await...of` بشكل سابق لأوانه (عبر `break` أو `return` أو خطأ)، فإنها تستدعي تلقائيًا أسلوب `.return()` الخاص بالمولد. يتسبب هذا في انتقال المولد إلى كتلة `finally` الخاصة به، مما يسمح لك بأداء إجراءات التنظيف.
async function* fileReaderWithCleanup(filePath) {
let fileHandle;
try {
console.log("GENERATOR: Opening file...");
fileHandle = await fs.promises.open(filePath, 'r');
// ... logic to yield lines from the file ...
yield 'line 1';
yield 'line 2';
yield 'line 3';
} finally {
if (fileHandle) {
console.log("GENERATOR: Closing file handle.");
await fileHandle.close();
}
}
}
async function main() {
for await (const line of fileReaderWithCleanup('my-file.txt')) {
console.log("CONSUMER:", line);
if (line === 'line 2') {
console.log("CONSUMER: Breaking the loop early.");
break; // Exit the loop
}
}
}
main();
// Output:
// GENERATOR: Opening file...
// CONSUMER: line 1
// CONSUMER: line 2
// CONSUMER: Breaking the loop early.
// GENERATOR: Closing file handle.
5. المقارنة بآليات الضغط الخلفي الأخرى
المولدات غير المتزامنة ليست هي الطريقة الوحيدة للتعامل مع الضغط الخلفي في نظام JavaScript البيئي. من المفيد أن نفهم كيف تقارن بأساليب أخرى شائعة.
تدفقات Node.js (`.pipe()` و `pipeline`)
يحتوي Node.js على واجهة برمجة تطبيقات Streams مضمنة وقوية تعاملت مع الضغط الخلفي لسنوات. عند استخدام `readable.pipe(writable)`، يدير Node.js تدفق البيانات بناءً على المخازن المؤقتة الداخلية وإعداد `highWaterMark`. إنه نظام يعتمد على الأحداث ويدفع مع آليات ضغط خلفي مضمنة.
- التعقيد: من المعروف أن واجهة برمجة تطبيقات Node.js Streams معقدة للغاية للتنفيذ بشكل صحيح، خاصةً لتدفقات التحويل المخصصة. يتضمن ذلك توسيع الفئات وإدارة الحالة والأحداث الداخلية (`'data'` و `'end'` و `'drain'`).
- التعامل مع الأخطاء: التعامل مع الأخطاء باستخدام `.pipe()` أمر صعب، حيث أن الخطأ في تدفق واحد لا يدمر تلقائيًا الآخرين في خط الأنابيب. هذا هو السبب في تقديم `stream.pipeline` كبديل أكثر قوة.
- إمكانية القراءة: غالبًا ما تؤدي المولدات غير المتزامنة إلى تعليمات برمجية تبدو أكثر تزامنًا ويمكن القول إنها أسهل في القراءة والتفكير فيها، خاصةً بالنسبة للتحويلات المعقدة.
لإدخال/إخراج عالي الأداء ومنخفض المستوى في Node.js، لا تزال واجهة برمجة تطبيقات Streams الأصلية اختيارًا ممتازًا. ومع ذلك، بالنسبة لمنطق مستوى التطبيق وتحويلات البيانات، غالبًا ما توفر المولدات غير المتزامنة تجربة مطور أبسط وأكثر أناقة.
البرمجة التفاعلية (RxJS)
تستخدم مكتبات مثل RxJS مفهوم Observables. مثل تدفقات Node.js، تعد Observables في المقام الأول نظامًا يعتمد على الدفع. يصدر المنتج (Observable) قيمًا، ويتفاعل المستهلك (Observer) معها. الضغط الخلفي في RxJS ليس تلقائيًا؛ يجب إدارته بشكل صريح باستخدام مجموعة متنوعة من المشغلين مثل `buffer` أو `throttle` أو `debounce` أو الجدولة المخصصة.
- النموذج: يوفر RxJS نموذج برمجة وظيفي قوي لتأليف وإدارة تدفقات الأحداث غير المتزامنة المعقدة. إنه قوي للغاية لسيناريوهات مثل معالجة أحداث واجهة المستخدم.
- منحنى التعلم: يحتوي RxJS على منحنى تعليمي حاد بسبب العدد الهائل من المشغلين والتحول في التفكير المطلوب للبرمجة التفاعلية.
- السحب مقابل الدفع: تظل النقطة الفاصلة. المولدات غير المتزامنة تعتمد في الأساس على السحب (المستهلك يتحكم)، في حين أن Observables تعتمد على الدفع (المنتج يتحكم، ويجب على المستهلك أن يتفاعل مع الضغط).
المولدات غير المتزامنة هي ميزة لغة أصلية، مما يجعلها خيارًا خفيف الوزن وخاليًا من التبعية للعديد من مشكلات الضغط الخلفي التي قد تتطلب خلاف ذلك مكتبة شاملة مثل RxJS.
الخلاصة: احتضن السحب
الضغط الخلفي ليس ميزة اختيارية؛ إنه مطلب أساسي لبناء تطبيقات معالجة البيانات المستقرة والقابلة للتطوير وذات الذاكرة الفعالة. إن إهماله هو وصفة لفشل النظام.
لسنوات، اعتمد مطورو JavaScript على واجهات برمجة تطبيقات معقدة تعتمد على الأحداث أو مكتبات خارجية لإدارة التحكم في تدفق التدفق. مع إدخال المولدات غير المتزامنة وبناء الجملة `for await...of`، أصبح لدينا الآن أداة قوية وأصلية وبديهية مدمجة مباشرة في اللغة.
من خلال التحول من نموذج يعتمد على الدفع إلى نموذج يعتمد على السحب، توفر المولدات غير المتزامنة ضغطًا خلفيًا متأصلًا. تحدد سرعة معالجة المستهلك بشكل طبيعي معدل المنتج، مما يؤدي إلى تعليمات برمجية:
- آمنة للذاكرة: تزيل المخازن المؤقتة غير المحدودة وتمنع أعطال نفاد الذاكرة.
- قابلة للقراءة: تحول المنطق غير المتزامن المعقد إلى حلقات بسيطة تبدو متسلسلة.
- قابلة للتركيب: تسمح بإنشاء خطوط أنابيب تحويل بيانات أنيقة وقابلة لإعادة الاستخدام.
- قوية: تبسط معالجة الأخطاء وإدارة الموارد باستخدام كتل `try...catch...finally` القياسية.
في المرة القادمة التي تحتاج فيها إلى معالجة تدفق البيانات - سواء كان ذلك من ملف أو واجهة برمجة تطبيقات أو أي مصدر غير متزامن آخر - لا تصل إلى التخزين المؤقت اليدوي أو ردود الاتصال المعقدة. احتضن الأناقة المستندة إلى السحب للمولدات غير المتزامنة. إنه نمط JavaScript حديث سيجعل التعليمات البرمجية غير المتزامنة أكثر نظافة وأمانًا وأكثر قوة.