← Назад к вопросам

Как микро-задачу превратить в макро-задачу?

2.0 Middle🔥 161 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как превратить микро-задачу в макро-задачу в JavaScript

Этот вопрос касается Event Loop в JavaScript и разницы между микротасками (microtasks) и макротасками (macrotasks).

Основные понятия

Event Loop - это механизм, который определяет порядок выполнения кода в JavaScript:

┌─────────────────────────────────────────┐
│         Event Loop итерация             │
├─────────────────────────────────────────┤
│                                         │
│  1. Выполнить одну макротаску          │
│  2. Выполнить ВСЕ микротаски           │
│  3. Перерендер (если нужен)            │
│  4. Перейти к шагу 1                   │
│                                         │
└─────────────────────────────────────────┘

Микротаски vs Макротаски

Микротаски (Microtasks) - HIGH PRIORITY:

  • Promise.then/catch/finally
  • async/await (в реальности это Promise)
  • queueMicrotask()
  • MutationObserver
  • процесс.nextTick() (Node.js)

Макротаски (Macrotasks) - LOW PRIORITY:

  • setTimeout
  • setInterval
  • setImmediate (Node.js)
  • requestAnimationFrame
  • fetch (технически, результат макротаска)
  • DOM события
  • UI рендеринг
  • script тег

Пример: Порядок выполнения

console.log('1. Скрипт начался');

setTimeout(() => {
  console.log('2. setTimeout');
}, 0);

Promise.resolve()
  .then(() => {
    console.log('3. Promise');
  });

console.log('4. Скрипт закончился');

// Вывод:
// 1. Скрипт начался
// 4. Скрипт закончился
// 3. Promise          <- Микротаска! Выполняется раньше setTimeout
// 2. setTimeout       <- Макротаска! Выполняется после всех микротасок

Способы превратить микро-задачу в макро-задачу

1. Использование setTimeout (самый простой способ)

// Микротаска - Promise
Promise.resolve()
  .then(() => {
    console.log('Микротаска');
  });

// Превращение в макротаску - используем setTimeout
setTimeout(() => {
  Promise.resolve()
    .then(() => {
      console.log('Макротаска (обернута в setTimeout)');
    });
}, 0);

console.log('Синхронный код');

// Вывод:
// Синхронный код
// Микротаска
// Макротаска (обернута в setTimeout)

2. Практический пример в React

// Проблема: микротаска выполняется ДО рендера
const Component = () => {
  const [state, setState] = useState(false);
  
  useEffect(() => {
    // Это микротаска - выполнится ДО следующего рендера
    Promise.resolve()
      .then(() => {
        // Выполнится ДО рендера другого компонента
        console.log('Микротаска');
      });
  }, []);
  
  return <div>Component</div>;
};

// Решение: обернуть в setTimeout (макротаска)
const Component = () => {
  const [state, setState] = useState(false);
  
  useEffect(() => {
    // Это макротаска - выполнится ПОСЛЕ рендера
    setTimeout(() => {
      console.log('Макротаска - выполнится позже');
    }, 0);
  }, []);
  
  return <div>Component</div>;
};

3. Демонстрация с несколькими операциями

console.log('Start');

// Микротаска 1
Promise.resolve()
  .then(() => console.log('Microtask 1'));

// Макротаска 1
setTimeout(() => {
  console.log('Macrotask 1');
  
  // Микротаска внутри макротаски
  Promise.resolve()
    .then(() => console.log('Microtask inside Macrotask 1'));
}, 0);

// Микротаска 2
queueMicrotask(() => {
  console.log('Microtask 2');
});

// Макротаска 2
setTimeout(() => {
  console.log('Macrotask 2');
}, 0);

console.log('End');

// Вывод:
// Start
// End
// Microtask 1
// Microtask 2
// Macrotask 1
// Microtask inside Macrotask 1
// Macrotask 2

4. Превращение Promise в макротаску

// Исходная микротаска
const microTask = () => {
  return Promise.resolve('микротаска');
};

// Обертка для превращения в макротаску
const microToMacroTask = (fn) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      Promise.resolve()
        .then(() => fn())
        .then(resolve);
    }, 0);
  });
};

// Использование
microTask().then(result => {
  console.log('Микротаска:', result);
});

microToMacroTask(microTask).then(result => {
  console.log('Макротаска:', result);
});

// Вывод:
// Микротаска: микротаска
// Макротаска: микротаска

5. Вспомогательная функция для явного управления

// Явно выполнить как макротаску
const executeAsMacrotask = (fn) => {
  return new Promise((resolve) => {
    setTimeout(() => resolve(fn()), 0);
  });
};

// Явно выполнить как микротаску
const executeAsMicrotask = (fn) => {
  return Promise.resolve().then(fn);
};

// Использование
executeAsMicrotask(() => {
  console.log('Явная микротаска');
});

executeAsMacrotask(() => {
  console.log('Явная макротаска');
});

console.log('Синхронный код');

// Вывод:
// Синхронный код
// Явная микротаска
// Явная макротаска

Real-world примеры

Проблема в Angular

// Микротаска - может быть проблема с change detection
this.dataService.getData()
  .then(data => {
    this.data = data;  // Изменение внутри Promise
  });

// Решение - явно запустить как макротаску
this.dataService.getData()
  .then(data => {
    setTimeout(() => {
      this.data = data;  // Теперь будет новая макротаска
    });
  });

Проблема с DOM манипуляцией

// Микротаска
const items = document.querySelectorAll('.item');

Promise.resolve()
  .then(() => {
    items.forEach(item => {
      item.classList.add('active');  // Может не сработать правильно
    });
  });

// Решение
const items = document.querySelectorAll('.item');

setTimeout(() => {
  items.forEach(item => {
    item.classList.add('active');  // Работает правильно
  });
}, 0);

React хук с правильным тайингом

const useDelayedEffect = (callback, deps) => {
  useEffect(() => {
    // Микротаска - выполнится немного раньше
    // Promise.resolve().then(callback);
    
    // Макротаска - выполнится после рендера
    const timeoutId = setTimeout(callback, 0);
    
    return () => clearTimeout(timeoutId);
  }, deps);
};

// Использование
const Component = () => {
  useDelayedEffect(() => {
    console.log('Выполнится после полного рендера');
  }, []);
  
  return <div>Component</div>;
};

Визуализация очередности

Выполнение кода:

│
├─ Синхронный код
│  └─ console.log('Start')
│
├─ Event Loop шаг 1: выполнить макротаску
│  └─ setTimeout callback #1
│
├─ Event Loop шаг 2: выполнить ВСЕ микротаски
│  ├─ Promise #1
│  ├─ Promise #2
│  └─ queueMicrotask #1
│
├─ Event Loop шаг 3: перерендер (если нужен)
│  └─ requestAnimationFrame
│
├─ Event Loop шаг 4: выполнить макротаску
│  └─ setTimeout callback #2
│
├─ Event Loop шаг 2: выполнить ВСЕ микротаски
│  └─ (микротаски из setTimeout #2)
│
└─ ... и так дальше

Таблица быстрого выбора

Нужно сделатьСпособ
Выполнить АСАПСинхронный код
Выполнить очень скороPromise, async/await
Выполнить после текущего циклаsetTimeout(..., 0)
Выполнить в следующем кадреrequestAnimationFrame
Выполнить через времяsetTimeout(..., ms)
Проверить мутации DOMMutationObserver (микротаска)

Практический совет

Если испытываешь проблемы с тайингом выполнения:

  1. Используй Promise если нужно ОЧЕНЬ скоро (микротаска)
  2. Используй setTimeout(..., 0) если нужно позже (макротаска)
  3. Используй requestAnimationFrame если нужно совместить с браузерным рендером

Заключение

Основной способ превратить микро-задачу в макро-задачу - это обернуть её в setTimeout(..., 0). Это переместит выполнение на следующую итерацию Event Loop, после того как будут выполнены все микротаски из текущей итерации.