Чому іноді корисно setTimeout (fn, 0)?


872

Нещодавно я зіткнувся з досить неприємною помилкою, де код <select>динамічно завантажувався через JavaScript. Цей динамічно завантажений <select>мав попередньо вибране значення. В IE6, ми вже мали код , щоб встановити вибраний <option>, тому що іноді <select>«s selectedIndexзначення буде синхронізований з обраним <option>» s indexатрибута, як показано нижче:

field.selectedIndex = element.index;

Однак цей код не працював. Незважаючи на те, що поле selectedIndexбуло встановлено правильно, неправильний індекс в кінцевому підсумку буде обраний. Однак якщо я затримав alert()заяву в потрібний час, буде обраний правильний варіант. Думаючи, що це може бути якась проблема часу, я спробував щось випадкове, що я бачив у коді раніше:

var wrapFn = (function() {
    var myField = field;
    var myElement = element;

    return function() {
        myField.selectedIndex = myElement.index;
    }
})();
setTimeout(wrapFn, 0);

І це спрацювало!

Я вирішив свою проблему, але мені неприємно, що я не знаю точно, чому це вирішує мою проблему. Хтось має офіційне пояснення? Яку проблему з браузером я уникаю, називаючи свою функцію "пізніше" за допомогою setTimeout()?


2
Більшість питань описують, чому це корисно. Якщо вам потрібно знати, чому це відбувається - прочитайте мою відповідь: stackoverflow.com/a/23747597/1090562
Сальвадор Далі

18
Філіп Робертс пояснює це найкращим чином тут у своїй розмові "Що за чорт у цьому циклі подій?" youtube.com/watch?v=8aGhZQkoFbQ
vasa

Якщо ви поспішаєте, це частина відео, що він починає точно вирішувати питання: youtu.be/8aGhZQkoFbQ?t=14m54s . Без поваги, все відео варто переглянути точно.
Вільям Гемпшир

4
setTimeout(fn)те саме, що setTimeout(fn, 0), btw.
vsync

Відповіді:


827

У питанні існувала умова перегонів між:

  1. Спроба браузера ініціалізувати випадаючий список, готовий до оновлення обраного індексу та
  2. Ваш код для встановлення вибраного індексу

Ваш код послідовно вигравав цю гонку та намагався встановити вибір, що випадає, до того, як браузер буде готовий, тобто помилка з’явиться.

Ця гонка існувала тому, що у JavaScript є єдиний потік виконання, який ділиться з візуалізацією сторінок. Фактично, запуск JavaScript блокує оновлення DOM.

Ваше вирішення було:

setTimeout(callback, 0)

Викликаючи setTimeoutзворотний виклик, і нуль, як другий аргумент, планує, щоб зворотний дзвінок запускався асинхронно , після найменшої затримки - що складе близько 10 мс, коли вкладка має фокус, а потік виконання JavaScript не зайнятий.

Тому рішенням ОП було затримка встановлення вибраного індексу приблизно на 10 мс. Це дало браузеру можливість ініціалізувати DOM, виправляючи помилку.

Кожна версія Internet Explorer демонструвала химерну поведінку, і таке рішення було часом необхідним. Крім того, це могла бути справжня помилка в кодовій базі ОП.


Дивіться, як Філіп Робертс розмовляв: "Що за крок, це петля подій? для більш ретельного пояснення.


276
"Рішення полягає в" призупиненні "виконання JavaScript, щоб дозволити потоки візуалізації назбирати." Не зовсім правда, те, що робить setTimeout, - це додавати нову подію до черги подій браузера, і механізм візуалізації вже в цій черзі (не зовсім правдивий, але досить близький), щоб він виконувався перед подією setTimeout.
Девід Малдер

48
Так, це набагато більш детальна і набагато правильніша відповідь. Але моє "достатньо правильно", щоб люди зрозуміли, чому трюк працює.
статик

2
@DavidMulder, чи означає це браузер розбору css та надання в інший потік від потоку виконання JavaScript?
ясон

8
Ні, вони в принципі розбираються в одній нитці, інакше декілька рядків маніпуляції з DOM весь час породжуватимуть поповнення, що вкрай погано вплине на швидкість виконання.
Девід Малдер

30
Це відео - найкраще пояснення того, чому ми встановили TimeTime
davibq

665

Передмова:

Деякі з інших відповідей є правильними, але насправді не ілюструють, у чому полягає вирішення проблеми, тому я створив цю відповідь, щоб представити цю детальну ілюстрацію.

Як такий, я публікую детальну інформацію про те, що робить браузер і як використовує setTimeout()довідку . Це виглядає довго, але насправді дуже просте і зрозуміле - я просто зробив це дуже докладно.

ОНОВЛЕННЯ: Я створив JSFiddle, щоб продемонструвати пояснення нижче: http://jsfiddle.net/C2YBE/31/ . Величезне спасибі @ThangChung за допомогу в його стартовому старті.

UPDATE2: На всякий випадок, якщо веб-сайт JSFiddle помер або видалить код, я додав код до цієї відповіді в самому кінці.


ДЕТАЛІ :

Уявіть веб-додаток із кнопкою "зроби щось" та ділом результатів.

onClickОброблювач для кнопки «зробити що - то» викликає функцію «LongCalc ()», який робить 2 речі:

  1. Робить дуже довгий розрахунок (скажімо, займає 3 хв)

  2. Друкує результати обчислення у розділ результатів.

Тепер ваші користувачі починають тестувати це, натискають кнопку "зробити щось", і сторінка сидить там, здається, нічого не роблять протягом 3 хвилин, вони стають неспокійними, знову натискають на кнопку, чекають 1 хв, нічого не відбувається, знову натискають кнопку ...

Проблема очевидна - ви хочете DIV "Статус", який показує, що відбувається. Подивимося, як це працює.


Таким чином, ви додаєте DIV "Статус" (спочатку порожній) та модифікуєте onclickобробник (функцію LongCalc()), щоб зробити 4 речі:

  1. Заселіть статус "Обчислення ... може зайняти ~ 3 хв" у статус DIV

  2. Робить дуже довгий розрахунок (скажімо, займає 3 хв)

  3. Друкує результати обчислення у розділ результатів.

  4. Зарахуйте статус "Розрахунок зроблено" в статус DIV

І ви із задоволенням надаєте програму користувачам для повторної перевірки.

Вони повертаються до вас, виглядаючи дуже розлюченими. І поясніть, що, натиснувши кнопку, Статус DIV ніколи не оновлювався статусом "Розрахунок ..." !!!


Ви чухаєте голову, запитуєте про StackOverflow (або читайте документи або google) і усвідомлюєте проблему:

Веб-переглядач розміщує всі свої завдання "TODO" (як завдання UI, так і команди JavaScript), що виникають в результаті подій, в одну чергу . І, на жаль, повторне малювання DIV "Статус" новим значенням "Обчислення ..." - це окреме TODO, яке переходить до кінця черги!

Ось розбивка подій під час тесту користувача, вміст черги після кожної події:

  • Чергу: [Empty]
  • Подія: натисніть кнопку. Черга за подією:[Execute OnClick handler(lines 1-4)]
  • Подія: виконайте перший рядок в обробці OnClick (наприклад, змініть значення статусу DIV). Черга після події: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]. Зауважте, що хоча зміни DOM відбуваються миттєво, для повторного малювання відповідного елемента DOM вам потрібна нова подія, ініційована зміною DOM, яка пішла в кінці черги .
  • ПРОБЛЕМА !!! ПРОБЛЕМА !!! Деталі пояснюються нижче.
  • Подія: виконати другий рядок у обробці (обчислення). Черга після: [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value].
  • Подія: Виконати 3-й рядок у обробці (заповнити результат DIV). Черга після: [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result].
  • Подія: Виконати 4-й рядок у обробці (заповнити статус DIV за допомогою "DONE"). Черга: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value].
  • Подія: виконати мається на увазі returnз onclickпідрозділу обробника. Ми знімаємо «Виконати обробник OnClick» з черги і починаємо виконувати наступний елемент у черзі.
  • ПРИМІТКА. Оскільки ми вже закінчили розрахунок, для користувача вже минуло 3 хвилини. Подія повторного розіграшу ще не відбулася !!!
  • Подія: перемалюйте статус DIV зі значенням "Обчислення". Ми робимо повторний малюнок і знімаємо це з черги.
  • Подія: повторно намалюйте результат DIV зі значенням результату. Ми робимо повторний малюнок і знімаємо це з черги.
  • Подія: перемалюйте статус DIV зі значенням "Готово". Ми робимо повторний малюнок і знімаємо це з черги. Глядачі з різкими очима можуть навіть помітити "Статус DIV зі значенням" Обчислення ", що блимає на частку мікросекунди - ПІСЛЯ ЗАКОНЧЕННЯ РОЗРАХУНКУ

Отже, основна проблема полягає в тому, що подія повторного розіграшу для "Статус" DIV розміщується на черзі в кінці, ПІСЛЯ події "Execute line 2", яка займає 3 хвилини, тому фактичне повторне розіграш не відбувається до ПІСЛЯ робиться розрахунок.


На допомогу приходить setTimeout(). Як це допомагає? Тому що, викликаючи довгий виконуючий код через setTimeout, ви фактично створюєте 2 події: setTimeoutсаме виконання та (через 0 тайм-аут) окремий запис черги для коду, який виконується.

Отже, щоб виправити свою проблему, ви модифікуєте onClickобробник, щоб він був ДВОМИ твердженнями (у новій функції або просто блоком всередині onClick):

  1. Заселіть статус "Обчислення ... може зайняти ~ 3 хв" у статус DIV

  2. Виконати setTimeout()з 0 тайм-аутом та викликом LongCalc()функціонування .

    LongCalc()функція майже така ж, як і минулого разу, але очевидно, що не має оновлення статусу "Обчислення ..." як перший крок; і натомість розпочинає обчислення відразу.

Отже, як зараз виглядає послідовність подій та черга?

  • Чергу: [Empty]
  • Подія: натисніть кнопку. Черга за подією:[Execute OnClick handler(status update, setTimeout() call)]
  • Подія: виконайте перший рядок в обробці OnClick (наприклад, змініть значення статусу DIV). Черга після події: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Подія: виконати другий рядок у обробці (виклик setTimeout). Черга після: [re-draw Status DIV with "Calculating" value]. У черзі немає нічого нового в ньому ще 0 секунд.
  • Подія: сигнал тривоги закінчується через 0 секунд. Черга після: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Подія: перемалюйте статус DIV зі значенням "Обчислення" . Черга після: [execute LongCalc (lines 1-3)]. Зауважте, що ця повторна розіграш може насправді трапитися перед тим, як тривогу не згасне, яка працює так само добре.
  • ...

Ура! Статус DIV щойно оновився до "Розрахунок ..." до того, як розпочався розрахунок !!!



Нижче наведено зразок коду з JSFiddle, що ілюструє ці приклади: http://jsfiddle.net/C2YBE/31/ :

HTML-код:

<table border=1>
    <tr><td><button id='do'>Do long calc - bad status!</button></td>
        <td><div id='status'>Not Calculating yet.</div></td>
    </tr>
    <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
        <td><div id='status_ok'>Not Calculating yet.</div></td>
    </tr>
</table>

Код JavaScript: (Виконується onDomReadyта може потребувати jQuery 1.9)

function long_running(status_div) {

    var result = 0;
    // Use 1000/700/300 limits in Chrome, 
    //    300/100/100 in IE8, 
    //    1000/500/200 in FireFox
    // I have no idea why identical runtimes fail on diff browsers.
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 300; k++) {
                result = result + i + j + k;
            }
        }
    }
    $(status_div).text('calculation done');
}

// Assign events to buttons
$('#do').on('click', function () {
    $('#status').text('calculating....');
    long_running('#status');
});

$('#do_ok').on('click', function () {
    $('#status_ok').text('calculating....');
    // This works on IE8. Works in Chrome
    // Does NOT work in FireFox 25 with timeout =0 or =1
    // DOES work in FF if you change timeout from 0 to 500
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
});

12
чудова відповідь DVK! Ось історія,
kumikoda

4
Дійсно класна відповідь, DVK. Щоб полегшити уявлення, я поставив цей код на jsfiddle jsfiddle.net/thangchung/LVAaV
thangchung

4
@ThangChung - я спробував зробити кращу версію (2 кнопки, по одній для кожного випадку) в JSFiddle. Він працює як демонстрація на Chrome і IE, але не на FF чомусь - див. Jsfiddle.net/C2YBE/31 . Я запитав, чому FF не працює тут: stackoverflow.com/questions/20747591/…
DVK

1
@DVK "Браузер розміщує всі свої завдання" TODO "(як завдання UI, так і команди JavaScript), що виникають в результаті подій, в одну чергу". Сер, чи можете ви надати джерело для цього? Браузери Imho arent повинні мати різні інтерфейси користувальницького інтерфейсу (рендеринг) та теми JS .... без образи, не призначені .... просто хочу дізнатися ..
bhavya_w

1
@bhavya_w Ні, все відбувається на одній нитці. Це є причиною, що довгий розрахунок js може заблокувати користувальницький інтерфейс
Seba Kerckhof

88

Подивіться статтю Джона Ресіга про те, як працюють таймери JavaScript . Коли ви встановите тайм-аут, він фактично ставить в чергу асинхронний код, поки двигун не виконає поточний стек викликів.


23

setTimeout() купує вам деякий час до завантаження елементів DOM, навіть якщо встановлено значення 0.

Перевірте це: setTimeout


23

Більшість браузерів мають процес, який називається основний потік, який відповідає за виконання деяких завдань JavaScript, оновлення інтерфейсу, наприклад: малювання, перемальовування або перезавантаження тощо

Деякі завдання виконання JavaScript та оновлення інтерфейсу користуються чергою до черги повідомлень веб-переглядача, після чого надсилаються до основного потоку браузера для виконання.

Коли оновлення інтерфейсу створюються, поки основний потік зайнятий, завдання додаються в чергу повідомлень.

setTimeout(fn, 0);додайте це fnдо кінця черги, яку потрібно виконати. Він планує завдання, яке потрібно додати до черги повідомлень через певний проміжок часу.


"Кожне виконання завдань JavaScript та оновлення користувальницького інтерфейсу додається до системи черг подій браузера, потім ці завдання надсилаються до основної потоки інтерфейсу браузера, яку потрібно виконати." .... джерело, будь ласка?
bhavya_w

4
Високопродуктивний JavaScript (Ніколас Закас, Стоян Стефанов, Росс Гармс, Жульєн Лекомте та Метт Суїні)
Арлі

Зниження цього add this fn to the end of the queue. Найважливіше - куди саме setTimeoutдодається ця функція, кінець цього циклу циклу або сам початок наступного циклу циклу.
Зелений

19

Тут є суперечливі відповідні відповіді, і без доказів неможливо дізнатися, кому вірити. Ось доказ того, що @DVK має рацію, а @SalvadorDali невірно. Останній стверджує:

"І ось чому. Неможливо встановити timeTimeout із затримкою в часі 0 мілісекунд. Мінімальне значення визначається браузером, і це не 0 мілісекунд. Історично браузери встановлюють цей мінімум 10 мілісекунд, але специфікації HTML5 і сучасні браузери встановлюють його в 4 мілісекунди. "

Мінімальний час очікування 4 мс не має значення для того, що відбувається. Дійсно, що setTimeout висуває функцію зворотного виклику до кінця черги виконання. Якщо після setTimeout (зворотний дзвінок, 0) у вас є код блокування, який триває кілька секунд, зворотний виклик не буде виконуватися протягом декількох секунд, поки код блокування не закінчиться. Спробуйте цей код:

function testSettimeout0 () {
    var startTime = new Date().getTime()
    console.log('setting timeout 0 callback at ' +sinceStart())
    setTimeout(function(){
        console.log('in timeout callback at ' +sinceStart())
    }, 0)
    console.log('starting blocking loop at ' +sinceStart())
    while (sinceStart() < 3000) {
        continue
    }
    console.log('blocking loop ended at ' +sinceStart())
    return // functions below
    function sinceStart () {
        return new Date().getTime() - startTime
    } // sinceStart
} // testSettimeout0

Вихід:

setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033

ваша відповідь нічого не доводить. Це просто показує, що на вашій машині в конкретній ситуації комп'ютер кидає вам деякі цифри. Щоб довести щось пов’язане, вам потрібно трохи більше кількох рядків коду та декількох чисел.
Сальвадор Далі

6
@SalvadorDali, я вважаю, що моє доказ є досить зрозумілим, щоб зрозуміти більшість людей. Я думаю, ти відчуваєш захист і не доклав зусиль, щоб зрозуміти це. Я буду радий спробувати уточнити це, але я не знаю, що ви не можете зрозуміти. Спробуйте запустити код на власній машині, якщо ви підозрюєте мої результати.
Володимир Корнеа

Я спробую це зрозуміти востаннє. Моя відповідь - це уточнення деяких цікавих властивостей у таймауті, інтервалі та добре підтримується достовірними та впізнаваними джерелами. Ви твердо заявляли, що це неправильно, і вказали на свій фрагмент коду, який ви чомусь назвали доказом. У твоєму коді немає нічого поганого (я не проти). Я просто кажу, що моя відповідь не є помилковою. Це уточнює деякі моменти. Я збираюся припинити цю війну, бо не можу бачити сенсу.
Сальвадор Далі

15

Одна з причин цього - відкласти виконання коду до окремої наступної циклу подій. Відповідаючи на якусь подію браузера (наприклад, натискання миші), іноді доводиться виконувати операції лише після обробки поточної події. setTimeout()Об'єкт є найпростішим способом зробити це.

редагуйте зараз, що це 2015 рік. Я повинен зазначити, що є також requestAnimationFrame(), що не зовсім те саме, але достатньо близько до setTimeout(fn, 0)того, що варто згадати.


Це саме одне з місць, де я бачив, як його використовують. =)
Зайбот

FYI: ця відповідь була злитий тут з stackoverflow.com/questions/4574940 / ...
Shog9

2
полягає у відкладенні виконання коду до окремої, наступної циклу подій : як ви з'ясувати подальший цикл подій ? Як ви з'ясуєте, що таке поточний цикл подій? Звідки ви знаєте, у який вий циклі подій ви зараз?
Зелений

@ Зелений добре, ви ні, насправді; насправді немає прямої видимості того, що саме виконує JavaScript.
Pointy

requestAnimationFrame вирішив проблему, з якою у мене з IE та Firefox іноді не оновлювався інтерфейс користувача
David

9

Це старі запитання зі старими відповідями. Я хотів додати новий погляд на цю проблему і відповісти, чому це відбувається, а не чому це корисно.

Отже, у вас є дві функції:

var f1 = function () {    
   setTimeout(function(){
      console.log("f1", "First function call...");
   }, 0);
};

var f2 = function () {
    console.log("f2", "Second call...");
};

а потім зателефонуйте їм у наступному порядку, f1(); f2();щоб побачити, що другий виконаний перший.

І ось чому: не можна мати setTimeoutз затримкою часу 0 мілісекунд. Значення Мінімальна визначається браузером і не 0 мілісекунд. Історичні веб-переглядачі встановлювали цей мінімум на 10 мілісекунд, але специфікації HTML5 та сучасних браузерів встановлюють його в 4 мілісекунди.

Якщо рівень вкладеності більший за 5, а час очікування менше 4, то збільште час очікування до 4.

Також від мозіли:

Щоб реалізувати тайм-аут 0 мс у сучасному браузері, ви можете скористатися window.postMessage (), як описано тут .

Інформація про PS береться після прочитання наступної статті .


1
@ user2407309 Ви жартуєте? ви маєте на увазі, що специфікація HTML5 неправильна, і ви правильні? Прочитайте джерела, перш ніж ви звернетесь з призовом і зробіть вагомі претензії. Моя відповідь заснована на специфікації HTML та історичному записі. Замість того, щоб знову і знову робити відповіді, які пояснюють абсолютно однакові речі, я додав щось нове, те, що не було показано в попередніх відповідях. Я не кажу, що це єдина причина, я просто показую щось нове.
Сальвадор Далі

1
Це неправильно: "І ось чому: неможливо встановити TimeTimeout із затримкою в часі 0 мілісекунд." Це не тому. Затримка 4 мс не має значення, чому setTimeout(fn,0)корисна.
Володимир Корнеа

@ user2407309 його можна легко змінити, щоб "додати до причин, заявлених іншими, це неможливо ....". Тож смішно зголоситися саме через це, особливо якщо ти сам не відповідаєш нічого нового. Досить буде лише невеликої редакції.
Сальвадор Далі

10
Сальвадор Далі: Якщо ви ігноруєте тут емоційні аспекти війни з мікро полум’ям, вам, мабуть, доведеться визнати, що @VladimirKornea має рацію. Це правда, що браузери відображають затримку 0 мс до 4 мс, але навіть якби їх не було, результати все одно були б однаковими. Тут рушійним механізмом є те, що код висувається на чергу, а не на стек викликів. Погляньте на цю чудову презентацію JSConf, це може допомогти з’ясувати проблему: youtube.com/watch?v=8aGhZQkoFbQ
hashchange

1
Мене бентежить питання, чому ви вважаєте, що ваша цитата на кваліфікований мінімум 4 мс - це загальний мінімум 4 мс. Як показує ваша цитата зі специфікації HTML5, мінімум становить 4 мс лише тоді, коли ви вклали дзвінки на setTimeout/ setIntervalбільше п'яти рівнів; якщо ви цього не зробили, мінімум - 0 мс (що не вистачає на машинах часу). Документи Mozilla розширюють те, щоб охопити повторювані, а не просто вкладені випадки (тому setIntervalінтервал 0 буде негайно перенесено на кілька разів, а потім затримати довше після цього), але просте використання setTimeoutз мінімальним вкладом дозволяється негайно в черзі.
ShadowRanger

8

Оскільки він проходить тривалість 0, я вважаю, що це потрібно для того, щоб видалити код, переданий до, setTimeoutз потоку виконання. Отже, якщо це функція, яка може зайняти деякий час, вона не завадить виконувати наступний код.


FYI: ця відповідь була злитий тут з stackoverflow.com/questions/4574940 / ...
Shog9

7

Обидва ці два найвищі відповіді - неправильні. Ознайомтеся з описом MDN на моделі одночасності та циклом подій , і вам повинно стати зрозумілим, що відбувається (що ресурс MDN - це справжня дорогоцінний камінь). А просто використання setTimeout може додавати до коду несподівані проблеми на додаток до «вирішення» цієї маленької проблеми.

Тут насправді відбувається не те, що "браузер може бути не зовсім готовим, оскільки одночасність", або щось на основі "кожного рядка - це подія, яка додається в задній частині черги".

Jsfiddle забезпечується ДВК дійсно ілюструє проблему, але його пояснення цьому не є правильним.

Що відбувається в його коді - це те, що він спочатку прикріплює обробник події до clickподії на #doкнопці.

Потім, коли ви фактично натискаєте кнопку, messageстворюється a із посиланням на функцію обробника подій, яка додається до message queue. Коли event loopце повідомлення досягає, воно створює frameна стеці з викликом функції до обробника подій клацання у jsfiddle.

І тут стає цікаво. Ми настільки звикли думати Javascript як асинхронний, що ми схильні нехтувати цим крихітним фактом: будь-який кадр повинен бути виконаний у повному обсязі, перш ніж наступний кадр може бути виконаний . Ніякої сукупності, люди.

Що це означає? Це означає, що кожного разу, коли функція викликається з черги повідомлень, вона блокує чергу, поки стек, який вона створює, не буде спустошений. Або, у більш загальних рисах, він блокується, поки функція не повернеться. І це блокує все , включаючи операції з рендерінгу DOM, прокрутку тощо. Якщо ви хочете підтвердження, просто спробуйте збільшити тривалість тривалої операції у скрипці (наприклад, запустіть зовнішню петлю ще 10 разів), і ви помітите, що під час її запуску ви не можете прокручувати сторінку. Якщо він працює досить довго, ваш браузер запитає вас, чи хочете ви вбити процес, оскільки це робить сторінку невідповідною. Кадр виконується, і цикл подій та черга повідомлень застрягають, поки він не закінчиться.

То чому цей побічний ефект тексту не оновлюється? Тому що в той час як ви вже змінили значення елемента в DOM - ви можете console.log()його значення відразу після його зміни і бачити , що він був змінений (який показує , чому пояснення ДВК неправильно) - браузер чекає стек виснажувати ( onфункція обробника для повернення), і, таким чином, повідомлення закінчується, щоб в кінцевому підсумку можна було обійтися виконанням повідомлення, яке було додано під час виконання, як реакція на нашу операцію мутації, і щоб відобразити цю мутацію в інтерфейсі користувача .

Це тому, що ми насправді чекаємо, коли код закінчить працювати. Ми не говорили, що "хтось це отримує, а потім викликає цю функцію з результатами, дякую, і тепер я зробив так, що повертаюсь до Imma, роби все, що зараз", як це зазвичай робимо з нашим подійним асинхронним Javascript. Ми вводимо функцію обробника подій клацання, ми оновлюємо елемент DOM, ми викликаємо іншу функцію, інша функція працює тривалий час, а потім повертається, ми оновлюємо той самий елемент DOM, а потім повертаємося з початкової функції, ефективно спорожняючи стек. І тоді браузер може перейти до наступного повідомлення в черзі, яке, можливо, може бути повідомленням, яке генерується нами, викликаючи якесь внутрішнє подія типу "DOM-мутація".

Користувацький інтерфейс браузера не може (або не бажає) оновити інтерфейс користувача, поки не завершиться поточний кадр, що виконується (функція повернулася). Особисто я вважаю, що це скоріше дизайн, ніж обмеження.

Чому setTimeoutріч працює тоді? Це робиться так, тому що він ефективно видаляє виклик тривалої функції з власного кадру, плануючи її виконання пізніше в windowконтексті, щоб він сам негайно повертався і дозволяв черзі повідомлень обробляти інші повідомлення. Ідея полягає в тому, що повідомлення "оновлення" інтерфейсу, яке було запущено нами в Javascript при зміні тексту в DOM, тепер випереджає повідомлення, що стоїть на черзі для тривалої функції, так що оновлення інтерфейсу відбувається, перш ніж ми заблокуємо довго.

Зауважте, що а) Функція, яка триває, все ще блокує все, коли вона працює, і б) вам не гарантується, що оновлення інтерфейсу дійсно випереджає її в черзі повідомлень. У моєму веб-переглядачі Chrome у червні 2018 року значення 0не "вирішує" проблему, яку демонструє скрипка - 10. Я насправді трохи задушений цим, тому що мені здається логічним, що повідомлення про оновлення інтерфейсу повинно бути в черзі перед ним, оскільки його тригер виконується перед плануванням тривалої функції, яку потрібно запустити "пізніше". Але, можливо, в двигуні V8 є якісь оптимізації, які можуть заважати, а може, я просто не розумію.

Гаразд, так у чому проблема використання setTimeoutта яке краще рішення для даного конкретного випадку?

По-перше, проблема з використанням setTimeoutбудь-якого обробника подій, як це, щоб спробувати полегшити іншу проблему, схильна до псування з іншим кодом. Ось приклад із моєї роботи:

Колега, неправильно зрозумівши цикл подій, спробував "потік" Javascript, використовуючи якийсь код візуалізації шаблону setTimeout 0для його візуалізації. Він більше не тут, щоб запитувати, але я можу припустити, що, можливо, він вставив таймери для вимірювання швидкості візуалізації (що було б зворотною безпосередністю функцій) і виявив, що використання цього підходу спричинить блискавично швидкі відповіді від цієї функції.

Перша проблема очевидна; ви не можете надати тему javascript, тому ви нічого не виграєте, додаючи притуплення. По-друге, тепер ви фактично від'єднали візуалізацію шаблону зі стека можливих слухачів подій, які, можливо, очікують, що цей шаблон буде виведений, хоча це, можливо, ще не було. Фактична поведінка цієї функції тепер була недетермінованою, як це було - несвідомо - будь-якою функцією, яка би виконувала її чи залежала від неї. Ви можете робити здогадки, але ви не можете правильно кодувати його поведінку.

"Виправлення" при написанні нового обробника подій, яке залежало від його логіки, також слід було використовувати setTimeout 0. Але це не виправлення, це важко зрозуміти, і не весело налагоджувати помилки, викликані таким кодом. Іноді проблем ніколи не виникає, в інших випадках він послідовно виходить з ладу, а потім знову, іноді він працює і порушується епізодично, залежно від поточної продуктивності платформи та всього іншого, що відбувається в той час. Тому особисто я б порадив не використовувати цей хак (він є хак, і ми всі повинні знати , що це таке), якщо ви дійсно не знаєте , що ви робите , і які його наслідки.

Але що можемо ми робити замість цього? Ну, як підказує посилання на статтю MDN, або розділіть роботу на кілька повідомлень (якщо ви можете), щоб інші повідомлення, які стоять у черзі, могли бути переплетені з вашою роботою та виконані під час її запуску, або використовувати веб-працівника, який може працювати у тандемі зі своєю сторінкою та повертайте результати, коли завершите її обчислення.

О, і якщо ви думаєте: "Ну хіба я не міг би просто поставити зворотній дзвінок у функцію, що триває, щоб зробити його асинхронним?", То ні. Зворотний виклик не робить його асинхронним, все одно доведеться запустити тривалий код, перш ніж явно викликати зворотний дзвінок.


3

Інша річ, яка полягає в тому, - це натиснути виклик функції на дно стека, запобігаючи переповненню стека, якщо ви рекурсивно викликаєте функцію. Це впливає на whileцикл, але дозволяє двигуну JavaScript запускати інші асинхронні таймери.


Зниження цього push the function invocation to the bottom of the stack. Про що stackти говориш, це незрозуміло. Найважливіше - куди саме setTimeoutдодається ця функція, кінець цього циклу циклу або сам початок наступного циклу циклу.
Зелений

1

Зателефонувавши на setTimeout, ви надаєте сторінці час, щоб реагувати на все, що робить користувач. Це особливо корисно для функцій, які виконуються під час завантаження сторінки.


1

Деякі інші випадки, коли setTimeout корисний:

Ви хочете розбити довгий цикл або обчислення на більш дрібні компоненти, щоб браузер не здавався "замороженим" або сказав "Сценарій на сторінці зайнятий".

Ви хочете відключити кнопку для надсилання форми при натисканні, але якщо ви відключите кнопку в обробці onClick, форма не надсилатиметься. setTimeout з нульовим часом робить трюк, дозволяючи закінчувати подію, форму починати надсилати, тоді вашу кнопку можна відключити.


2
Відключення було б краще виконати в ході події, що подається; це буде швидше, і вам гарантовано зателефонують до того, як форма буде подана технічно, оскільки ви можете зупинити подання.
Кріс

Дуже правильно. Я вважаю, що відключення onclick простіше для прототипування, оскільки ви можете просто ввести onclick = "this.disabled = true" у кнопці, тоді як вимкнення при надсиланні вимагає трохи більше роботи.
fabspro

1

Відповіді про петлі виконання та надання DOM до завершення якогось іншого коду є правильними. Нульові секундні тайм-аути в JavaScript допомагають зробити код псевдо-багатопоточним, навіть якщо це не так.

Хочу додати, що НАЙКРАЩЕ значення для крос-браузера / крос-платформи нульової секунди таймауту в JavaScript фактично становить приблизно 20 мілісекунд замість 0 (нуль), тому що багато мобільних браузерів не можуть зареєструвати тайм-аути менше 20 мілісекунд через обмеження годин. на чіпах AMD.

Крім того, тривалі процеси, що не передбачають маніпуляції з DOM, повинні надсилатися Веб-працівникам вже зараз, оскільки вони забезпечують справжнє багатопотокове виконання JavaScript.


2
Я трохи скептично ставлюсь до вашої відповіді, але висловив це, оскільки це змусило мене зробити додаткові дослідження щодо стандартів браузера. Під час дослідження стандартів я переходжу туди, куди завжди йду, MDN: developer.mozilla.org/en-US/docs/Web/API/window.setTimeout HTML5, специфікація говорить про 4ms. Це нічого не говорить про обмеження годин на мобільних мікросхемах. Нелегко гуглить джерелом інформації, щоб створити резервну копію ваших заяв. Виявив мову Dart від Google, видалив setTimeout взагалі на користь об’єкта Timer.
Іван Заброський

8
(...) тому що багато мобільних браузерів не можуть зареєструвати таймаути менше 20 мілісекунд через обмеження годин (...) Кожна платформа має обмеження в часі через свій тактовий час, і жодна платформа не здатна виконати наступну річ рівно за 0 мс після поточний. Timeout 0ms вимагає якнайшвидшого виконання функції , а обмеження часу на певній платформі жодним чином не змінює значення цього.
Петро Доброгост

1

setTimout on 0 також дуже корисний у шаблоні налаштування відкладеної обіцянки, яку потрібно повернути відразу:

myObject.prototype.myMethodDeferred = function() {
    var deferredObject = $.Deferred();
    var that = this;  // Because setTimeout won't work right with this
    setTimeout(function() { 
        return myMethodActualWork.call(that, deferredObject);
    }, 0);
    return deferredObject.promise();
}

1

Проблема полягала в тому, що ви намагалися виконати операцію Javascript на неіснуючому елементі. Елемент ще потрібно було завантажувати, і це setTimeout()дає більше часу для завантаження елемента такими способами:

  1. setTimeout()викликає асинхронність події, тому виконується після всього синхронного коду, надаючи вашому елементу більше часу для завантаження. Асинхронні зворотні виклики, такі як зворотний виклик setTimeout(), розміщуються в черзі подій і ставляться на стек циклом подій після того, як стек синхронного коду порожній.
  2. Значення 0 для мс як другого аргументу у функції setTimeout()часто трохи вище (4-10 мс залежно від браузера). Цей дещо більший час, необхідний для виконання setTimeout()зворотних викликів, викликається кількістю "тиків" (де галочка натискає зворотний виклик на стеці, якщо стек порожній) циклу подій. Через причини продуктивності та ресурсу акумулятора кількість кліщів у циклі подій обмежена на певну кількість менше 1000 разів на секунду.

-1

Javascript - це однопоточний додаток, що не дозволяє одночасно запускати функцію, тому для досягнення цього циклу подій використовуються. Точно те, що setTimeout (fn, 0) робить, що його підштовхується до квесту завдань, який виконується, коли стек викликів порожній. Я знаю, що це пояснення досить нудне, тому я рекомендую вам пройти це відео, це допоможе вам, як все працює під кришкою браузера. Перегляньте це відео: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.