在Vue中實現Svelte的Defer Transition

點選上方 前端瓶子君,關注公眾號

回復演算法,加入前端程式設計面試演算法每日一題群

來源:Jayden.李

https://juejin.cn/post/6949441502321836046

最近觀看了Rich Harris的影片,驚嘆於Svelte框架的高效同時,還發現了Vue所不具備的一些關於影片的原生支援—defer transitions.

先看看Svelte所謂的defer transition的效果吧。

svelteTodo.gif

這是使用Svelte做的Todo Demo應用。整個todo分成3個部分。輸入部分,todo串列和done串列。當點選todo串列中的條目時,相應條目會被「移動」到done串列,反之亦然。

在這裡,條目從一個串列轉移到另一個串列,不是很突兀的閃現,而是非常友好地從點選處,移動到目的地;同時,當串列中條目離開時,剩餘的條目會絲滑地向上移動填補空缺的位置。在Svelte里,只需要僅僅加上幾行程式碼,就能實現,對於開發者非常友好且高效。參考如下程式碼或者Svelte教學

1
2
3
4
5
6
7
8
9
10
11
{#each todos.filter(t => !t.done) as todo (todo.id)}
    <label
 in:receive="{<!-- -->{key: todo.id}}"
        out:send="{<!-- -->{key: todo.id}}"
        animate:flip>
        <input type=checkbox on:change={() => mark(todo, true)}>
            {todo.description}
            <button on:click="{() => remove(todo)}">remove</button>
    </label>
{/each}
複製程式碼

僅僅在element上加上了in、out和animate屬性。這裡,in和out各自接受框架提供的函式receive和send並且給他們提供了篩選條件。animate屬性接收內建的flip方法。這裡的flip不是翻轉,而是FLIP技術技術,vue在中也有用到。主要用於整體移動串列剩餘條目去填補所失去元素的位置。

於是我就在想,如果是Vue的話,如何能達到相應的效果呢。(不想看詳細說明的話,可以直接檢視code pen中的程式碼)

Vue原生提供了兩個套件支援影片。transition和transition-group。由於是list的移動,所以我們這裡使用transition-group。具體使用方法可以參考Vue教學Transitions & Animation。

要想達到同樣的效果,有兩大UI影片效果要實現。

  1. 串列中條目消失時,剩餘條目移動補齊空位

  2. 條目消失同時在另外一個串列寫入時,條目移動

第一個需求的實現比較簡單,vue原生已經提供了良好的支援,參考Vue檔案中的List-Move-Transitions即可

為了實現第二個需求,有幾個問題必須解決:

  1. 消失條目的位置訊息

  2. 寫入條目的位置訊息

  3. 動效開始與結束的時機

我們先看看前兩個問題的如何解決。根據檔案的介紹,transition-group提供了javascript hook。分別是:

1
2
3
4
5
6
7
8
9
10
 v-on:before-enter
 v-on:enter
 v-on:after-enter
 v-on:enter-cancelled

 v-on:before-leave
 v-on:leave
 v-on:after-leave
 v-on:leave-cancelled
複製程式碼

視覺化表示的話,大概是如下圖所示:

Screen Shot 2021-04-08 at 5.54.00 pm.png

before-enter: 用於設定寫入條目的transition的初始值。此時無法取得BoundingClientRect. enter: 動效期。此時enter鉤子函式的入參el能取得boundingClientRect after-enter: 動效結束後的回呼函式 enter-cancelled: 取消enter的鉤子

leave也是類似。

所以,我們能拿到條目元素DOMRect訊息的時機衹有enter和leave的時候。

這樣,我們就可以在leave時候,拿到leave條目的DOMRect資料並且儲存起來。在enter的時候, 我們就能同時擁有leave條目和enter條目的位置訊息了。

位置訊息是拿到了,那怎麼才能在條目進入的時候,有從消失條目移動過來的效果呢。(可以先想想, 再看後面的解釋)。

所以,我們想要達成移動的效果,首先需要隱藏掉leave條目元素,

1
2
3
4
5
6
7
leave(el, done) {
      console.log("before leave");
      const rect = el.getBoundingClientRect();
      sendRectMap.set(el.dataset.key, rect);
      el.style.display = "none";
},
複製程式碼

然後給enter條目元素設定關於位置初始狀態,初始化的位置即為leave條目元素的位置,然後當transition開始生效的時候,讓其位置恢復到寫入(enter)的位置。

這種方法其實就是所謂的FLIP技術。transition-group套件里也使用了這種技術來移動剩餘串列填充移走條目空白。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var first = el.getBoundingClientRect();

// Now set the element to the last position.
el.classList.add('totes-at-the-end');

// Read again. This forces a sync
// layout, so be careful.
var last = el.getBoundingClientRect();

// You can do this for other computed
// styles as well, if needed. Just be
// sure to stick to compositor-only
// props like transform and opacity
// where possible.
var invert = first.top - last.top;

// Invert.
el.style.transform =
    `translateY(${invert}px)`;

// Wait for the next frame so we
// know all the style changes have
// taken hold.
requestAnimationFrame(function() {

  // Switch on animations.
  el.classList.add('animate-on-transforms');

  // GO GO GOOOOOO!
  el.style.transform = '';
});
複製程式碼

那麼接下來的問題就是,在什麼時機去設定enter條目元素transition的初始狀態,在什麼時機去設定enter條目元素transition的結束時狀態。

按照上面提到的javascript hook,我們可以在before-enter鉤子函式里設定初始狀態,接著在enter鉤子函式里設定transition結束狀態。那麼,我們的初始狀態是什麼呢?我們透過getBoundingClientRect,可以取得到enter元素(後用to來標識)的DOMRect資料,包括top, left, bottom, right, width, height , x, y。同時我們也能透過之前儲存的leave位置map取得到leave條目的位置訊息(稱為from)。所以在before-enter時,我們透過計算得到的偏移量,透過translate去初始化to元素的位置。然後再在enter階段,translate其值到from, 再加上transition到css中即可。

在before-enter鉤子中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
        // 移動元素的標識符
        const key = el.dataset.key;
        // enter條目 map,注意這裡,在before-enter鉤子中,
        // receiveRectMap是get不到to的。需要特殊處理。後面有提及
        const to = receiveRectMap.get(key);
        // leave條目 map
        const from = sendRectMap.get(key);

        // 計算偏移量
        const dx = from.left - to.left;
        const dy = from.top - to.top;
       
        // 初始化to條目的位置
        el.style.transform = `translate(${dx}px, ${dy}px)`;
        el.style.opacity = 0;
複製程式碼

在enter鉤子中:

1
2
3
4
5
        el.style.transition = "all 800ms";
        el.style.transform = "";
        el.style.opacity = 1;
        el.style.display = "block";
複製程式碼

上面的程式碼中,在before-enter裡面,to是透過receiveRectMap.get(key)來取得的。但是,這時,receiveRectMap中還沒有對應key的DOMRect值。雖然,before-enter的入參是el(HTMLElement),但是該el元素的DOMRect中的所有值都為0,所以我們需要在enter方法中,把el塞入到receiveRectMap中。這樣就會有一個矛盾,那就是無法在before-enter中透過translate初始化to元素的位置了。所以,我們這裡使用defer transition技術,延遲transition的發生。

我們可以在enter中使用setTimeout或者requestAnimationFrame實現defer transition,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
requestAnimationFrame(() => {
        const key = el.dataset.key;
    // 這樣,receiveRectMap中就有該key的值了。
        const to = receiveRectMap.get(key);
        const from = sendRectMap.get(key);

        const dx = from.left - to.left;
        const dy = from.top - to.top;

        // 由於我們延遲了transition的發生,
        // 所以to元素的位置其實已經到達了目的地位置,
        // 所以我們需要使用transition手動地將其轉場到from位置,這一行很重要
        el.style.transition = "all 0ms";
        el.style.transform = `translate(${dx}px, ${dy}px)`;
       
        // 初始化結束後,在下一個animation frame中,使用FLIP技術,使其移動回來。
 requestAnimationFrame(() => {
          el.style.transition = "all 800ms";
          el.style.transform = "";
          el.style.opacity = 1;
          el.style.display = "block";
        });
      });
複製程式碼

完整程式碼可以參考codepen

最後效果:

vue-defer-transition.gif

最後

歡迎關注【前端瓶子君】??ヽ(°▽°)ノ?

回復「演算法」,加入前端程式設計原始碼演算法群,每日一道面試題(工作日),第二天瓶子君都會很認真的解答喲!

回復「交流」,吹吹水、聊聊技術、吐吐槽!

回復「閱讀」,每日刷刷高品質好文!

如果這篇文章對你有幫助,「在看」是最大的支援

》》面試官也在看的演算法資料《《

「在看和轉發」就是最大的支援