Promise.를 사용하여 CSS 속성을 설정하면 실제로 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>

내가 일어날 것으로 예상되는 것 :

  • 클릭이 발생합니다
  • 상자가 가로로 100px 번역을 시작합니다 (2 초 소요)
  • 클릭하면 새로운 Promise것도 생성됩니다. 내부 Promise에서 setTimeout기능이 2 초로 설정되었습니다.
  • 조치가 완료된 후 (2 초가 경과 한 후) setTimeout콜백 기능을 실행하고 transitionnone으로 설정하십시오 . 그런 다음 원래 값으로 setTimeout되돌려 transform서 상자를 원래 위치에 표시합니다.
  • 상자는 전환 효과 문제 없이 원래 위치에 나타납니다.
  • 모든 작업이 끝나면 transition상자 값을 원래 값으로 다시 설정하십시오.

그러나 알 수 있듯이 transition값을 실행할 none때 보이지 않는 것 같습니다 . 위와 같은 다른 방법, 예를 들어 keyframe 및를 사용하는 방법 transitionend이 있지만 왜 이런 일이 발생합니까? 콜백 이 완료된 후에transition 만 명시 적으로 원래 값으로 되돌려 서 약속을 해결합니다.setTimeout

편집하다

요청에 따라 문제가되는 동작을 표시하는 코드는 다음과 같습니다. 문제


어떤 브라우저에서 이것을보고 있습니까? Chrome에서 의도 한 내용을보고 있습니다.
테리

Windows 용 @Terry Firefox 73.0 (64 비트).
Richard

문제를 설명하는 gif를 질문에 첨부 할 수 있습니까? 내가 말할 수있는 한 Firefox에서 예상대로 렌더링 / 동작하고 있습니다.
테리

약속이 해결되면 원래 전환이 복원되지만이 시점에서 상자는 여전히 변형됩니다. 따라서 다시 전환됩니다. jsfiddle.net/khrismuc/3mjwtack
Chris G

여러 번 실행하면 문제를 한 번 재현했습니다. 전환 실행은 컴퓨터가 백그라운드에서 수행하는 작업에 따라 다릅니다. 약속을 해결하기 전에 지연 시간을 조금 더 추가하면 도움이 될 수 있습니다.
Teemu

답변:


4

이벤트 루프 배치 스타일이 변경됩니다. 한 줄에서 요소의 스타일을 변경하면 브라우저에 해당 변경 사항이 즉시 표시되지 않습니다. 다음 애니메이션 프레임까지 기다립니다. 이것이 예를 들어

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

깜박 거리지 않습니다. 브라우저는 모든 Javascript가 완료된 후에 설정 한 스타일 값만 고려합니다.

렌더링은 마이크로 태스크를 포함하여 모든 Javascript가 완료된 후에 발생합니다 . 약속의는 microtask 발생 (다른 모든 자바 스크립트가 끝난 효과적으로 빨리으로 실행되지만 다른 어떤 전에 - 실행에 기회가있다 - 같은 렌더링 등)..then

브라우저가에 의한 변경 사항을 렌더링하기 전에 마이크로 작업에서 transition속성을 설정하고 있습니다 .''style.transform = ''

a 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>


이것이 내가 찾던 대답입니다. 감사. 아마도 이러한 마이크로 태스크다시 그리기 역학 을 설명하는 링크를 제공 할 수 있습니까?
Richard

이것은 좋은 요약처럼 보입니다 : javascript.info/event-loop
CertainPerformance

2
그것은 사실이 아닙니다. 이벤트 루프는 스타일 변경에 대해 아무 것도 말하지 않습니다. 대부분의 브라우저는 가능한 한 다음 그림 프레임을 기다리려고하지만 그게 전부입니다. 더 많은 정보 ;-)
Kaiido

3

요소가 숨겨진 문제를 시작 하지만 transition속성에서 직접 시작하면 변형의 변형이 작동하지 않습니다 .

CSSOM과 DOM이 "다시 그리기"프로세스를 위해 어떻게 연결되는지 이해 하려면 이 답변 을 참조하십시오 .
기본적으로 브라우저는 일반적으로 다음 페인팅 프레임 이 모든 새 상자 위치를 다시 계산하고 CSSOM에 CSS 규칙을 적용 할 때까지 기다립니다 .

당신의 약속 처리기 그래서, 당신이 다시 때 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 );

그렇습니다. 그러나 transitionend이벤트에 응답 하면 전환이 끝날 때까지 시간 초과를 하드 코딩하지 않아도됩니다. transitionToPromise.js 는 여러분이 글을 쓸 수있는 전환 을 약속합니다 transitionToPromise(box, 'transform', 'translateX(100px)').then(() => /* four lines as per answer above */).
Roamer-1888

두 가지 장점 : (1) 전환 시간은 자바 스크립트를 수정하지 않고도 CSS에서 수정 될 수 있습니다. (2) transitionToPromise.js는 재사용 가능합니다. BTW, 나는 그것을 시도하고 잘 작동합니다.
Roamer-1888

@ Roamer-1888 그렇습니다. (evt)=>if(evt.propertyName === "transform"){ ...거짓 긍정을 피하기 위해 개인적으로 수표 를 추가하고 그러한 사건을 약속하는 것을 좋아하지는 않습니다 someAncestor.hide() . Promise는 절대로 발생하지 않으며 전환이 중단됩니다. 따라서 자신에게 가장 적합한 것을 결정하는 것은 실제로 OP에 달려 있지만 개인적으로 경험에 따라 전환 이벤트보다 시간 초과를 선호합니다.
Kaiido

1
찬반 양론 어쨌든 약속의 유무에 관계 없이이 대답은 두 개의 setTimeouts 및 requestAnimationFrame과 관련된 것보다 훨씬 깨끗합니다.
Roamer-1888

그래도 질문이 하나 있습니다. 당신은이 말했다 requestAnimationFrame()트리거됩니다 직전에 다음 브라우저 재 페인트. 또한 브라우저는 일반적으로 모든 새 상자 위치를 다시 계산하기 위해 다음 페인팅 프레임까지 기다립니다 . 그러나 여전히 강제 리플 로우를 수동으로 트리거해야합니다 (첫 번째 링크에 대한 답변). 따라서 requestAnimationFrame()다시 그리기 직전에 발생 하더라도 브라우저가 여전히 최신 계산 스타일을 계산 하지 않았다는 결론을 내립니다 . 따라서 수동으로 스타일을 다시 계산 해야 합니다. 옳은?
Richard

0

나는 이것이 당신이 찾고있는 것이 아니라고 생각하지만 호기심과 완전성을 위해이 효과에 대한 CSS 전용 접근법을 작성할 수 있는지 알고 싶었습니다.

거의 ...하지만 여전히 한 줄의 자바 스크립트를 포함해야한다는 것이 밝혀졌습니다.

작업 예 :

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'다시 전환에서 상자를 방지하는 두 번 있지만, 원래의 전환을 복원하지 않습니다 (클래스의)
Chris G
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.