JavaScript event loop ਜੋ ਤੁਹਾਡੇ ਵੈੱਬ ਪੇਜ ਨੂੰ ਚਲਾਉਂਦਾ ਹੈ, ਉਹ ਉਸ ਤੋਂ ਬਹੁਤ ਵੱਖਰਾ ਵਿਵਹਾਰ ਕਰਦਾ ਹੈ ਜੋ ਇੱਕ Node.js ਸਰਵਰ ਨੂੰ ਚਲਾਉਂਦਾ ਹੈ, ਅਤੇ ਜੇਕਰ ਤੁਸੀਂ ਸਾਵਧਾਨ ਨਹੀਂ ਹੋ ਤਾਂ ਇਹ ਅਸੰਗਤਤਾ UI ਨੂੰ ਫ੍ਰੀਜ਼ ਕਰ ਸਕਦੀ ਹੈ ਜਾਂ I/O ਨੂੰ ਰੋਕ ਸਕਦੀ ਹੈ। ਜੋ ਕੋਈ ਵੀ ਅਜਿਹਾ async ਕੋਡ ਲਿਖਦਾ ਹੈ ਜੋ ਦੋਵਾਂ ਮਾਹੌਲਾਂ ਵਿੱਚ ਚੱਲਦਾ ਹੈ, ਉਸ ਲਈ ਇਹ ਜਾਣਨਾ ਬਹੁਤ ਜ਼ਰੂਰੀ ਹੈ ਕਿ ਇਹ ਦੋਵੇਂ ਕਿੱਥੇ ਵੱਖ ਹੁੰਦੇ ਹਨ।

ਇਹ ਅੰਤਰ ਕਿਉਂ ਮਹੱਤਵਪੂਰਨ ਹੈ

Event loop ECMAScript spec ਦੁਆਰਾ ਪਰਿਭਾਸ਼ਿਤ ਨਹੀਂ ਕੀਤਾ ਗਿਆ ਹੈ; ਇਹ host ਵਿੱਚ ਹੁੰਦਾ ਹੈ। ਬ੍ਰਾਊਜ਼ਰਾਂ ਨੂੰ ਫਰੇਮਾਂ ਰੈਂਡਰ ਕਰਦੇ ਸਮੇਂ ਪੇਜ ਨੂੰ responsive ਰੱਖਣਾ ਪੈਂਦਾ ਹੈ, ਜਦੋਂ ਕਿ Node.js non-blocking I/O ਦੇ ਆਲੇ-ਦੁਆਲੇ ਬਣਾਇਆ ਗਿਆ ਹੈ। ਇੱਕ host ਵਿੱਚ ਕੰਮ ਕਰਨ ਵਾਲੇ ਪੈਟਰਨਾਂ ਨੂੰ ਦੂਜੇ ਵਿੱਚ ਮਿਲਾਉਣ ਨਾਲ ਅਜਿਹੇ ਬੱਗ (bugs) ਪੈਦਾ ਹੋ ਸਕਦੇ ਹਨ ਜਿਨ੍ਹਾਂ ਨੂੰ ਦੁਬਾਰਾ ਲੱਭਣਾ ਮੁਸ਼ਕਲ ਹੁੰਦਾ ਹੈ: promises ਦੀ ਇੱਕ ਲੰਬੀ ਚੇਨ ਬ੍ਰਾਊਜ਼ਰ ਦੇ repaint ਨੂੰ ਰੋਕ ਸਕਦੀ ਹੈ, ਜਦੋਂ ਕਿ ਇੱਕ ਅਣਚੇਤੇ process.nextTick ਲੂਪ ਕਾਰਨ Node ਕਦੇ ਵੀ ਆਪਣੇ I/O phases ਤੱਕ ਨਹੀਂ ਪਹੁੰਚ ਪਾਉਂਦਾ।

ਬ੍ਰਾਊਜ਼ਰ ਦਾ turn-based loop

ਬ੍ਰਾਊਜ਼ਰ ਵਿੱਚ, ਲੂਪ ਇੱਕ ਸਿੰਗਲ ਸਾਈਕਲ ਚਲਾਉਂਦਾ ਹੈ ਜੋ task execution, microtask draining, ਅਤੇ rendering ਨੂੰ ਆਪਸ ਵਿੱਚ ਜੋੜਦਾ ਹੈ:

  1. ਇੱਕ macrotask ਚਲਾਓ (ਜਿਵੇਂ ਕਿ ਇੱਕ click handler, ਇੱਕ setTimeout, ਆਦਿ)।
  2. ਸਾਰੇ microtasks ਨੂੰ ਖਤਮ ਕਰੋ (promises, queueMicrotask)।
  3. ਜੇਕਰ ਇੱਕ ਫਰੇਮ ਦੀ ਵਾਰੀ ਆ ਗਈ ਹੈ, ਤਾਂ 60 fps ਦੇ ਟੀਚੇ ਨੂੰ ਪ੍ਰਾਪਤ ਕਰਨ ਲਈ paint ਅਤੇ composite ਕਰੋ।
  4. ਇਸ ਨੂੰ ਦੁਹਰਾਓ।

ਦੋ APIs ਡਿਵੈਲਪਰਾਂ ਨੂੰ ਇਸ ਸਾਈਕਲ ਵਿੱਚ ਸਪੱਸ਼ਟ ਹੁੱਕ (hooks) ਪ੍ਰਦਾਨ ਕਰਦੇ ਹਨ:

  • requestAnimationFrame – ਬ੍ਰਾਊਜ਼ਰ ਦੁਆਰਾ paint ਕਰਨ ਤੋਂ ਠੀਕ ਪਹਿਲਾਂ ਕਾਲ ਕੀਤਾ ਜਾਂਦਾ ਹੈ। ਇਹ ਐਨੀਮੇਸ਼ਨ ਕੰਮ ਲਈ ਸਹੀ ਜਗ੍ਹਾ ਹੈ ਕਿਉਂਕਿ callback ਮੌਜੂਦਾ microtasks ਤੋਂ ਬਾਅਦ ਪਰ ਅਗਲੇ ਫਰੇਮ ਤੋਂ ਪਹਿਲਾਂ ਚੱਲਦਾ ਹੈ।
  • requestIdleCallback – ਉਦੋਂ ਚਲਾਇਆ ਜਾਂਦਾ ਹੈ ਜਦੋਂ ਬ੍ਰਾਊਜ਼ਰ ਕੋਲ ਕੋਈ ਉੱਚ-ਪ੍ਰਾਥਮਿਕਤਾ (high-priority) ਵਾਲਾ ਕੰਮ ਨਹੀਂ ਹੁੰਦਾ। ਇਹ ਐਨਾਲਿਟਿਕਸ ਜਾਂ ਡਾਟਾ ਪ੍ਰੀ-ਲੋਡਿੰਗ ਵਰਗੇ ਘੱਟ ਪ੍ਰਭਾਵ ਵਾਲੇ ਕੰਮਾਂ ਲਈ useful ਹੈ।

ਖ਼ਤਰਾ: microtask starvation

ਕਿਉਂਕਿ ਬ੍ਰਾਊਜ਼ਰ ਰੈਂਡਰ ਕਰਨ ਤੋਂ ਪਹਿਲਾਂ microtask queue ਨੂੰ ਖਾਲੀ ਕਰ ਦਿੰਦਾ ਹੈ, ਇਸ ਲਈ promises ਦੀ ਇੱਕ ਲੰਬੀ ਚੇਨ UI ਨੂੰ ਕਦੇ ਵੀ paint ਕਰਨ ਤੋਂ ਰੋਕ ਸਕਦੀ ਹੈ। Call stack ਰੁਕਦਾ ਨਹੀਂ ਹੈ; ਪੇਜ ਬਸ ਰੈਂਡਰ ਸਟੈਪ ਤੱਕ ਕਦੇ ਪਹੁੰਚ ਹੀ ਨਹੀਂ ਪਾਉਂਦਾ, ਜੋ ਯੂਜ਼ਰ ਨੂੰ ਫ੍ਰੀਜ਼ ਹੋਣ ਵਾਂਗ ਮਹਿਸੂਸ ਹੁੰਦਾ ਹੈ।

Node ਦਾ libuv-driven loop

Node.js ਆਪਣੇ ਲੂਪ ਨੂੰ libuv ਨੂੰ ਸੌਂਪ ਦਿੰਦਾ ਹੈ, ਜੋ ਕਿ ਇੱਕ C ਲਾਇਬ੍ਰੇਰੀ ਹੈ ਜੋ ਕੰਮ ਨੂੰ ਵੱਖ-ਵੱਖ phases ਵਿੱਚ ਵੰਡਦੀ ਹੈ, ਜਿਨ੍ਹਾਂ ਵਿੱਚੋਂ ਹਰ ਇੱਕ ਦੀ ਆਪਣੀ queue ਹੁੰਦੀ ਹੈ:

  1. TimerssetTimeout ਅਤੇ setInterval ਤੋਂ callbacks।
  2. Pending callbacks – ਦੇਰੀ ਨਾਲ ਆਉਣ ਵਾਲੇ I/O callbacks ਜੋ OS ਪੱਧਰ 'ਤੇ ਪਹਿਲਾਂ ਹੀ ਪੂਰੇ ਹੋ ਚੁੱਕੇ ਹਨ।
  3. Poll – ਨਵੇਂ I/O events (ਫਾਈਲ ਰੀਡ, ਨੈੱਟਵਰਕ ਡਾਟਾ) ਪ੍ਰਾਪਤ ਕਰਦਾ ਹੈ।
  4. ChecksetImmediate callbacks ਚਲਾਉਂਦਾ ਹੈ।
  5. Close callbacks – ਜਦੋਂ ਕੋਈ socket ਜਾਂ handle ਬੰਦ ਹੁੰਦਾ ਹੈ ਤਾਂ ਚੱਲਦਾ ਹੈ।

ਦੋ ਰਚਨਾਵਾਂ (constructs) ਇਸ phase ਦੇ ਕ੍ਰਮ ਤੋਂ ਬਾਹਰ ਹੁੰਦੀਆਂ ਹਨ:

  • process.nextTick – ਮੌਜੂਦਾ operation ਖਤਮ ਹੋਣ ਤੋਂ ਤੁਰੰਤ ਬਾਅਦ, microtask queue ਤੋਂ ਪਹਿਲਾਂ ਚੱਲਦਾ ਹੈ।

ਖ਼ਤਰਾ: I/O starvation

ਜੇਕਰ ਕੋਈ ਫੰਕਸ਼ਨ ਬਿਨਾਂ ਰੁਕੇ ਵਾਰ-ਵਾਰ process.nextTick ਨੂੰ ਸ਼ੈਡਿਊਲ ਕਰਦਾ ਹੈ, ਤਾਂ Node ਕਦੇ ਵੀ “next-tick” ਸਟੈਪ ਤੋਂ ਅੱਗੇ ਨਹੀਂ ਵਧ ਪਾਉਂਦਾ। ਨੈੱਟਵਰਕ ਰਿਕਵੈਸਟਾਂ, ਫਾਈਲ ਰੀਡ, ਅਤੇ timers ਖਾਲੀ ਬੈਠੇ ਰਹਿੰਦੇ ਹਨ, ਜਿਸ ਨਾਲ ਸਰਵਰ-ਸਾਈਡ ਲੇਟੈਂਸੀ (latency) ਵਧ ਜਾਂਦੀ ਹੈ ਜਾਂ ਸਰਵਰ ਪੂਰੀ ਤਰ੍ਹਾਂ ਹੈਂਗ ਹੋ ਸਕਦਾ ਹੈ।

setImmediate ਬਨਾਮ setTimeout ਅਭਿਆਸ ਵਿੱਚ

ਦੋਵੇਂ ਅਗਲੇ iteration ਲਈ callbacks ਨੂੰ ਸ਼ੈਡਿਊਲ ਕਰਦੇ ਹਨ, ਪਰ ਉਹਨਾਂ ਦਾ ਸਾਪੇਖਿਕ ਕ੍ਰਮ ਇਸ ਗੱਲ 'ਤੇ ਨਿਰਭਰ ਕਰਦਾ ਹੈ ਕਿ ਉਹਨਾਂ ਨੂੰ ਕਿੱਥੇ ਕਾਲ ਕੀਤਾ ਗਿਆ ਹੈ:

  • Top-level code – ਇਸਦਾ ਕ੍ਰਮ ਯਕੀਨੀ ਨਹੀਂ ਹੈ; ਇਹ ਇਸ ਗੱਲ 'ਤੇ ਨਿਰਭਰ ਕਰਦਾ ਹੈ ਕਿ ਪ੍ਰੋਸੈਸ ਕਿੰਨੀ ਤੇਜ਼ੀ ਨਾਲ ਸ਼ੁਰੂ ਹੁੰਦਾ ਹੈ।
  • Inside an I/O callback – ਕ੍ਰਮ ਨਿਸ਼ਚਿਤ (deterministic) ਹੁੰਦਾ ਹੈ: setImmediate, setTimeout(fn, 0) ਤੋਂ ਪਹਿਲਾਂ ਚੱਲਦਾ ਹੈ। Poll phase ਖਤਮ ਹੋਣ ਤੋਂ ਬਾਅਦ, libuv Check phase (ਜਿੱਥੇ setImmediate ਹੁੰਦਾ ਹੈ) ਵੱਲ ਵਧਦਾ ਹੈ, ਇਸ ਤੋਂ ਪਹਿਲਾਂ ਕਿ ਉਹ ਜ਼ੀਰੋ-ਡਿਲੇਅ ਟਾਈਮਆਊਟ ਲਈ Timers phase ਵਿੱਚ ਦੁਬਾਰਾ ਦਾਖਲ ਹੋਵੇ।

ਇਹ ਬਾਰੀਕੀ ਉਦੋਂ ਮਹੱਤਵਪੂਰਨ ਹੁੰਦੀ ਹੈ ਜਦੋਂ ਤੁਸੀਂ ਸਹੀ ਕ੍ਰਮ (sequencing) 'ਤੇ ਨਿਰਭਰ ਕਰਦੇ ਹੋ, ਜਿਵੇਂ ਕਿ ਰੀਡ ਪੂਰਾ ਹੋਣ ਤੋਂ ਤੁਰੰਤ ਬਾਅਦ ਕਿਸੇ ਰਿਸੋਰਸ ਨੂੰ ਸਾਫ਼ ਕਰਨਾ।

ਮੁੱਖ ਅੰਤਰ ਇੱਕ ਨਜ਼ਰ ਵਿੱਚ

  • ਟੀਚਾ (Goal): ਬ੍ਰਾਊਜ਼ਰ ਵਿਜ਼ੂਅਲ ਅਪਡੇਟਸ ਨੂੰ ਪਹਿਲ ਦਿੰਦੇ ਹਨ; Node I/O ਤਿਆਰੀ ਨੂੰ ਪਹਿਲ ਦਿੰਦਾ ਹੈ।
  • Rendering hook: requestAnimationFrame (ਸਿਰਫ਼ ਬ੍ਰਾਊਜ਼ਰ)।
  • Phase-specific hook: setImmediate (ਸਿਰਫ਼ Node, Check phase ਵਿੱਚ ਚੱਲਦਾ ਹੈ)।
  • High-priority queue: process.nextTick (ਸਿਰਫ਼ Node, microtasks ਤੋਂ ਪਹਿਲਾਂ ਚੱਲਦਾ ਹੈ)।
  • Starvation ਦਾ ਖ਼ਤਰਾ: ਬ੍ਰਾਊਜ਼ਰਾਂ ਵਿੱਚ ਲੰਬੀਆਂ promise ਚੇਨਾਂ; Node ਵਿੱਚ ਅਨਲਿਮਿਟਡ process.nextTick

ਅੱਗੇ ਕੀ ਦੇਖਣਾ ਹੈ

ਜੇਕਰ ਤੁਸੀਂ ਅਜਿਹਾ codebase ਮੈਂਨਟੇਨ ਕਰਦੇ ਹੋ ਜੋ ਦੋਵਾਂ ਮਾਹੌਲਾਂ ਵਿੱਚ ਚੱਲਦਾ ਹੈ (ਜਿਵੇਂ ਕਿ isomorphic libraries), ਤਾਂ ਕਿਸੇ ਵੀ ਅਜਿਹੀ ਜਗ੍ਹਾ ਦੀ ਜਾਂਚ ਕਰੋ ਜਿੱਥੇ ਤੁਸੀਂ:

  • Event loop ਨੂੰ ਸਮਾਂ ਦਿੱਤੇ ਬਿਨਾਂ ਬਹੁਤ ਸਾਰੇ promises ਨੂੰ ਚੇਨ (chain) ਕਰਦੇ ਹੋ। ਰੈਂਡਰਰ ਨੂੰ ਮੌਕਾ ਦੇਣ ਲਈ await new Promise(r => setTimeout(r, 0)) ਇਨਸਰਟ ਕਰੋ ਜਾਂ ਬ੍ਰਾਊਜ਼ਰ ਵਿੱਚ requestIdleCallback ਦੀ ਵਰਤੋਂ ਕਰੋ।
  • ਅਜਿਹੇ ਕੰਮ ਲਈ process.nextTick ਦੀ ਵਰਤੋਂ ਕਰਦੇ ਹੋ ਜਿਸ ਨੂੰ ਦੇਰੀ ਨਾਲ ਵੀ ਕੀਤਾ ਜਾ ਸਕਦਾ ਹੈ। ਜਦੋਂ ਤੁਹਾਨੂੰ “next-tick” ਦੀ ਤੁਰੰਤ ਲੋੜ ਨਾ ਹੋਵੇ, ਤਾਂ setImmediate ਜਾਂ ਇੱਕ ਆਮ promise ਨੂੰ ਤਰਜੀਹ ਦਿਓ।
  • ਇਹ ਮੰਨ ਲੈਂਦੇ ਹੋ ਕਿ setTimeout(fn, 0) ਅਤੇ setImmediate ਇੱਕ ਦੂਜੇ ਦੀ ਜਗ੍ਹਾ ਵਰਤੇ ਜਾ ਸਕਦੇ ਹਨ। ਜੇਕਰ ਕ੍ਰਮ ਮਹੱਤਵਪੂਰਨ ਹੈ, ਤਾਂ I/O callbacks ਦੇ ਅੰਦਰ ਕ੍ਰਮ ਦੀ ਜਾਂਚ ਕਰੋ।

ਮੁੱਖ ਨੁਕਤੇ

event loop ਇੱਕ host-specific scheduler ਹੈ, ਨਾ ਕਿ ਇੱਕ universal JavaScript feature। Browsers rendering ਨੂੰ loop ਵਿੱਚ ਪਰੋ ਦਿੰਦੇ ਹਨ; Node I/O ਨੂੰ libuv phases ਵਿੱਚ ਵੱਖ ਕਰ ਦਿੰਦਾ ਹੈ। Priority mechanisms ਦੀ ਦੁਰਵਰਤੋਂ ਕਰਨਾ—ਜਿਵੇਂ browser ਵਿੱਚ microtasks, ਅਤੇ Node ਵਿੱਚ process.nextTick—ਸਿਸਟਮ ਦੇ ਉਸ ਹਿੱਸੇ ਨੂੰ ਪ੍ਰਭਾਵਿਤ ਕਰ ਸਕਦਾ ਹੈ ਜਿਸਦੀ ਸੇਵਾ ਲਈ ਹਰੇਕ environment ਬਣਾਇਆ ਗਿਆ ਹੈ। ਆਪਣੇ async patterns ਨੂੰ host ਦੇ loop model ਦੇ ਅਨੁਸਾਰ ਰੱਖੋ, ਅਤੇ ਤੁਸੀਂ frozen pages ਅਤੇ blocked servers ਦੋਵਾਂ ਤੋਂ ਬਚ ਸਕੋਗੇ।