在列表開發中,若是須要刪除列表中的某一項,咱們經常使用的刪除方式就是刪除該項後從新更新列表並渲染列表,會有一種生硬的感受,以下:css
gif 中,刪除 box_6 這個列表項以後,以後的列表項按位置直接替換了,這個時候,若是須要在刪除列表某一項後,該項後續的模塊頂上來的過程有個過渡的動畫,這樣就會順滑多了react
// index.js
import React, { useState } from "react";
import { v4 as uuidv4 } from "uuid";
import "./style.css";
const data = Array.from({ length: 10 }, (_, index) => ({
text: `box_${index + 1}`,
id: uuidv4()
}));
export default function App() {
const [list, setList] = useState(data);
const handleDeleteClick = index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
};
return (
<div className="card-list"> {list.map((item, index) => { return ( <ListItem key={item.id} item={item} index={index} handleDeleteClick={handleDeleteClick} /> ); })} </div>
);
}
export const ListItem = ({ item, index, handleDeleteClick }) => {
const itemRef = useRef(null);
return (
<div className="card" ref={itemRef}> <div className="del" onClick={() => handleDeleteClick(index)} /> {item.text} </div>
);
};
複製代碼
刪除列表中的某一項後,觸發了列表的從新渲染,因爲列表項是以惟一 uuid 做爲列表項渲染的 key 根據 React DOM Diff 規則,刪除項以前的列表項因爲 key 不變,所以組件不會變化,而以後的列表項的 key 由於發生了錯位變化因此只會進行一次刪除操做和以後列表項數的移動操做,虛擬 DOM 的構建性能會比較好,可是 React Virtual DOM 轉化爲真實 DOM 的掛載過程仍是會從新繪製,所以整個變化過程看起來會顯得很生硬;markdown
在刪除先後記錄列表中的每一項的相對於列表的位置,加上 transform 動畫便可,動畫時間小於兩次刪除以前的時間間隔就行,主要問題在於,如何記錄列表每一項在刪除動做先後的相對位置dom
import React, { useState, useEffect, useLayoutEffect, useRef } from "react";
import { v4 as uuidv4 } from "uuid";
import "./style.css";
const data = Array.from({ length: 10 }, (_, index) => ({
text: `box_${index + 1}`,
id: uuidv4()
}));
export default function App() {
const [list, setList] = useState(data);
const listRef = useRef(null);
const handleDeleteClick = index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
};
return (
<div className="card-list" ref={listRef}> {list.map((item, index) => { return ( <ListItem key={item.id} item={item} index={index} handleDeleteClick={handleDeleteClick} listRef={listRef} /> ); })} </div>
);
}
const useSimpleFlip = ({ ref, infoInit = null, infoFn, effectFn }, deps) => {
const infoRef = useRef(
typeof infoInit === "function" ? infoInit() : infoInit
);
// useLayoutEffect hook 記錄每次刪除動做後, render 前的列表項當前相對列表容器的相對位置
// useOnceEffect 修正列表項首次渲染後的初始相對位置記錄
useLayoutEffect(() => {
const prevInfo = infoRef.current;
const nextInfo = infoFn(ref, { prevInfo, infoRef });
const res = effectFn(ref, { prevInfo, nextInfo, infoRef });
infoRef.current = nextInfo;
return res;
}, deps);
useOnceEffect(
() => {
infoRef.current = infoFn(ref, { prevInfo: infoRef.current, infoRef });
},
true,
deps
);
};
const useOnceEffect = (effect, condition, deps) => {
const [once, setOnce] = useState(false);
useEffect(() => {
if (condition && !once) {
effect();
setOnce(true);
}
}, deps);
return once;
};
export const ListItem = ({ item, index, handleDeleteClick, listRef }) => {
const itemRef = useRef(null);
useSimpleFlip(
{
infoInit: null,
ref: itemRef,
infoFn: r => {
if (r.current && listRef.current) {
return {
position: {
left:
r.current.getBoundingClientRect().left -
listRef.current.getBoundingClientRect().left,
top:
r.current.getBoundingClientRect().top -
listRef.current.getBoundingClientRect().top
}
};
} else {
return null;
}
},
effectFn: (r, { nextInfo, prevInfo }) => {
if (prevInfo && nextInfo) {
const translateX = prevInfo.position.left - nextInfo.position.left;
const translateY = prevInfo.position.top - nextInfo.position.top;
const a = r.current.animate(
[
{ transform: `translate(${translateX}px, ${translateY}px)` },
{ transform: "translate(0, 0)" }
],
{
duration: 300,
easing: "ease"
}
);
return () => a && a.cancel();
}
}
},
[index]
);
return (
<div className="card" ref={itemRef}> <div className="del" onClick={() => handleDeleteClick(index)} /> {item.text} </div>
);
};
複製代碼