日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
前端必學(xué)的動畫實(shí)現(xiàn)思路!

一個(gè)合理的動畫是良好用戶體驗(yàn)中必不可少的一部分。我們平常是怎樣寫動畫的?CSS 中的 animation 和 transition,還有 requestAnimationFrame?

創(chuàng)新互聯(lián)公司于2013年創(chuàng)立,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目網(wǎng)站設(shè)計(jì)、成都網(wǎng)站設(shè)計(jì)網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個(gè)夢想脫穎而出為使命,1280元東昌做網(wǎng)站,已為上家服務(wù),為東昌各地企業(yè)和個(gè)人服務(wù),聯(lián)系電話:18980820575

示例

請看下面的示例:

這是一個(gè)可添加的數(shù)字的隨機(jī)亂序列表。首先想一想,我們第一直覺可能會這樣做:將這些數(shù)字的 DOM 節(jié)點(diǎn)用絕對定位來布局,數(shù)字變化后計(jì)算 top、left 的值,再配合 transition 實(shí)現(xiàn)該動畫。這種方式看似簡單,其實(shí)內(nèi)部要維護(hù)各種位置信息,所有坐標(biāo)都需要手動管理,相當(dāng)繁雜,非常不利于后期擴(kuò)展。如果這些節(jié)點(diǎn)換成高度不固定的圖片,那計(jì)算量可想而知。

那有沒有一種更好的方式實(shí)現(xiàn)呢?肯定的,接下來介紹一個(gè)金光閃閃的概念:FLIP。

提前預(yù)覽:

??https://minjieliu.github.io/react-flip-demo???

FLIP

FLIP 其實(shí)是幾個(gè)單詞的縮寫:即 First、Last 、Invert 、Play。

讓我們分解一下:

First

涉及動畫的元素的初始狀態(tài)(比如位置、縮放、透明等)。

Last

涉及動畫的元素的最終狀態(tài)。

Invert

這一步為核心,即找出這個(gè)元素是如何變化的。例如該元素在 First 和 Last 之間向右移動了 50px,你就需要在 X 方向 translateX(-50px),使元素看起來在 First 位置。

這里有一個(gè)知識點(diǎn)值得注意,DOM 元素屬性的改變(比如 left、right、transform 等),會被集中起來延遲到瀏覽器下一幀統(tǒng)一渲染,所以我們可以得到一個(gè)這樣的中間時(shí)間點(diǎn):DOM 位置信息改變了,而瀏覽器還沒渲染[1]。也就意味著在一定的時(shí)間內(nèi),我們能獲取 DOM 改變后的位置,但在瀏覽器中位置還未改變。經(jīng)測試,這個(gè)過程超過 10ms 就顯得不穩(wěn)定了。因此 setTimeout(fn, 0)、 React useEffect 和 Vue $nextTick 都可以實(shí)現(xiàn) Invert 過程。

Play

即從 Invert 回到最終狀態(tài),有了兩個(gè)點(diǎn)的位置信息,中間的過渡動畫就可以使用 transition 實(shí)現(xiàn)。本文采用 Web Animation API[2] 實(shí)現(xiàn),動畫執(zhí)行過程中不會添加 CSS 到 DOM 上,相當(dāng)干凈。

實(shí)現(xiàn)

這里主要使用 React 方式實(shí)現(xiàn)該效果,其他框架原理都一樣可參考。

一個(gè)列表,將子元素 5 列為一行:

.list {
display: flex;
flex-wrap: wrap;
width: 400px;
}
.item {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border: 1px solid #eee;
}
function ListShuffler() {
const [data, setData] = useState([0, 1, 2, 3, 4, 5]);
const listRef = useRef(null);
return (

{data.map((item) => (

{item}

))}

);
}

首先,我們需要記錄 First 和 Last 的位置信息,并用來計(jì)算 Invert 偏移差,因此用 Map 對象來存儲最合適不過了,有了這個(gè)方法,我們就可以用它來生成前后快照:

function createChildElementRectMap(nodes: HTMLElement | null | undefined) {
if (!nodes) {
return new Map();
}
const elements = Array.from(nodes.childNodes) as HTMLElement[];
// 使用節(jié)點(diǎn)作為 Map 的 key 存儲當(dāng)前快照,下次直接用 node 引用取值,相當(dāng)方便
return new Map(elements.map((node) => [node, node.getBoundingClientRect()]));
}

點(diǎn)擊添加的時(shí)候記錄 First 快照:

// 使用 ref 存儲 DOM 之前的位置信息
const lastRectRef = useRef>(new Map());
function handleAdd() {
// 添加一條到頂部,讓后面節(jié)點(diǎn)運(yùn)動
setData((prev) => [prev.length, ...prev]);
// 并存儲改變前的 DOM 快照
lastRectRef.current = createChildElementRectMap(listRef.current);
}

接下來 DOM 更新后還需要改變后的快照,在 React 中,無論是 useEffect 還是 useLayoutEffect 這里都可以拿到:

useLayoutEffect(() => {
// 改變后的 DOM 快照,此時(shí) UI 并未更新
const currentRectMap = createChildElementRectMap(listRef.current);
}, [data]);

現(xiàn)在,我們就可以把之前的快照進(jìn)行遍歷,實(shí)現(xiàn) Invert 并 Play:

// 遍歷之前的快照
lastRectRef.current.forEach((prevRect, node) => {
// 前后快照的 DOM 引用一樣,可以直接獲取
const currentRect = currentRectMap.get(node);
// Invert
const invert = {
left: prevRect.left - currentRect.left,
top: prevRect.top - currentRect.top,
};
const keyframes = [
{
transform: `translate(${invert.left}px, ${invert.top}px)`,
},
{ transform: 'translate(0, 0)' },
];
// Play 執(zhí)行動畫
node.animate(keyframes, {
duration: 800,
easing: 'cubic-bezier(0.25, 0.8, 0.25, 1)',
});
});

大功告成!這里每個(gè)節(jié)點(diǎn)有單獨(dú)的動畫,各個(gè)節(jié)點(diǎn)之間互不沖突。也就是說無論節(jié)點(diǎn)位置多么復(fù)雜,處理起來都能從容應(yīng)對。

比如圖片亂序只需要從 lodash 引入 shuffle 修改數(shù)據(jù)就可以完美實(shí)現(xiàn)展現(xiàn)。

import { shuffle } from 'lodash-es';
function shuffleList() {
setData(shuffle);
// 并存儲改變前的 DOM 快照
lastRectRef.current = createChildElementRectMap(listRef.current);
}

以上總體思路就是 First -> Last -> Invert -> Play 的一個(gè)變換過程。預(yù)覽下:

你發(fā)現(xiàn)沒有,每次做完操作都需要手動更新快照,作為開發(fā)者不能忍,我們要懶到極致,好好封裝一下。

直白需求:

  1. 數(shù)據(jù)變化后自動執(zhí)行動畫
  2. 可以不關(guān)心任何動畫邏輯
  3. 不要限制 DOM 結(jié)構(gòu)
  4. 用法要簡單
  5. 性能要好

開干!

在 React 更新模型中,執(zhí)行順序?yàn)椋簊etState -> render -> layoutEffect。因此可以把 setState 生成快照的步驟放到 render 中,從而與操作解耦。(如果放到 useLayoutEffect 中動畫頻繁會出現(xiàn)位置計(jì)算不準(zhǔn)確的問題)

useMemo(() => {
// render 時(shí)立即執(zhí)行
lastRectRef.current.forEach((item) => {
item.rect = item.node.getBoundingClientRect();
});
}, [data]);

加上之前 useLayoutEffect 那部分邏輯,我們可以抽到一個(gè)獨(dú)立組件中(Flipper),用 flipKey 來控制,只要 flipKey 變化就執(zhí)行動畫,即實(shí)現(xiàn) 1、2 兩點(diǎn)。

Flipper.tsx

export default function Flipper({ flipKey, children }: FlipperProps) {
const lastRectRef = useRef>(new Map());
const uniqueIdRef = useRef(0);
// 通過 ref 創(chuàng)建函數(shù),傳遞 context 避免引起穿透渲染
const fnRef = useRef({
add(flipItem) {
lastRectRef.current.set(flipItem.flipId, flipItem);
},
remove(flipId) {
lastRectRef.current.delete(flipId);
},
nextId() {
return (uniqueIdRef.current += 1);
},
});
useMemo(() => {
lastRectRef.current.forEach((item) => {
item.rect = item.node.getBoundingClientRect();
});
}, [flipKey]);
useLayoutEffect(() => {
const currentRectMap = new Map();
lastRectRef.current.forEach((item) => {
currentRectMap.set(item.flipId, item.node.getBoundingClientRect());
});
lastRectRef.current.forEach(() => {
// 之前的 FLIP 代碼
});
}, [flipKey]);
return {children};
}

最開始的方式是通過原生方法遍歷 DOM,因此我們只能限制子節(jié)點(diǎn)一個(gè)層級,并且操作方式也脫離的 React 的編寫模型,加以改進(jìn)可以使用 Context 來通信存儲:

FlipContext.ts

import React, { createContext } from 'react';
export type FlipItemType = {
// 子組件的唯一標(biāo)識
flipId: number;
// 子組件通過 ref 獲取的節(jié)點(diǎn)
node: HTMLElement;
// 子組件的位置快照
rect?: DOMRect;
};
export interface IFlipContext {
// mount 后執(zhí)行 add
add: (item: FlipItemType) => void;
// unout 后執(zhí)行 remove
remove: (flipId: number) => void;
// 自增唯一 id
nextId: () => number;
}
export const FlipContext = createContext(
undefined as unknown as React.MutableRefObject,
);

最后則是要實(shí)現(xiàn)采集每個(gè)動畫元素的節(jié)點(diǎn)。將動畫的節(jié)點(diǎn)使用自定義組件 Flipped 包裹并 cloneElement(children { ref }) 劫持 ref,mount 時(shí)將子組件 ref 添加到 Context,unmount 時(shí)則移除。react-photo-view[3] 的封裝方式也是如此。即實(shí)現(xiàn) 3、4 兩點(diǎn)。

Flipped.tsx

import React, {
cloneElement,
memo,
useContext,
useLayoutEffect,
useRef,
} from 'react';
import { FlipContext } from './FlipContext';
export interface FlippedProps {
children: React.ReactElement;
innerRef?: React.RefObject;
}
function Flipped({ children, innerRef }: FlippedProps) {
// Flipper.tsx 將 ref 通過 Context 傳遞,避免穿透渲染
const ctxRef = useContext(FlipContext);
const ref = useRef(null);
const currentRef = innerRef || ref;
useLayoutEffect(() => {
const ctx = ctxRef.current;
const node = currentRef.current;
// 生成唯一 ID
const flipId = ctx.nextId();
if (node) {
// mount 后添加節(jié)點(diǎn)
ctx.add({ flipId, node });
}
return () => {
// unmout 后刪除節(jié)點(diǎn)
ctx.remove(flipId);
};
}, []);
return cloneElement(children, { ref: currentRef });
}
export default memo(Flipped);

好了,看一下如何使用,一共就兩個(gè) API,從原本的 JSX 只需包裹一下就有動畫了:



{data.map((item) => (

{item}


))}

是不是超簡單!最后,還剩性能問題一個(gè)非常重要的指標(biāo)。因?yàn)槊總€(gè)節(jié)點(diǎn)都是獨(dú)立的動畫,數(shù)據(jù)量大了之后渲染肯定卡頓。經(jīng)過測試,5000 個(gè) DIV 節(jié)點(diǎn)的數(shù)字?jǐn)?shù)組的隨機(jī)動畫完成更新時(shí)間為大約 2 秒,這是很不能接受的。我們可以只允許屏幕內(nèi)的節(jié)點(diǎn)有動畫,其他節(jié)點(diǎn)就跳過,只需要稍微判斷一下兩個(gè)狀態(tài)都不在屏幕內(nèi)就好了,這可以節(jié)約 2 / 3 的時(shí)間:

const isLastRectOverflow =
rect.right < 0 ||
rect.left > innerWidth ||
rect.bottom < 0 ||
rect.top > innerHeight;
const isCurrentRectOverflow =
currentRect.right < 0 ||
currentRect.left > innerWidth ||
currentRect.bottom < 0 ||
currentRect.top > innerHeight;
if (isLastRectOverflow && isCurrentRectOverflow) {
return;
}
// node.animate() ...

記得之前 react-beautiful-dnd[4] 庫剛出來的時(shí)候拖拽動畫迷倒了不少人。但是現(xiàn)在有了 FLIP 再配合 react-dnd[5] 就可以輕松實(shí)現(xiàn)此類動畫,功能上就更是屬于碾壓狀態(tài)。而 react-motion[6] 之類的動畫庫實(shí)現(xiàn)該動畫就繁雜很多,因?yàn)樗玫氖墙^對定位控制的類型。下面的例子僅僅用剛封裝的 Flipper 包裹了一下:

以下是源碼:

https://github.com/MinJieLiu/react-flip-demo 其中里面的 Flipper 組件目錄可以直接拷貝到項(xiàng)目中使用,100 來行代碼相當(dāng)輕量 ????。

注意:Web Animation 只兼容 Chrome 75 以上,兼容古董瀏覽器可以考慮 Web Animations API polyfill[7]。

現(xiàn)成的方案

什么?你有更復(fù)雜的動畫需求,自己不想動手,可以看看這個(gè),支持更多特性

react-flip-toolkit[8] 一款有 3.4K Star FLIP 的庫。實(shí)現(xiàn)了你所能想到的功能。

交錯(cuò)效果:

嵌套比例變換:

以及更多

  • Guitar 商城[9]
  • React-flip-toolkit logo[10]
  • 使用 Portals[11]

結(jié)語

相信這種動畫思路肯定能大幅度簡化編寫動畫的門檻,想起自己以前傻傻的用絕對定位計(jì)算位置,真是可笑可笑~


本文題目:前端必學(xué)的動畫實(shí)現(xiàn)思路!
文章出自:http://www.dlmjj.cn/article/djjsjhp.html