समस्या और लागू संदर्भ
चार विधियों के साथ एक EventEmitter क्लास लागू करें: on(eventName, listener) एक स्थायी लिसनर पंजीकृत करता है, once(eventName, listener) एक बार चलने वाले लिसनर को पंजीकृत करता है, off(eventName, listener) एक मिलान वाले पंजीकरण को हटाता है, और emit(eventName, ...args) सिंक्रोनस रूप से एक इवेंट डिस्पैच करता है। इवेंट का नाम स्ट्रिंग या Symbol हो सकता है। एक ही फ़ंक्शन को एक इवेंट के लिए एक से अधिक बार पंजीकृत किया जा सकता है।
यह प्रश्न Node-शैली के सेमेन्टिक्स के एक स्पष्ट सबसेट का उपयोग करता है। लिसनर्स पंजीकरण क्रम में सिंक्रोनस रूप से चलते हैं। एक सामान्य लिसनर फ़ंक्शन के अंदर this एमीटर होता है। जब इवेंट में लिसनर्स होते हैं तो emit, true लौटाता है और अन्यथा false लौटाता है, और लिसनर के रिटर्न मानों को अनदेखा कर दिया जाता है। एक once पंजीकरण को उसके लिसनर को कॉल करने से पहले हटा दिया जाना चाहिए ताकि एक रीएंट्रेंट emit इसे फिर से इनवोक न कर सके।
एक डिस्पैच के लिए लिसनर्स तब तय होते हैं जब वह डिस्पैच शुरू होता है। यदि कोई लिसनर डिस्पैच के दौरान किसी अन्य को हटा देता है, तो हटाया गया लिसनर अभी भी पहले से चल रहे डिस्पैच में भाग लेता है लेकिन बाद वाले में नहीं। एक नया जोड़ा गया लिसनर बाद के डिस्पैच तक प्रतीक्षा करता है। यह कार्यान्वयन Node.js के विशेष error इवेंट, लिसनर-काउंट चेतावनियों, prependListener, एसिंक्रोनस इटरेशन या Promise रिजेक्शन कैप्चर को पुन: प्रस्तुत नहीं करता है; वे व्यापक अनुकूलता असाइनमेंट से संबंधित हैं।
साक्षात्कारकर्ता क्या मूल्यांकन कर रहा है
एक मजबूत उत्तर कंटेनर चुनने से पहले इवेंट कॉन्ट्रैक्ट को परिभाषित करता है। एक Map<eventName, entries[]> किसी इवेंट के लिए ऐरे का पता लगाता है, और ऐरे पंजीकरण क्रम को सुरक्षित रखता है। प्रत्येक प्रविष्टि मूल फ़ंक्शन को संग्रहीत करती है, क्या यह एक once पंजीकरण है, क्या यह निष्पादित हो चुका है, और क्या यह सक्रिय रहता है। एक Set हटाना सुविधाजनक बनाता है लेकिन चुपचाप डुप्लिकेट फ़ंक्शनों को समाहित कर लेता है, जिससे बताए गए डुप्लिकेट-पंजीकरण नियम का उल्लंघन होता है।
दूसरा संकेत लाइव ऐरे को डिस्पैच स्नैपशॉट से अलग करना है। लाइव ऐरे को सीधे पुनरावृत्त करने से पहले के लिसनर की off कॉल इंडेक्स को शिफ्ट कर सकती है और अगले आइटम को छोड़ सकती है। एक on कॉल एक लिसनर को भी जोड़ सकता है जो अप्रत्याशित रूप से वर्तमान डिस्पैच में शामिल हो जाता है। सक्रिय प्रविष्टियों को पहले कॉपी करने से म्यूटेशन सीमा अगले emit पर तय हो जाती है।
तीसरा संकेत रीएंट्रेंट once व्यवहार है। कॉलबैक लौटने के बाद एक बार के लिसनर को हटाना बहुत देर हो चुकी होती है: कॉलबैक उसी इवेंट को तब उत्सर्जित कर सकता है जब उसका पंजीकरण अभी भी मौजूद हो। इनवोकेशन से पहले प्रविष्टि को निष्क्रिय और निष्पादित के रूप में चिह्नित किया जाना चाहिए। निष्पादित फ़्लैग बाहरी स्नैपशॉट को नेस्टेड डिस्पैच द्वारा पहले ही किए जाने के बाद प्रविष्टि को इनवोक करने से भी रोकता है।
जटिलता को सटीक रूप से बताया जाना चाहिए। यदि किसी इवेंट में k लिसनर्स हैं, तो on और once में ऐरे अपेंड अमोर्टाइज्ड O(1) है। off पीछे की ओर स्कैन करता है और एक ऐरे तत्व को हटाता है, इसलिए यह O(k) है। स्नैपशॉट निर्माण, इनवोकेशन और क्लीनअप emit को O(k) बनाते हैं, साथ ही लिसनर्स का अपना रनिंग समय भी। इवेंट लुकअप को आमतौर पर Map कार्यान्वयन के लिए औसत O(1) माना जाता है, लेकिन ECMAScript को केवल रैखिक से बेहतर औसत पहुंच की आवश्यकता होती है। एक Map सभी चार ऑपरेशनों को बिना शर्त O(1) नहीं बनाता है।
उत्तर देने से पहले स्पष्ट करने योग्य प्रश्न
- किन विधियों और रिटर्न मानों की आवश्यकता है? केवल
on,offऔरemitके साथ, बनाए रखने के लिए कोई वन-टाइम स्थिति नहीं है।onceजोड़ने के लिए एक सटीक निष्कासन बिंदु की आवश्यकता होती है।emitसे लिसनर परिणाम लौटाने से डेटा प्रवाह बदल जाएगा; यह कॉन्ट्रैक्ट केवल यह लौटाता है कि क्या लिसनर्स मौजूद थे। - क्या एक ही फ़ंक्शन को एक से अधिक बार पंजीकृत किया जा सकता है, और क्या
offएक या सभी पंजीकरणों को हटाता है? यह कॉन्ट्रैक्ट डुप्लिकेट की अनुमति देता है और सबसे हाल के मिलान पंजीकरण को हटाता है। एक डीडुप्लिकेटिंग Set एक अलग कॉन्ट्रैक्ट को लागू करेगा। - डिस्पैच के दौरान जोड़ना और हटाना कब प्रभावी होता है? यह कॉन्ट्रैक्ट स्नैपशॉट सेमेन्टिक्स का उपयोग करता है: शुरुआत में मौजूद लिसनर्स वर्तमान डिस्पैच में शामिल होते हैं, जबकि जोड़ना और हटाना बाद के डिस्पैच को प्रभावित करता है। तत्काल हटाने की आवश्यकता वाले कॉन्ट्रैक्ट को प्रत्येक कॉल से पहले एक सक्रिय-स्थिति जांच की आवश्यकता होगी।
- क्या लिसनर्स सिंक्रोनस हैं, और जब कोई एरर थ्रो करता है तो क्या होता है? लिसनर्स क्रम में सिंक्रोनस रूप से चलते हैं। थ्रो की गई एरर
emitसे प्रसारित होती है, इसलिए बाद के लिसनर्स नहीं चलते हैं। निष्क्रिय प्रविष्टियों की सफाई अभी भीfinallyमें होनी चाहिए। - क्या पूर्ण Node.js अनुकूलता आवश्यक है? यदि है, तो विशेष
errorव्यवहार, मेटा-इवेंट्स, लिसनर सीमाएं और अधिक API दायरे में प्रवेश करते हैं। यह कार्यान्वयन केवल घोषित मुख्य सबसेट को कवर करता है; क्लास का नाम साझा करने से पूर्ण अनुकूलता स्थापित नहीं होती है।
ये उत्तर स्टोरेज मॉडल, लूप स्थितियों, रिटर्न मान और एरर बाउंड्री को बदलते हैं, इसलिए कोडिंग से पहले उन्हें तय करें।
30-सेकंड उत्तर फ्रेमवर्क
“मैं Map में प्रति इवेंट एक एंट्री ऐरे स्टोर करूंगा, पंजीकरण क्रम में सिंक्रोनस रूप से डिस्पैच करूंगा, और डुप्लिकेट की अनुमति दूंगा। off नवीनतम मिलान को हटाता है; डिस्पैच म्यूटेशन अगली बार प्रभावी होते हैं। emit सक्रिय प्रविष्टियों का स्नैपशॉट लेता है। once को कॉल करने से पहले, यह रीएंट्रेंसी को ब्लॉक करने के लिए प्रविष्टि को निष्क्रिय और निष्पादित चिह्नित करता है, फिर finally में सफाई करता है। औसत Map लुकअप के तहत, पंजीकरण अमोर्टाइज्ड O(1) है, जबकि off और emit, O(k) हैं।”
चरण-दर-चरण गहन विश्लेषण
सबसे छोटा कार्यान्वयन प्रत्येक इवेंट को फ़ंक्शनों के ऐरे में मैप करता है और उन्हें क्रम से कॉल करता है। यह एक ऐसे नमूने को पास करता है जो दो फ़ंक्शनों को पंजीकृत करता है और एक बार उत्सर्जित करता है, लेकिन चार परिणाम-परिवर्तनकारी प्रश्नों को अनुत्तरित छोड़ देता है: डुप्लिकेट फ़ंक्शन कैसे हटाए जाते हैं, जब डिस्पैच के दौरान ऐरे बदलता है तो क्या होता है, एक बार के लिसनर को कब हटाया जाता है, और लिसनर द्वारा एरर थ्रो करने के बाद स्थिति को कैसे साफ किया जाता है।
अनुशंसित कार्यान्वयन पंजीकरण रिकॉर्ड को उसके फ़ंक्शन से अलग करता है। एक ही फ़ंक्शन के दो पंजीकरण अलग-अलग प्रविष्टियां बने रहते हैं, और once को एक रैपर के पीछे मूल फ़ंक्शन को छिपाने की आवश्यकता नहीं होती है:
class EventEmitter {
constructor() {
this.events = new Map();
}
on(eventName, listener) {
return this._add(eventName, listener, false);
}
once(eventName, listener) {
return this._add(eventName, listener, true);
}
off(eventName, listener) {
const entries = this.events.get(eventName);
if (!entries) return this;
for (let index = entries.length - 1; index >= 0; index -= 1) {
const entry = entries[index];
if (entry.active && entry.listener === listener) {
entry.active = false;
entries.splice(index, 1);
break;
}
}
if (entries.length === 0) {
this.events.delete(eventName);
}
return this;
}
emit(eventName, ...args) {
const entries = this.events.get(eventName);
if (!entries) return false;
const snapshot = entries.filter((entry) => entry.active);
if (snapshot.length === 0) return false;
try {
for (const entry of snapshot) {
if (entry.once) {
if (entry.fired) continue;
entry.fired = true;
entry.active = false;
}
entry.listener.apply(this, args);
}
} finally {
const currentEntries = this.events.get(eventName);
if (currentEntries) {
const activeEntries = currentEntries.filter((entry) => entry.active);
if (activeEntries.length === 0) {
this.events.delete(eventName);
} else if (activeEntries.length !== currentEntries.length) {
this.events.set(eventName, activeEntries);
}
}
}
return true;
}
_add(eventName, listener, once) {
if (typeof listener !== "function") {
throw new TypeError("listener must be a function");
}
const entries = this.events.get(eventName);
const entry = { listener, once, fired: false, active: true };
if (entries) {
entries.push(entry);
} else {
this.events.set(eventName, [entry]);
}
return this;
}
}स्नैपशॉट वर्तमान डिस्पैच के लिए सदस्यता और क्रम तय करता है, लेकिन प्रविष्टियों के संदर्भ रखता है। डिस्पैच के दौरान off द्वारा हटाई गई सामान्य प्रविष्टि अभी भी वर्तमान स्नैपशॉट से इनवोक की जाती है; अगला स्नैपशॉट बनने पर यह गायब हो जाती है। एक नई प्रविष्टि केवल लाइव ऐरे में जोड़ी जाती है, इसलिए यह पुराने स्नैपशॉट में प्रवेश नहीं कर सकती है।
एक बार की प्रविष्टि के लिए दो स्थिति फ़ील्ड की आवश्यकता होती है। active = false इसे नेस्टेड emit से छुपाता है। fired = true एक कम स्पष्ट रीएंट्रेंट क्रम को संभालता है: एक नेस्टेड डिस्पैच प्रविष्टि को पहले इनवोक कर सकता है जबकि बाहरी स्नैपशॉट अभी भी वही संदर्भ रखता है। बाहरी लूप बाद में fired देखता है और दूसरे इनवोकेशन को छोड़ देता है। दोनों निशान लिसनर कॉल से पहले होते हैं, इसलिए थ्रो की गई एरर वन-टाइम पंजीकरण को पुनर्जीवित नहीं कर सकती है।
finally ब्लॉक निष्क्रिय प्रविष्टियों को कॉम्पैक्ट करता है। इसके बिना, एरर थ्रो करने वाला लिसनर लाइव ऐरे में तार्किक रूप से हटाई गई once प्रविष्टि छोड़ सकता है। अपवाद अभी भी emit के कॉलर को प्रसारित होता है; finally विफलता को दबाए बिना आंतरिक इनवेरिएंट को सुरक्षित रखता है।
कम से कम, इन सीमाओं का परीक्षण करें: कोई लिसनर नहीं होने पर false लौटाता है; तर्क और this अग्रेषित किए जाते हैं; डुप्लिकेट पंजीकरण डुप्लिकेट कॉल उत्पन्न करते हैं और off केवल नवीनतम को हटाता है; बार-बार डिस्पैच once लिसनर को एक बार कॉल करता है; डिस्पैच के दौरान बाद के लिसनर को हटाने से वह उस डिस्पैच से नहीं हटता; जोड़ा गया लिसनर अगले डिस्पैच की प्रतीक्षा करता है; नेस्टेड डिस्पैच once प्रविष्टि को दो बार कॉल नहीं कर सकता है; और एरर थ्रो करने वाला लिसनर अभी भी वन-टाइम प्रविष्टि को हटा देता है।
उच्च गुणवत्ता वाला नमूना उत्तर
“मैं इसे पूर्ण अनुकूलता के बजाय Node-शैली के मुख्य कॉन्ट्रैक्ट तक सीमित रखूंगा। इवेंट के नाम स्ट्रिंग या Symbols हो सकते हैं। लिसनर्स पंजीकरण क्रम में सिंक्रोनस रूप से चलते हैं, डुप्लिकेट की अनुमति है, और off सबसे हाल के मिलान पंजीकरण को हटाता है। डिस्पैच एक स्नैपशॉट का उपयोग करता है, इसलिए एक डिस्पैच के दौरान जोड़ना और हटाना बाद के डिस्पैच को प्रभावित करता है। emit रिपोर्ट करता है कि क्या उसे लिसनर्स मिले, और लिसनर अपवाद इसके कॉलर तक प्रसारित होते हैं।
मैं Map में प्रति इवेंट एक एंट्री ऐरे स्टोर करूंगा। ऐरे क्रम को सुरक्षित रखता है, जबकि प्रत्येक प्रविष्टि मूल फ़ंक्शन और वन-टाइम स्थिति को बरकरार रखती है। emit पहले वर्तमान स्नैपशॉट को फ़िल्टर करता है। वन-टाइम प्रविष्टि को इनवोक करने से पहले, यह इसे निष्क्रिय और निष्पादित के रूप में चिह्नित करता है: निष्क्रिय इसे नेस्टेड डिस्पैच द्वारा चुने जाने से रोकता है, और निष्पादित बाहरी स्नैपशॉट को नेस्टेड डिस्पैच द्वारा पहले से किए जाने के बाद इनवोक करने से रोकता है। लूप के चारों ओर एक try...finally अपवाद को पकड़े बिना लिसनर द्वारा थ्रो करने के बाद सफाई की गारंटी देता है।
यह मॉडल डुप्लिकेट पंजीकरणों और स्नैपशॉट सेमेन्टिक्स को सुरक्षित रखता है। पारंपरिक औसत Map लुकअप धारणा के तहत, on और once में जोड़ना अमोर्टाइज्ड O(1) है। off एक ऐरे को स्कैन और शिफ्ट करता है, और emit प्रविष्टियों को कॉपी, इनवोक और साफ करता है, इसलिए दोनों O(k) हैं। यदि off को O(1) होना होता, तो मैं अधिक कोड और मेमोरी स्वीकार करते हुए डबली लिंक्ड-लिस्ट प्रविष्टियों के साथ एक इंडेक्स का उपयोग करता; वर्तमान बाधाएं उस लागत को उचित नहीं ठहराती हैं।”
सामान्य गलतियाँ
- लिसनर्स को
Setमें स्टोर करना → एक ही फ़ंक्शन को दो बार पंजीकृत करने पर एक आइटम रह जाता है और डुप्लिकेट सेमेन्टिक्स बदल जाता है → एक एंट्री ऐरे का उपयोग करें ताकि प्रत्येक पंजीकरण विशिष्ट बना रहे। - लाइव ऐरे को सीधे पुनरावृत्त करना →
offइंडेक्स को शिफ्ट करता है औरonवर्तमान डिस्पैच में एक नया आइटम ला सकता है → डिस्पैच शुरू होने पर सक्रिय प्रविष्टियों का एक स्नैपशॉट लें। - कॉलबैक लौटने के बाद
onceको हटाना → एक कॉलबैक हटाने से पहले इवेंट में फिर से प्रवेश कर सकता है, और थ्रो की गई एरर निष्कासन को पूरी तरह से रोक सकती है → इनवोकेशन से पहलेactive = falseऔरfired = trueसेट करें। - निष्पादित स्थिति के बिना निष्क्रिय स्थिति रिकॉर्ड करना → एक बार नेस्टेड डिस्पैच द्वारा इनवोक करने के बाद, एक पुराना बाहरी स्नैपशॉट उसी प्रविष्टि को फिर से इनवोक कर सकता है → साझा प्रविष्टि पर एक निष्पादित गार्ड संग्रहीत करें।
- प्रत्येक मिलान फ़ंक्शन को हटाने के लिए
offमेंfilterका उपयोग करना → एक कॉल सभी डुप्लिकेट पंजीकरणों को मिटा देती है → पीछे की ओर स्कैन करें और केवल नवीनतम सक्रिय प्रविष्टि को हटाएं। - लिसनर एरर को पकड़ना और जारी रखना → कॉलर विफलता खो देता है और एरर बाउंड्री चुपचाप बदल जाती है →
finallyमें स्थिति को साफ करें और अपवाद को प्रसारित होने दें। - यह दावा करना कि प्रत्येक ऑपरेशन
O(1)है → Map सख्त निरंतर लुकअप का वादा नहीं करता है, और ऐरे खोज, निष्कासन, स्नैपशॉट निर्माण और ट्रैवर्सल लिसनर गणना पर निर्भर करते हैं → Map धारणा बताएं, फिर अमोर्टाइज्डon/once O(1)औरoff/emit O(k)अलग से दें।
अनुवर्ती प्रश्न और उत्तर
अनुवर्ती 1: डिस्पैच के दौरान हटाए गए लिसनर को अभी भी उस डिस्पैच को पूरा क्यों करना चाहिए?
एक स्नैपशॉट तब प्रतिभागियों को तय करता है जब emit शुरू होता है और पहले के लिसनर को बाद के इंडेक्स को बदलने से रोकता है। कार्यान्वयन स्नैपशॉट में प्रविष्टि संदर्भ रखता है लेकिन सामान्य लिसनर को कॉल करने से पहले active को दोबारा नहीं जांचता है, इसलिए यह अभी भी वर्तमान डिस्पैच में चलता है। अगला स्नैपशॉट इसे बाहर करता है। एक उत्पाद प्रत्येक कॉल से पहले active की जांच करके तत्काल निष्कासन चुन सकता है, लेकिन वह एक अलग कॉन्ट्रैक्ट है और इसके लिए अलग परीक्षणों की आवश्यकता होती है।
अनुवर्ती 2: आप कैसे साबित कर सकते हैं कि once रीएंट्रेंसी के तहत अधिकतम एक बार चलता है?
एक वन-टाइम प्रविष्टि केवल तभी अपने कॉल पथ में प्रवेश कर सकती है जब fired गलत हो। इस तक पहुंचने वाला पहला डिस्पैच लिसनर को कॉल करने से पहले fired को सही पर सेट करता है। नेस्टेड और बाहरी स्नैपशॉट समान प्रविष्टि संदर्भ रखते हैं, इसलिए प्रत्येक बाद का प्रयास सही देखता है और इसे छोड़ देता है। यह नेस्टिंग क्रम की परवाह किए बिना अधिकतम-एक-बार व्यवहार स्थापित करता है। पहले एक सामान्य लिसनर रखकर सबसे कठिन क्रम का परीक्षण करें जो केवल अपनी पहली कॉल पर पुनरावर्ती रूप से उत्सर्जित करता है, जिसके बाद एक once लिसनर आता है।
अनुवर्ती 3: यदि off को O(1) होना चाहिए तो क्या बदलता है?
एक ऐरे O(1) में मनमाना-प्रविष्टि निष्कासन और स्थिर क्रम दोनों प्रदान नहीं कर सकता है। प्रति इवेंट एक डबली लिंक्ड लिस्ट और एक Map<listener, nodes[]> बनाए रखें जो किसी फ़ंक्शन के लिए सबसे हाल ही में पंजीकृत नोड का पता लगाता है। उस नोड को अनलिंक करना O(1) है, जबकि emit, O(k) बना रहता है। लागत प्रति पंजीकरण दो पॉइंटर्स, डुप्लिकेट-फ़ंक्शन इंडेक्स रखरखाव और अधिक जटिल स्नैपशॉट व्यवहार है। कम निष्कासन वाले छोटे लिसनर सेटों के लिए, ऐरे को सत्यापित करना आसान है।
अनुवर्ती 4: क्या होगा यदि कोई लिसनर Promise लौटाता है या एरर थ्रो करता है?
वर्तमान emit सिंक्रोनस है और रिटर्न मानों को अनदेखा करता है। एक सिंक्रोनस थ्रो डिस्पैच को रोकता है और कॉलर को प्रसारित करता है; finally केवल स्थिति को साफ करता है। लौटाए गए Promise का इंतज़ार (await) नहीं किया जाता है। यदि कॉलर्स को पूर्णता की आवश्यकता है, तो एक अलग emitAsync कॉन्ट्रैक्ट परिभाषित करें और क्रमिक या समानांतर निष्पादन के साथ-साथ फेल-फास्ट या सभी-परिणाम एरर हैंडलिंग चुनें। उन निर्णयों के बिना मौजूदा लूप में await जोड़ने से API अस्पष्ट हो जाता है।
अनुवर्ती 5: आप कैसे सत्यापित करेंगे कि डिस्पैच के दौरान जोड़ा गया लिसनर अगले डिस्पैच तक प्रतीक्षा करता है?
A को पंजीकृत करें और कॉल किए जाने पर A से B को पंजीकृत कराएं। पहले emit स्नैपशॉट में केवल A शामिल है, इसलिए केवल A दिखाई देता है। दूसरे स्नैपशॉट में A और B शामिल हैं, जिससे A और फिर B बनता है। साथ ही C के पहले स्नैपशॉट में प्रवेश करने के बाद A से C को हटवाएं: पहले डिस्पैच को अभी भी A और फिर C का उत्पादन करना चाहिए, जबकि दूसरा C को बाहर करता है। साथ में, वे अभिकथन म्यूटेशन सीमा के दोनों पक्षों को कवर करते हैं।