เหตุใดการตั้งค่าคุณสมบัติ CSS โดยใช้ Promise แล้วไม่เกิดขึ้นจริงในบล็อกนั้น


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 (การกระทำนี้ใช้เวลาสองวินาที)
  • เมื่อคลิกใหม่Promiseจะถูกสร้างขึ้น ภายในกล่าวว่าPromiseเป็นsetTimeoutฟังก์ชั่นการกำหนดเป็น 2 วินาที
  • หลังจากการดำเนินการเสร็จสมบูรณ์ (ผ่านไปสองวินาที) ให้setTimeoutเรียกใช้ฟังก์ชันการโทรกลับและตั้งค่าtransitionเป็นไม่มี หลังจากทำเช่นนั้นแล้วsetTimeoutจะเปลี่ยนกลับtransformเป็นค่าดั้งเดิมดังนั้นจึงแสดงผลกล่องเพื่อให้ปรากฏที่ตำแหน่งเดิม
  • กล่องจะปรากฏขึ้นที่ตำแหน่งเดิมโดยไม่มีปัญหาผลการเปลี่ยนแปลงที่นี่
  • หลังจากเสร็จสิ้นทั้งหมดให้ตั้งtransitionค่าของกล่องกลับไปเป็นค่าดั้งเดิม

อย่างไรก็ตามตามที่เห็นtransitionค่าดูเหมือนจะไม่noneทำงานเมื่อ ฉันรู้ว่ามีวิธีอื่น ๆ เพื่อให้บรรลุดังกล่าวข้างต้นเช่นการใช้คีย์เฟรมและtransitionendแต่สิ่งนี้เกิดขึ้นได้อย่างไร ฉันอย่างชัดเจนตั้งค่าtransitionกลับไปเป็นค่าเดิมเท่านั้นหลังจากsetTimeoutเสร็จสิ้นการเรียกกลับของมันดังนั้นการแก้ไขสัญญา

แก้ไข

ตามคำขอนี่คือ gif ของรหัสที่แสดงพฤติกรรมที่มีปัญหา: ปัญหา


คุณเห็นสิ่งนี้ในเบราว์เซอร์ใด บน Chrome ฉันเห็นสิ่งที่ตั้งใจไว้
เทอร์รี่

@Terry Firefox 73.0 (64 บิต) สำหรับ Windows
ริชาร์ด

คุณสามารถแนบ gif กับคำถามที่แสดงปัญหาได้หรือไม่ เท่าที่ฉันจะบอกได้ว่ามันยังแสดงผล / พฤติกรรมตามที่คาดไว้ใน Firefox
เทอร์รี่

เมื่อสัญญาได้รับการแก้ไขการเปลี่ยนแปลงเดิมจะถูกกู้คืน แต่ ณ จุดนี้กล่องยังคงถูกเปลี่ยน ดังนั้นจึงเปลี่ยนกลับ คุณต้องรออย่างน้อย 1 เฟรมเพิ่มเติมก่อนที่จะรีเซ็ตการเปลี่ยนเป็นค่าดั้งเดิม: jsfiddle.net/khrismuc/3mjwtack
Chris G

เมื่อทำงานหลายครั้งฉันพยายามทำให้เกิดปัญหาซ้ำอีกครั้ง การดำเนินการเปลี่ยนแปลงขึ้นอยู่กับสิ่งที่คอมพิวเตอร์กำลังทำอยู่เบื้องหลัง การเพิ่มเวลาในการล่าช้าก่อนที่จะแก้ไขสัญญาอาจช่วยได้
Teemu

คำตอบ:


4

การวนลูปเหตุการณ์เป็นกลุ่มมีการเปลี่ยนแปลงรูปแบบ หากคุณเปลี่ยนสไตล์ขององค์ประกอบในหนึ่งบรรทัดเบราว์เซอร์จะไม่แสดงการเปลี่ยนแปลงนั้นทันที มันจะรอจนกระทั่งเฟรมภาพเคลื่อนไหวถัดไป นี่คือเหตุผลเช่น

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

ไม่ส่งผลให้ริบหรี่; เบราว์เซอร์จะใส่ใจเฉพาะค่าสไตล์ที่กำหนดไว้หลังจากที่ Javascript ทั้งหมดทำงานเสร็จ

การแสดงผลที่เกิดขึ้นหลังจาก Javascript ทั้งหมดเสร็จสมบูรณ์รวมทั้ง microtasks .thenของสัญญาที่เกิดขึ้นใน microtask (ซึ่งจะทำงานได้อย่างมีประสิทธิภาพโดยเร็วที่สุดเท่าทุก Javascript อื่น ๆ ได้เสร็จสิ้น แต่ก่อนสิ่งอื่นใด - เช่นการแสดงผล - มีโอกาสที่จะทำงาน)

สิ่งที่คุณกำลังทำคือการที่คุณตั้งค่าtransitionคุณสมบัติการ''ใน microtask 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>


นี่คือคำตอบที่ฉันต้องการ ขอบคุณ บางทีคุณอาจให้ลิงก์ที่อธิบายถึงกลไกและการทาสีซ้ำเหล่านี้หรือไม่?
Richard

นี่เป็นข้อมูลสรุปที่ดี: javascript.info/event-loop
CertainPerformance

2
นั่นไม่ใช่ความจริงที่แน่นอน ห่วงเหตุการณ์ไม่ได้พูดอะไรเกี่ยวกับการเปลี่ยนแปลงสไตล์ เบราว์เซอร์ส่วนใหญ่จะพยายามรอเฟรมภาพต่อไปเมื่อทำได้ แต่ก็เกี่ยวกับมัน ข้อมูลเพิ่มเติม ;-)
Kaiido

3

คุณกำลังเผชิญกับการเปลี่ยนแปลงของการเปลี่ยนแปลงไม่ทำงานหากองค์ประกอบเริ่มต้นปัญหาที่ซ่อนอยู่แต่โดยตรงกับtransitionคุณสมบัติ

คุณสามารถอ้างถึงคำตอบนี้เพื่อทำความเข้าใจว่า CSSOM และ DOM เชื่อมโยงกับกระบวนการ "redraw" อย่างไร
โดยทั่วไปเบราว์เซอร์จะรอจนกว่าเฟรมภาพวาดถัดไปเพื่อคำนวณตำแหน่งกล่องใหม่ทั้งหมดและใช้กฎ CSS กับ CSSOM

ดังนั้นในการจัดการสัญญาของคุณเมื่อคุณตั้งค่าtransitionไป""ที่transform: ""ยังไม่ได้รับการคำนวณเลย เมื่อมันได้รับการคำนวณtransitionจะถูกรีเซ็ตไป""แล้วและ CSSOM จะทริกเกอร์การเปลี่ยนแปลงสำหรับการอัพเดทการแปลง

แต่เราสามารถบังคับให้เบราว์เซอร์ที่จะเรียก "reflow"""และทำให้เราสามารถทำให้มันคำนวณตำแหน่งขององค์ประกอบของคุณก่อนที่เราจะตั้งค่าการเปลี่ยนแปลงไป

ซึ่งทำให้การใช้งานของสัญญาค่อนข้างไม่จำเป็น:

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 โดยไม่จำเป็นต้องแก้ไขจาวาสคริปต์ (2) transitionToPromise.js สามารถใช้ซ้ำได้ BTW ฉันลองและใช้งานได้ดี
Roamer-1888

@ Roamer-1888 ใช่คุณทำได้แม้ว่าฉันจะเพิ่มการตรวจสอบเป็นการส่วนตัว(evt)=>if(evt.propertyName === "transform"){ ...เพื่อหลีกเลี่ยงการบวกเท็จและฉันไม่ชอบที่จะแนะนำกิจกรรมดังกล่าวเพราะคุณไม่เคยรู้ว่ามันจะยิงได้ someAncestor.hide() หรือไม่ สัญญาของคุณจะไม่เกิดไฟไหม้และการเปลี่ยนแปลงของคุณจะได้รับการติดคนพิการเพื่อให้เป็นจริงขึ้นอยู่กับ OP เพื่อตรวจสอบสิ่งที่ดีที่สุดสำหรับพวกเขา แต่ส่วนตัวและจากประสบการณ์ตอนนี้ผมชอบหมดเวลากว่าเหตุการณ์ transitionend..
Kaiido

1
ข้อดี & ข้อเสียฉันเดา ในกรณีใด ๆ ไม่ว่าจะมีหรือไม่มีคำสัญญาคำตอบนี้จะไกลกว่าหนึ่งที่เกี่ยวข้องกับสอง setTimeouts และ requestAnimationFrame
Roamer-1888

ฉันมีคำถามหนึ่งข้อ คุณบอกว่าrequestAnimationFrame()จะถูกเรียกใช้ก่อนที่จะทาสีเบราว์เซอร์ต่อไป นอกจากนี้คุณยังกล่าวว่าโดยทั่วไปแล้วเบราว์เซอร์จะรอจนกว่ากรอบภาพวาดถัดไปเพื่อคำนวณตำแหน่งกล่องใหม่ทั้งหมด แต่คุณยังจำเป็นต้องเรียกใช้ reflow แบบบังคับด้วยตนเอง (คำตอบของคุณในลิงก์แรก) ฉันจึงสรุปว่าแม้จะrequestAnimationFrame()เกิดขึ้นก่อนทาสีเบราว์เซอร์ยังไม่ได้คำนวณรูปแบบการคำนวณใหม่ล่าสุด ดังนั้นจึงจำเป็นต้องบังคับให้มีการคำนวณรูปแบบใหม่ด้วยตนเอง แก้ไข?
ริชาร์ด

0

ฉันขอขอบคุณที่นี่ไม่ใช่สิ่งที่คุณกำลังมองหา แต่ - ด้วยความอยากรู้อยากเห็นและเพื่อความสมบูรณ์ - ฉันต้องการที่จะดูว่าฉันสามารถเขียนวิธี CSS-only กับเอฟเฟ็กต์นี้ได้หรือไม่

เกือบ ... แต่มันกลับกลายเป็นว่าฉันยังต้องมีจาวาสคริปต์หนึ่งบรรทัด

ตัวอย่างการทำงาน:

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
ไม่ OP กำลังตั้งค่า''ให้ใช้กฎคลาสอีกครั้ง (ซึ่งถูกครอบงำโดยกฎองค์ประกอบ) โค้ดของคุณตั้งค่าเป็น'none'สองเท่าซึ่งจะป้องกันกล่องจากการเปลี่ยนกลับ แต่ไม่คืนค่าการเปลี่ยนแปลงดั้งเดิม (คลาส)
Chris G
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.