Почему установка свойства CSS с помощью Promise.then на самом деле не происходит в блоке 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>

Что я ожидаю, что произойдет:

  • Клик происходит
  • Box начинает перевод по горизонтали на 100 пикселей (это действие занимает две секунды)
  • При нажатии новая Promiseтакже создается. Внутри сказал Promise, setTimeoutфункция установлена ​​на 2 секунды
  • После того, как действие завершено (прошло две секунды), setTimeoutзапускается функция обратного вызова и устанавливается transitionзначение none. После этого setTimeoutтакже возвращается transformк своему первоначальному значению, таким образом отображая поле, чтобы появиться в исходном местоположении.
  • Поле появляется в исходном местоположении без проблем с эффектом перехода здесь
  • После того, как все это закончится, установите transitionзначение окна обратно в исходное значение

Тем не менее, как можно видеть, transitionзначение, по-видимому, не noneпри запуске. Я знаю, что есть другие методы для достижения вышеизложенного, например, использование ключевого кадра и т.д. transitionend, но почему это происходит? Я явно установить transitionобратно в исходное значение только послеsetTimeout того, как заканчивает свой обратный вызов, таким образом разрешения Promise.

РЕДАКТИРОВАТЬ

В соответствии с запросом, вот gif кода, отображающего проблемное поведение: проблема

Ричард
источник
В каком браузере вы это видите? На 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 = ''.

Если вы сбрасываете переход к пустой строке после a requestAnimationFrame(который будет запущен непосредственно перед следующей перерисовкой), а затем после a 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>

CertainPerformance
источник
Это ответ, который я искал. Спасибо. Не могли бы вы предоставить ссылку, которая описывает эти микрозадачи и механизм перекраски ?
Ричард
Это выглядит как хорошее резюме: javascript.info/event-loop
CertainPerformance
2
Это не совсем верно, нет. Цикл событий ничего не говорит об изменениях стиля. Большинство браузеров будут пытаться дождаться следующего кадра рисования, когда это возможно, но это все. больше информации ;-)
Kaiido
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-й шаг модели обработки цикла событий , перед этапами рендеринга .
Так что в вашем случае это очень похоже на синхронный запуск.

Кстати, остерегайтесь микро-задач, которые могут блокировать цикл while:

// this will freeze your page just like a while(1) loop
const makeProm = ()=> Promise.resolve().then( makeProm );
Kaiido
источник
Да, именно так, но при ответе на transitionendсобытие можно было бы избежать жесткого кодирования тайм-аута, соответствующего концу перехода. transitionToPromise.js обещает переход, позволяющий писать transitionToPromise(box, 'transform', 'translateX(100px)').then(() => /* four lines as per answer above */).
Roamer-1888
Два преимущества: (1) продолжительность перехода может быть изменена в CSS без необходимости изменения javascript; (2) transitionToPromise.js можно использовать повторно. Кстати, я пробовал, и это работает хорошо.
Roamer-1888
@ Roamer-1888 да, вы могли бы, хотя я бы лично добавил проверку, (evt)=>if(evt.propertyName === "transform"){ ...чтобы избежать ложных срабатываний, и мне не очень нравится обещать такие события, потому что вы никогда не знаете, произойдет ли это когда-нибудь (вспомните случай, например, someAncestor.hide() когда происходит переход, ваше обещание никогда не будет огня , и ваш переход будет застревать отключен , так что это действительно до ОП , чтобы определить , что лучше для них, но лично и по опыту, теперь я предпочитаю таймаута , чем transitionend событий..
Kaiido
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>

Rounin
источник
-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
Нет, OP устанавливает его так, чтобы ''снова применять правило класса (которое было отменено правилом элемента), ваш код просто устанавливает его 'none'дважды, что предотвращает переход блока обратно, но не восстанавливает его первоначальный переход (класса)
Крис G