Чому налаштування властивості CSS за допомогою Promise.then насправді не відбувається в тодішньому блоці?


11

Спробуйте запустити наступний фрагмент, а потім натисніть на поле.

const box = document.querySelector('.box')
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)'
    new Promise(resolve => {
      setTimeout(() => {
        box.style.transition = 'none'
        box.style.transform = ''
        resolve('Transition complete')
      }, 2000)
    }).then(() => {
      box.style.transition = ''
    })
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class = "box"></div>

Що я очікую, що трапиться:

  • Клацання відбувається
  • Поле починає перекладати горизонтально на 100 пікселів (ця дія займає дві секунди)
  • При натисканні Promiseтакож створюється новий . Усередині сказав Promise, A setTimeoutфункція встановлена на 2 секунди
  • Після завершення дії (минуло дві секунди), setTimeoutзапускає функцію зворотного дзвінка і встановлює transitionзначення "ні". Після цього setTimeoutтакож повертається transformдо свого початкового значення, таким чином, поле відображається у вихідному місці.
  • Поле з’являється в оригінальному місці, тут немає проблем з ефектом переходу
  • Після закінчення цих transitionповернень поверніть значення початкового значення

Однак, як видно, transitionзначення, здається, не noneпрацює під час запуску. Я знаю, що існують інші методи досягнення вищезазначеного, наприклад, використання ключових кадрів і transitionend, але чому це відбувається? Я явно встановлюю transitionназад його початкове значення лише після того, як setTimeoutзакінчується зворотний дзвінок, таким чином вирішуючи Обіцяння.

EDIT

Відповідно до запиту, ось графічний код, що відображає проблематичну поведінку: Проблема


У якому браузері ви це бачите? У Chrome я бачу, що це призначено.
Террі

@Terry Firefox 73.0 (64-розрядні) для Windows.
Річард

Чи можете ви долучити gif до свого питання, яке ілюструє проблему? Наскільки я міг сказати, це також візуалізація / поведінка, як очікувалося на Firefox.
Террі

Коли Обіцянка вирішиться, вихідний перехід відновлюється, але в цей момент вікно все одно перетворюється. Тому він переходить назад. Потрібно зачекати принаймні ще 1 кадр, перш ніж скинути перехід до початкового значення: jsfiddle.net/khrismuc/3mjwtack
Chris G

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

Відповіді:


4

Змінюється стиль партії циклу подій . Якщо ви зміните стиль елемента в одному рядку, браузер не відображатиме ці зміни відразу; він зачекає наступного кадру анімації. Ось чому, наприклад

elm.style.width = '10px';
elm.style.width = '100px';

не призводить до мерехтіння; браузер дбає лише про значення стилів, встановлені після того, як Javascript завершиться.

Візуалізація відбувається після завершення роботи Javascript, включаючи мікрозадачі . .thenЗ Promise відбувається в microtask (який буде працювати ефективно , як тільки всі інші Javascript закінчив, але перш ніж що - небудь інше - такі як рендеринг - мав шанс бігти).

Ви робите це налаштування transitionвластивості ''в мікрозадачі, перш ніж браузер почав відображати зміни, викликані style.transform = ''.

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

const box = document.querySelector('.box')
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)'
    setTimeout(() => {
      box.style.transition = 'none'
      box.style.transform = ''
      // resolve('Transition complete')
      requestAnimationFrame(() => {
        setTimeout(() => {
          box.style.transition = ''
        });
      });
    }, 2000)
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class="box"></div>


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

Це виглядає як хороший підсумок: javascript.info/event-loop
CertainPerformance

2
Це не зовсім так. Цикл подій нічого не говорить про зміни стилю. Більшість браузерів спробують дочекатися наступного кадру малювання, коли зможуть, але це вже про це. більше інформації ;-)
Каїдо

3

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

Ви можете звернутися до цієї відповіді, щоб зрозуміти, як CSSOM і DOM пов'язані для процесу "перемальовки".
В основному, браузери зазвичай чекають наступного кадру малювання, щоб перерахувати всі нові позиції поля і, таким чином, застосувати правила CSS до CSSOM.

Тож у вашому оброблювальному пристрої Promise, коли ви скидаєте transitionна "", показник transform: ""ще не був обчислений. Коли він буде розрахований, засіб буде transitionвже скинутий ""і CSSOM запустить перехід для оновлення перетворення.

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

Що робить використання Обіця зовсім непотрібним:

const box = document.querySelector('.box')
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)'
    setTimeout(() => {
      box.style.transition = 'none'
      box.style.transform = ''
      box.offsetWidth; // this triggers a reflow
      // even synchronously
      box.style.transition = ''
    }, 2000)
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class = "box"></div>


А для пояснення щодо мікрозадач, таких як Promise.resolve()або MutationEvents , або queueMicrotask()вам потрібно зрозуміти, що вони будуть запущені, як тільки буде виконано поточне завдання, 7-й крок моделі обробки циклу подій перед кроками візуалізації .
Тож у вашому випадку це дуже схоже на те, якби це було запущено синхронно.

До речі, остерігайтеся мікрозадач може бути таким же блокуючим, як цикл у той час:

// this will freeze your page just like a while(1) loop
const makeProm = ()=> Promise.resolve().then( makeProm );

Так, але відповідь на transitionendподію уникне необхідності жорсткого кодування тайм-ауту, щоб відповідати закінченню переходу. transitionToPromise.js буде promisify перехід дозволяє писати transitionToPromise(box, 'transform', 'translateX(100px)').then(() => /* four lines as per answer above */).
Roamer-1888

Дві переваги: ​​(1) тривалість переходу потім може бути змінена в CSS, не змінюючи javascript; (2) перехідToPromise.js є багаторазовим. До речі, я спробував це, і він працює добре.
Roamer-1888

@ Roamer-1888 Так, ви могли б, хоч я особисто додав би чек, (evt)=>if(evt.propertyName === "transform"){ ...щоб уникнути помилкових позитивних результатів, і мені дуже не подобається, що вони обіцяють подібні події, тому що ви ніколи не знаєте, чи коли-небудь він запуститься (подумайте про випадок, наприклад, someAncestor.hide() коли проходить перехід, Ваша Обіцянка ніколи не спрацьовує, і ваш перехід буде заважати. Отже, це дійсно до ОП, щоб визначити, що краще для них, але особисто та досвід, я зараз віддаю перевагу тайм-
аутам,

1
Плюси і мінуси, я думаю. У будь-якому випадку, з або без обіцянок, ця відповідь набагато чіткіше, ніж одна, що включає два setTimeouts та requestAnimationFrame.
Roamer-1888

У мене є одне питання, хоча. Ви сказали, що requestAnimationFrame()це буде запущено перед наступним перефарбуванням браузера. Ви також згадали, що браузери, як правило, чекають наступного кадру малювання, щоб перерахувати всі нові позиції поля . Тим не менш, вам все одно потрібно було вручну запустити примусовий поповнення (відповідь на першому посиланні). Я, отже, роблю висновок, що навіть коли requestAnimationFrame()трапляється безпосередньо перед перефарбуванням, браузер все ще не розраховував новітній обчислюваний стиль; таким чином, потрібно вручну змусити перерахунок стилів. Правильно?
Річард

0

Я вдячний, що це не зовсім те, що ви шукаєте, але - з цікавості та заради повноти - я хотів дізнатись, чи зможу я написати підхід, що стосується лише CSS до цього ефекту.

Майже ... але виявляється, мені все ж довелося включати єдиний рядок javascript.

Приклад роботи:

document.querySelector('.box').addEventListener('animationend', (e) => e.target.blur());
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  cursor: pointer;
}

.box:focus {
 animation: boxAnimation 2s ease;
}

@keyframes boxAnimation {
  100% {transform: translateX(100px);}
}
<div class="box" tabindex="0"></div>


-1

Я вважаю, що ваша проблема полягає лише в тому, що .thenви налаштовуєте transitionна '', коли ви повинні налаштувати її так, noneяк це робили в зворотному звороті таймера.

const box = document.querySelector('.box');
box.addEventListener('click', e => {
  if (!box.style.transform) {
    box.style.transform = 'translateX(100px)';
    new Promise(resolve => {
      setTimeout(() => {
        box.style.transition = 'none';
        box.style.transform = '';
        resolve('Transition complete');
      }, 2000)
    }).then(() => {
     box.style.transition = 'none'; // <<----
    })
  }
})
.box {
  width: 100px;
  height: 100px;
  border-radius: 5px;
  background-color: #121212;
  transition: all 2s ease;
}
<div class = "box"></div>


1
Ні, ОП налаштовує його знову ''застосовувати правило класу (яке було відмінено правилом елемента). Ваш код просто встановлює його в 'none'два рази, що запобігає переходу поля назад, але не відновлює його початковий перехід (клас)
Кріс Г
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.