हर माउंट किए गए JavaScript व्यू को एक नया AbortController दें, उसके सिग्नल के साथ लिसनर रजिस्टर करें और अनमाउंट करते समय उसे abort करें। यह ट्यूटोरियल ब्राउज़र में एक छोटा व्यू बनाता है और दिखाता है कि Mac पर Chrome में माउंट के बार-बार होने वाले चक्रों की जाँच कैसे करें।

व्यू दोबारा खुलने पर इवेंट हैंडलर क्यों जमा होते हैं?

पहली बार खुलने पर कोई व्यू सही दिख सकता है, लेकिन दोबारा खुलने पर एक ही घटना पर दो बार प्रतिक्रिया देने लगता है। इसका आम कारण ऐसा सेटअप कोड है जो हर माउंट पर एक और कॉलबैक बनाता है, लेकिन पिछला रजिस्ट्रेशन हटाता नहीं। अलग-अलग बनाए गए फ़ंक्शन अलग लिसनर ऑब्जेक्ट होते हैं, भले ही उनका स्रोत टेक्स्ट एक जैसा हो। MDN का addEventListener() दस्तावेज़ कॉलबैक की पहचान का यह नियम समझाता है।

DOM एक ही टार्गेट पर समान इवेंट प्रकार, कॉलबैक और कैप्चर मान वाले डुप्लिकेट रजिस्ट्रेशन रोकता है। लेकिन हर सेटअप में नया कॉलबैक बनने पर यह सुरक्षा बार-बार माउंट होने की समस्या हल नहीं करती। मानक यह भी बताता है कि लिसनर को दिया गया सिग्नल abort करने से वह लिसनर हट जाता है। इन नियमों से एक व्यावहारिक डिज़ाइन बनता है: एक माउंट अवधि के सभी नेटिव इवेंट लिसनर एक ही कंट्रोलर के अधीन हों। DOM Standard: EventTarget।

इस ट्यूटोरियल में पाठक का काम है कि सादे JavaScript काउंटर व्यू को खोलें, बंद करें और फिर से खोलें, बिना अतिरिक्त बटन या विंडो हैंडलर जोड़े। उदाहरण में कृत्रिम लेबल हैं, किसी फ्रेमवर्क की ज़रूरत नहीं है और माउंट के बीच काउंटर बने रहते हैं।

व्यू को किस लाइफ़साइकल अनुबंध का पालन करना चाहिए?

सेटअप लागू करने से पहले तय करें कि बार-बार होने वाली लाइफ़साइकल कॉल का क्या अर्थ होगा। नीचे दिया गया अनुबंध गलती से दोहराई गई कॉल को हानिरहित बनाता है और क्लीनअप की ज़िम्मेदारी स्पष्ट करता है।

ऑपरेशन अपेक्षित व्यवहार
पहला mount() व्यू जोड़ें और लिसनर का एक सेट रजिस्टर करें।
माउंट रहने के दौरान एक और mount() बिना कुछ बदले लौट जाएँ।
unmount() व्यू हटाने से पहले माउंट अवधि के लिसनर हटाएँ।
एक और unmount() बिना कुछ बदले लौट जाएँ।
अनमाउंट के बाद mount() नए कंट्रोलर का उपयोग करके नया सेट रजिस्टर करें; काउंटर बनाए रखें।
destroy() स्थायी रूप से अनमाउंट करें; बाद में माउंट करने पर त्रुटि दें।

ये ऐप्लिकेशन डिज़ाइन के निर्णय हैं। ब्राउज़र इवेंट रजिस्ट्रेशन और हटाने की सुविधा देता है; लाइफ़साइकल मेथड और उनका क्रम आपका व्यू तय करता है। बड़े ऐप्लिकेशन में, mount() कॉल करने वाले कॉम्पोनेंट या राउटर को उससे मेल खाने वाला unmount() भी व्यवस्थित करना चाहिए।

नया कंट्रोलर ज़रूरी है, क्योंकि abort किए गए सिग्नल को फिर से सक्रिय नहीं किया जा सकता। जब addEventListener() को पहले से abort किया गया सिग्नल मिलता है, तो DOM रजिस्ट्रेशन एल्गोरिदम लिसनर जोड़े बिना लौट जाता है। इसलिए पुराने कंट्रोलर का दोबारा उपयोग करने पर फिर से खुले व्यू के कंट्रोल काम करना बंद कर सकते हैं। DOM Standard: इवेंट लिसनर जोड़ना।

अपने Mac पर Chrome में पूरा उदाहरण बनाएँ

Chrome का नया टैब खोलें, एड्रेस बार में about:blank दर्ज करें और Return दबाएँ। पेज फ़ोकस में होने पर Command + Option + J दबाकर Console खोलें। Google अपने Chrome DevTools कीबोर्ड संदर्भ में Mac का यह शॉर्टकट बताता है।

नीचे दिया पूरा प्रोग्राम Console में चलाएँ। यह JavaScript के ज़रिए इंटरफ़ेस बनाता है, इसलिए दस्तावेज़ मार्कअप, पैकेज इंस्टॉलेशन या बिल्ड चरण की ज़रूरत नहीं है। Google के Console में JavaScript चलाने के ट्यूटोरियल में दिखाया गया है कि Chrome का कंसोल JavaScript चला सकता है और मौजूदा पेज का DOM बदल सकता है।

(() => {
  // Clean up the previous demo if this entire program is run again.
  window.lifecycleDemo?.destroy();

  const host = document.body;
  const root = document.createElement("section");
  const button = document.createElement("button");
  const output = document.createElement("p");

  button.type = "button";
  button.textContent = "Add one";
  root.append(button, output);

  let controller = null;
  let destroyed = false;
  let clicks = 0;
  let resizes = 0;

  function render() {
    output.textContent =
      `Clicks: ${clicks}; resize events: ${resizes}`;
  }

  function mount() {
    if (destroyed) {
      throw new Error("This view has been destroyed.");
    }
    if (controller !== null) return;

    const next = new AbortController();
    const options = { signal: next.signal };

    try {
      button.addEventListener("click", () => {
        clicks += 1;
        render();
      }, options);

      window.addEventListener("resize", () => {
        resizes += 1;
        render();
      }, options);

      render();
      host.append(root);
      controller = next;
    } catch (error) {
      // Roll back any registrations made before setup failed.
      next.abort();
      root.remove();
      throw error;
    }
  }

  function unmount() {
    if (controller === null) return;
    controller.abort();
    controller = null;
    root.remove();
  }

  function destroy() {
    unmount();
    destroyed = true;
  }

  window.lifecycleDemo = {
    mount,
    unmount,
    destroy,
    button,
    get counts() {
      return { clicks, resizes };
    }
  };

  mount();
})();

अब पेज पर Add one बटन और काउंटर की एक पंक्ति दिखेगी। बटन क्लिक करें: क्लिक की गिनती एक बढ़नी चाहिए। Chrome विंडो का आकार बदलें: resize इवेंट की गिनती बदलनी चाहिए। विंडो का आकार बदलने पर कई इवेंट बन सकते हैं, इसलिए यह गिनती इवेंट की है, माउस जेस्चर की नहीं।

बटन लिसनर और विंडो लिसनर एक ही सिग्नल साझा करते हैं। अनमाउंट करने पर रूट को हटाने से पहले वह कंट्रोलर abort हो जाता है। अगला माउंट एक नया कंट्रोलर और कॉलबैक का एक नया सेट बनाता है। काउंटर व्यू के क्लोज़र में रहते हैं, इसलिए दोबारा खोलने पर उनके मान बने रहते हैं; पूरा प्रोग्राम नष्ट करके फिर चलाने पर नया इंस्टेंस शुरू होता है।

सेटअप ब्लॉक आंशिक विफलता को भी संभालता है: अगर लिसनर रजिस्टर करने के बाद सेटअप में त्रुटि आती है, तो catch ब्लॉक नए कंट्रोलर को abort करता है, रूट अलग करता है और त्रुटि फिर से उठाता है। इससे विफल माउंट का कुछ सेटअप चुपचाप सक्रिय नहीं रह जाता।

बार-बार माउंट होने पर क्लीनअप की जाँच कैसे करें?

पहले लाइफ़साइकल को मैन्युअल रूप से आज़माएँ। मुख्य प्रोग्राम इंस्टॉल करने के बाद नीचे दिया क्रम चलाएँ। व्यू गायब हो जाएगा, दोबारा अनमाउंट करने से कोई अतिरिक्त असर नहीं होगा और फिर से खोलने पर वह पिछली गिनतियों के साथ वापस आएगा।

window.lifecycleDemo.unmount();
window.lifecycleDemo.unmount();
window.lifecycleDemo.mount();
window.lifecycleDemo.mount();

इसके बाद उसी कंसोल में यह पूरी जाँच चलाएँ। यह ऊपर दिए मुख्य प्रोग्राम पर निर्भर करती है। हर चक्र जानबूझकर सेटअप और क्लीनअप दो-दो बार कॉल करता है, माउंट रहने के दौरान हर टार्गेट पर एक इवेंट भेजता है और अनमाउंट रहने के दौरान भी वही इवेंट भेजता है। गिनती में अंतर होने पर तुरंत त्रुटि आती है।

(() => {
  const view = window.lifecycleDemo;
  if (!view) throw new Error("Run the main program first.");

  function check(actual, expected, label) {
    if (actual !== expected) {
      throw new Error(`${label}: expected ${expected}, got ${actual}`);
    }
  }

  for (let cycle = 0; cycle < 20; cycle += 1) {
    view.mount();
    view.mount();
    const before = view.counts;

    view.button.click();
    window.dispatchEvent(new Event("resize"));

    check(view.counts.clicks, before.clicks + 1, "Mounted click");
    check(view.counts.resizes, before.resizes + 1, "Mounted resize");

    view.unmount();
    view.unmount();
    const stopped = view.counts;

    // The button reference remains available while detached.
    view.button.click();
    window.dispatchEvent(new Event("resize"));

    check(view.counts.clicks, stopped.clicks, "Unmounted click");
    check(view.counts.resizes, stopped.resizes, "Unmounted resize");
  }

  view.mount();
  console.log("Passed 20 mount/unmount cycles.");
})();

कृत्रिम resize इवेंट विंडो को हिलाए बिना अपेक्षित बदलाव को निश्चित बनाता है। हटाए गए बटन की जाँच उपयोगी है, क्योंकि यह उदाहरण जानबूझकर उसे रीमाउंट करने के लिए बनाए रखता है। दोनों जाँचों को पास होना चाहिए: सिर्फ़ दिख रहे बटन की जाँच करने से विंडो लिसनर की जाँच नहीं होगी। यह परीक्षण हर काउंटर में 20 जोड़ता है और व्यू को माउंटेड स्थिति में छोड़ता है।

सीधे निरीक्षण के लिए Chrome DevTools कंसोल में getEventListeners(object) उपलब्ध कराता है। यह दिए गए ऑब्जेक्ट पर रजिस्टर किए गए लिसनर को इवेंट प्रकार के अनुसार समूहित करके लौटाता है। यह DevTools की यूटिलिटी है, इसलिए इसे ऐप्लिकेशन कोड में न रखें। Chrome Console Utilities API संदर्भ।

मुख्य प्रोग्राम चलाने के बाद, यह निरीक्षण क्रम क्लीनअप से पहले और बाद के रजिस्ट्रेशन की तुलना करता है। खाली डेमो पेज पर माउंटेड रहने के दौरान बटन के लिए एक click लिसनर और विंडो के लिए एक resize लिसनर दिखना चाहिए; अनमाउंट होने पर दोनों शून्य होने चाहिए।

(() => {
  const view = window.lifecycleDemo;
  if (!view) throw new Error("Run the main program first.");

  view.mount();
  console.log("Mounted", {
    button: (getEventListeners(view.button).click ?? []).length,
    window: (getEventListeners(window).resize ?? []).length
  });

  view.unmount();
  console.log("Unmounted", {
    button: (getEventListeners(view.button).click ?? []).length,
    window: (getEventListeners(window).resize ?? []).length
  });

  view.mount();
})();

मौजूदा ऐप्लिकेशन पेज पर दूसरे कोड के पास विंडो लिसनर हो सकते हैं। कुल संख्या के शून्य होने की अपेक्षा करने के बजाय, पहले और बाद के अंतर की तुलना करें और कॉलबैक का निरीक्षण करें।

इसके बजाय स्पष्ट रूप से हटाने का उपयोग कब करें?

removeEventListener() का उपयोग तब करें जब मौजूदा कोड के साथ एकीकरण करते हुए कॉलबैक पहले से संग्रहीत हों, या आवश्यक एनवायरनमेंट लिसनर के signal विकल्प का समर्थन न करता हो। क्लीनअप के लिए मूल टार्गेट, इवेंट प्रकार, कॉलबैक संदर्भ और कैप्चर मान सुरक्षित रखें। MDN बताता है कि कैप्चर का मान समान होना चाहिए, जबकि passive जैसे विकल्प हटाने के लिए मिलान तय नहीं करते। MDN का removeEventListener() संदर्भ।

उदाहरण के लिए, अगर सेटअप में bind() का उपयोग हो, तो एक bound कॉलबैक को एक बार संग्रहीत करें। क्लीनअप के दौरान फिर से bind() कॉल करने पर फ़ंक्शन का एक अलग संदर्भ बनता है। इसी तरह, हटाने वाली कॉल में वही arrow function दोबारा लिखने से पहले रजिस्टर किया गया फ़ंक्शन वापस नहीं मिलता। स्पष्ट क्लीनअप को सेटअप के पास रखें, ताकि दोनों साथ-साथ विकसित हों।

कंट्रोलर पैटर्न उन नेटिव लिसनर को संभालता है जिन्हें उसके सिग्नल के साथ रजिस्टर किया गया है। अगर कोई व्यू टाइमर, ऑब्ज़र्वर या लाइब्रेरी सब्सक्रिप्शन भी बनाता है, तो उनके दस्तावेज़ में दिए क्लीनअप ऑपरेशन उसी अनमाउंट सीमा पर करें। नेटिव लिसनर को सिग्नल देने से असंबंधित संसाधनों का क्लीनअप अपने आप तय नहीं होता।

कॉलबैक फिर भी कई बार चलें तो क्या जाँचें?

एक साथ कई लाइफ़साइकल मेथड बदलने के बजाय, लक्षण के आधार पर जाँच सीमित करें।

लक्षण अगली जाँच
एक क्लिक से गिनती दो बार बढ़ती है दूसरे रजिस्ट्रेशन पथ या दूसरे सक्रिय व्यू इंस्टेंस की जाँच करें।
दोबारा खोलने पर कंट्रोल काम करना बंद कर देते हैं जाँचें कि सेटअप ने abort किए गए सिग्नल का फिर से उपयोग तो नहीं किया।
अनमाउंट के बाद गिनतियाँ बदलती हैं हर इवेंट टार्गेट और यह जाँचें कि हर रजिस्ट्रेशन को अपेक्षित सिग्नल मिला था या नहीं।
स्पष्ट रूप से हटाने पर कोई असर नहीं होता टार्गेट, इवेंट प्रकार, कॉलबैक पहचान और कैप्चर मान की तुलना करें।
पहली कार्रवाई काम करती है, लेकिन बाद की नहीं जाँचें कि लिसनर once: true का उपयोग तो नहीं करता।

once विकल्प लिसनर को पहली बार चलने के बाद हटा देता है। यह सचमुच एक बार होने वाली बातचीत के लिए उपयुक्त हो सकता है, लेकिन व्यू की माउंटेड अवधि को व्यक्त नहीं करता: अनमाउंट होने से पहले इवेंट हो भी सकता है और नहीं भी। MDN अपने addEventListener() विकल्प संदर्भ में once और सिग्नल-आधारित हटाने, दोनों का वर्णन करता है।

डेमो पूरा होने पर window.lifecycleDemo.destroy() चलाकर व्यू अलग करें और उसके लिसनर हटाएँ। अपने व्यू के लिए भी यही व्यावहारिक अनुबंध रखें: एक सक्रिय सेटअप, बार-बार किया जा सकने वाला क्लीनअप, दोबारा खोलने पर नए रजिस्ट्रेशन, और ऐसी जाँच जो दिख रहे कंट्रोल तथा लंबे समय तक सक्रिय रहने वाले इवेंट टार्गेट, दोनों को आज़माए।

स्रोत