Tại sao cài đặt thuộc tính CSS bằng Promise.then không thực sự xảy ra ở khối sau đó?


11

Vui lòng thử và chạy đoạn mã sau, sau đó nhấp vào hộp.

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>

Những gì tôi mong đợi sẽ xảy ra:

  • Nhấp chuột xảy ra
  • Hộp bắt đầu dịch theo chiều ngang 100px (hành động này mất hai giây)
  • Khi nhấp chuột, một cái mới Promisecũng được tạo ra. Bên trong cho biết Promise, một setTimeoutchức năng được đặt thành 2 giây
  • Sau khi hành động hoàn thành (hai giây đã trôi qua), hãy setTimeoutchạy chức năng gọi lại của nó và đặt transitionthành không. Sau khi làm điều đó, setTimeoutcũng trở lại transformgiá trị ban đầu của nó, do đó làm cho hộp xuất hiện ở vị trí ban đầu.
  • Hộp xuất hiện ở vị trí ban đầu không có vấn đề về hiệu ứng chuyển tiếp ở đây
  • Sau khi tất cả kết thúc, đặt transitiongiá trị của hộp trở về giá trị ban đầu của nó

Tuy nhiên, như có thể thấy, transitiongiá trị dường như không phải là nonekhi chạy. Tôi biết rằng có các phương pháp khác để đạt được những điều trên, ví dụ như sử dụng keyframe và transitionend, nhưng tại sao điều này lại xảy ra? Tôi dứt khoát thiết lập transitiontrở lại giá trị ban đầu của nó chỉ sau khi các setTimeoutkết thúc cuộc gọi lại của nó, do đó giải quyết Promise.

BIÊN TẬP

Theo yêu cầu, đây là một gif của mã hiển thị hành vi có vấn đề: Vấn đề


Trong trình duyệt nào bạn đang thấy điều này? Trên Chrome, tôi đang thấy những gì nó được dự định.
Terry

@Terry Firefox 73.0 (64-bit) cho Windows.
Richard

Bạn có thể đính kèm một gif cho câu hỏi của bạn minh họa vấn đề? Theo như tôi có thể nói thì nó cũng hiển thị / hành xử như mong đợi trên Firefox.
Terry

Khi Promise giải quyết, quá trình chuyển đổi ban đầu được khôi phục, nhưng tại thời điểm này, hộp vẫn được chuyển đổi. Do đó, nó chuyển tiếp trở lại. Bạn cần đợi ít nhất 1 khung hình nữa trước khi đặt lại quá trình chuyển đổi sang giá trị ban đầu: jsfiddle.net/khrismuc/3mjwtack
Chris G

Khi chạy nhiều lần, tôi đã cố gắng tái tạo vấn đề một lần. Thực hiện chuyển đổi phụ thuộc vào những gì máy tính đang làm ở chế độ nền. Thêm một chút thời gian để trì hoãn trước khi giải quyết lời hứa có thể giúp ích.
Teemu

Câu trả lời:


4

Sự kiện vòng lặp phong cách thay đổi. Nếu bạn thay đổi kiểu của một thành phần trên một dòng, trình duyệt sẽ không hiển thị thay đổi đó ngay lập tức; nó sẽ đợi cho đến khung hình động tiếp theo. Đây là lý do tại sao, ví dụ

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

không dẫn đến nhấp nháy; trình duyệt chỉ quan tâm đến các giá trị kiểu được đặt sau khi tất cả Javascript đã hoàn thành.

Kết xuất xảy ra sau khi tất cả Javascript đã hoàn thành, bao gồm cả microt Nhiệm vụ . Các .thencủa một Promise xảy ra trong một microtask (mà hiệu quả sẽ chạy càng sớm càng tất cả javascript khác đã hoàn tất, nhưng trước khi bất cứ điều gì khác - chẳng hạn như dựng hình - đã có một cơ hội để chạy).

Những gì bạn đang làm là bạn đang đặt thuộc transitiontính ''vào microtask, trước khi trình duyệt bắt đầu hiển thị thay đổi do style.transform = ''.

Nếu bạn đặt lại quá trình chuyển đổi sang chuỗi trống sau một requestAnimationFrame(sẽ chạy ngay trước lần lặp lại tiếp theo), và sau đó setTimeout(sẽ chạy ngay sau lần lặp lại tiếp theo), nó sẽ hoạt động như mong đợi:

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>


Đây là câu trả lời tôi đang tìm kiếm. Cảm ơn. Có lẽ, bạn có thể cung cấp một liên kết mô tả các microtasksơn lại cơ học?
Richard

Đây có vẻ là một bản tóm tắt hay: javascript.info/event-loop
SurePerformance

2
Điều đó không chính xác đúng không. Vòng lặp sự kiện không nói gì về thay đổi phong cách. Hầu hết các trình duyệt sẽ cố gắng chờ khung vẽ tiếp theo khi có thể, nhưng đó là về nó. thêm thông tin ;-)
Kaiido

3

Bạn đang phải đối mặt với một biến thể của quá trình chuyển đổi không hoạt động nếu phần tử bắt đầu sự cố ẩn , nhưng trực tiếp trên thuộc transitiontính.

Bạn có thể tham khảo câu trả lời này để hiểu cách CSSOM và DOM được liên kết cho quá trình "vẽ lại".
Về cơ bản, các trình duyệt thường sẽ đợi cho đến khung vẽ tiếp theo để tính toán lại tất cả các vị trí hộp mới và do đó để áp dụng các quy tắc CSS cho CSSOM.

Vì vậy, trong trình xử lý Promise của bạn, khi bạn đặt lại transitionthành "", transform: ""vẫn chưa được tính toán. Khi nó được tính toán, ý transitionchí đã được đặt lại ""và CSSOM sẽ kích hoạt quá trình chuyển đổi cho bản cập nhật biến đổi.

Tuy nhiên, chúng tôi có thể buộc trình duyệt kích hoạt "chỉnh lại dòng" và do đó chúng tôi có thể làm cho nó tính toán lại vị trí của phần tử của bạn, trước khi chúng tôi đặt lại quá trình chuyển đổi sang "".

Điều này làm cho việc sử dụng Promise khá không cần thiết:

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>


Và đối với một lời giải thích trên vi nhiệm vụ, như Promise.resolve()hoặc MutationEvents , hoặc queueMicrotask(), bạn cần phải hiểu họ sẽ nhận ran ngay sau khi nhiệm vụ hiện nay được thực hiện, bước thứ 7 của mô hình xử lý sự kiện vòng , trước khi các bước vẽ .
Vì vậy, trong trường hợp của bạn, nó rất giống như nếu nó được chạy đồng bộ.

Nhân tiện, hãy cẩn thận các tác vụ vi mô có thể bị chặn như một vòng lặp while:

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

Có chính xác, nhưng phản ứng với transitionendsự kiện sẽ tránh phải mã hóa thời gian chờ để khớp với kết thúc quá trình chuyển đổi. transitionToPromise.js sẽ promisify quá trình chuyển đổi cho phép bạn viết transitionToPromise(box, 'transform', 'translateX(100px)').then(() => /* four lines as per answer above */).
Roamer-1888

Hai ưu điểm: (1) thời lượng của quá trình chuyển đổi có thể được sửa đổi trong CSS mà không cần sửa đổi javascript; (2) transToPromise.js có thể tái sử dụng. BTW, tôi đã thử nó và nó hoạt động tốt.
Roamer-1888

@ Roamer-1888, vâng, bạn có thể, mặc dù tôi sẽ tự mình thêm một tấm séc (evt)=>if(evt.propertyName === "transform"){ ...để tránh những thông tin sai lệch và tôi thực sự không muốn quảng cáo những sự kiện như vậy, bởi vì bạn không bao giờ biết liệu nó có phát sinh hay không (nghĩ về một trường hợp như someAncestor.hide() khi quá trình chuyển đổi diễn ra, Lời hứa của bạn sẽ không bao giờ kích hoạt và quá trình chuyển đổi của bạn sẽ bị vô hiệu hóa. Vì vậy, điều đó thực sự tùy thuộc vào OP để xác định điều gì là tốt nhất cho họ, nhưng cá nhân và theo kinh nghiệm, giờ đây tôi thích thời gian chờ hơn là sự kiện chuyển tiếp.
Kaiido

1
Ưu điểm & nhược điểm, tôi đoán vậy. Trong mọi trường hợp, có hoặc không có quảng cáo, câu trả lời này rõ ràng hơn câu trả lời liên quan đến hai setTimeouts và requestAnimationFrame.
Roamer-1888

Tôi có một câu hỏi mặc dù. Bạn nói rằng requestAnimationFrame()sẽ được kích hoạt ngay trước khi trình duyệt tiếp theo sơn lại. Bạn cũng đã đề cập rằng các trình duyệt thường sẽ đợi cho đến khung vẽ tiếp theo để tính toán lại tất cả các vị trí hộp mới . Tuy nhiên, bạn vẫn cần phải tự kích hoạt một dòng lại bắt buộc (câu trả lời của bạn trên liên kết đầu tiên). Do đó, tôi rút ra kết luận rằng ngay cả khi requestAnimationFrame()xảy ra ngay trước khi sơn lại, trình duyệt vẫn chưa tính được kiểu tính toán mới nhất; do đó, cần phải tự tính toán lại các kiểu. Chính xác?
Richard

0

Tôi đánh giá cao đây không hoàn toàn là những gì bạn đang tìm kiếm, nhưng - vì tò mò và vì mục đích hoàn chỉnh - tôi muốn xem liệu tôi có thể viết một cách tiếp cận chỉ CSS cho hiệu ứng này không.

Gần như ... nhưng hóa ra tôi vẫn phải đưa vào một dòng javascript.

Ví dụ làm việc:

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

Tôi tin rằng vấn đề của bạn chỉ là trong bạn .thenbạn đang thiết lập transitionđể '', khi bạn cần phải cài đặt nó vào nonenhư bạn đã làm trong giờ gọi lại.

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
Không, OP đang thiết lập nó để ''áp dụng lại quy tắc lớp (được ghi đè bởi quy tắc phần tử), mã của bạn chỉ cần đặt nó thành 'none'hai lần, điều này ngăn hộp chuyển trở lại nhưng không khôi phục chuyển đổi ban đầu (của lớp)
Chris G
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.