Getting Started with GSAP + React

GSAPReact可以非常好的配好使用,已经有很多的网站在这样做了。GSAP是一个不挑框架的动画库,你完全可以在React中非常轻松的使用GSAP,还有像VueRegular等等任意的框架,只要你需要,都可以搭配上GSAP。这篇文章主要是来介绍在React中使用GSAP的技巧,让你可以更轻松的实现各种需求。

创建一个React应用

如果你喜欢在本地开发应用,可以用Create React App这个命令行工具,帮你快速的创建一个搭配GSAPReact的项目。

创建一个项目

npx create-react-app gsap-app
cd gsap-app
npm start

项目创建后,要对项目进行初始化

npm i gsap
npm start

把该引入的东西引入

import React from "react";
import { gsap } from "gsap";
   
export default function App() {
 return (
   <div className="app">
     <div className="box">Hello</div>
   </div>
 );
}

实现交互动效

我们先来尝试一个小挑战,实现一个基于用户交互的动效。在React中实现这个效果很简单,我们可以通过某个事件回调函数来触发相应的动画执行。在下面这个案例中,onMouseEnter事件会触发元素放大,onMouseLeave事件会触发元素恢复。

但是如果我们想要一个动画在元素加载完之后,没有任何交互行为就会自动触发该如何实现呢?

元素加载后就执行动画 - useLayoutEffect()

useLayoutEffect()这个hookReact在所有DOM结构加载之后立即执行。用这个hook能保证在元素加载渲染完成后再执行动画。以下是一个代码结构案例:

const comp = useRef(); // 先创建一个ref,我们后面会用到一个层级比较高的元素上

useLayoutEffect(() => {
  
  // -- 动画代码写这里 --
  
  return () => { 
    // 清楚代码(可选)
  }
  
}, []); // <- 一个空数组作为依赖项,避免这个hook重复执行

使用Ref来指定元素

i

千万别忘了空的数组依赖。如果你忘了,那么React会在每次重新渲染的时候重新执行这个hook。

为了让元素变化,我们需要告诉GSAP我们要让哪个元素发生变化。React中获取DOM节点的方式就是通过Ref来获取。这个方式非常安全可靠。

const boxRef = useRef();

useLayoutEffect(() => {
  // 通过Ref的方式来获取到dom元素
  console.log(boxRef) // { current: div.box }
  // 然后我们就可以用gsap的方式来进行动画了
  gsap.to(boxRef.current, {
    rotation: "+=360"
  });
});

return (
  <div className="App">
    <div className="box" ref={boxRef}>Hello</div>
  </div>
);

然而,很多时候,我们需要给很多元素都设置动画。如果我们需要给每一个元素都设置一个Ref的话,那样简直是反人类。

所以我们可以使用一个特殊性的方式来解决这个看似头疼的问题,就是使用gsap.context()

gsap.context()是你的好朋友

gsap.context()React开发者提供两个非常棒的功能,一个是scoped selectors(范围内元素选择),另一个是更重要的 - animation cleanup(动画清理)。

i

GSAP Context和React Context是不一样的

Scope Selectors

我们可以给context传入一个Ref来指定一个区域范围,我们就可以在这个范围(子元素们)内,用gsap的方式,比如传入某个元素的类名来选中这些元素,让他们产生动画了。不需要去给每一个元素都设置一个Ref

const comp = useRef(); // 创建一个Ref用来传递给某个元素,来指向一个区域
const circle = useRef();

useLayoutEffect(() => {
  
  // 创建一个context,当这里执行之后,所有相关的gasp动画代码也就被创建了
  // 所以一定要记得通过revert方法来做一个清理,不然可能会多次执行
  let ctx = gsap.context(() => {
    
    // 在这里我们就可以通过css选择器来实现元素的选中
    // 这里只会让有box类名的元素进行动画效果
    gsap.to(".box", {...});
    // 或者你直接用ref也是可以的
    gsap.to(circle.current, { rotation: 360 });
    
  }, comp); // <- 重要!在gsap.context方法中第二个参数一个要传入相应的Ref
  
  return () => ctx.revert(); // 就是这里在做清理
  
}, []); // <- 设置一个空数组,比避免每次渲染的时候这个Hook重复执行
  
// ...

下面是代码示例:

下面这个demo中,React先是渲染了一个方块和一个圆形的DOM元素,然后GSAP把它们旋转了360度,当这些元素被卸载,这些动画会通过ctx.revert()方法清除。

深入一点

是用Refs 还是 scoped selectors ?

通过gsap.context()方式可以很方便的实现在React操作元素的实现动画效果。

需要注意的是,通过这个方式来实现的动画效果,是会穿透子层的。比如你在范围内部的子元素中还有子元素用的同一个类名,那么那个元素也会被驱动。

比如下面这个例子,整个app作为一个Scoped进行处理,然后通过scoped的方式驱动的是.box类名元素。右侧白色的边框的盒子,是和左侧的绿色盒子、紫色圆形平级的,它内部子元素-即绿色方块也用了.box的类名,因此也被驱动产生动画了。这就是我们说的穿透效果。

在旋转的circle元素是用Ref方式来驱动的,而白色边框中的紫色圆形,既没有单独设置ref也没有使用.box的类名,因此没有变化效果。

Cleaning up

清除

useLayoutEffect()给我们提供了清除函数,我们可以用跟他来清除动画效果。在React 18的strict 模式中,必要的动画清除对于避免一些意外情况是非常重要的。这样的方式也是符合React的最佳实践的。

gsap.context让清理这个事情变得非常简单高效。所有的GSAP动画和ScrollTriggers在创建时都被收集到了这个清除函数revert中,只要调用revert函数就能一次把它们都清理了。

你也可以利用清除函数去清除一些容易引起内存泄露的代码,比如事件监听函数。

useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    const animation1 = gsap.to(".box1", { rotation: "+=360" });

    const animation2 = gsap.to(".box2", {
      scrollTrigger: {
        //...
      }
    });
  }, el);

  const onMove = () => {
    //...
  };
  window.addEventListener("pointermove", onMove);

  //  当组件被移除时,清除函数会调用
  return () => {
    ctx.revert(); // 清除动画

    window.removeEventListener("pointermove", onMove); // 清除事件监听函数
  };
}, []);

i

gsap.matchMedia()其实内部是调用的gsap.context(),你可以直接在matchMedia的实例上调用revert方法来进行清除工作(没必要把他们结合起来)。

Reusing components

复用组件

在一个应用内,你可能会需要控制很多的元素进行变化,你可以把整个应用作为scoped范围,然后通过一些特定的类名或者属性参数来对元素进行分别的控制。

i

React建议使用类名来定义用户的样式,然后用data属性来配合实现一些JS功能的操作,比如动画效果。这篇文章中,我们主要是用类名,这样更方便理解。

Creating and controlling timelines

创建和控制时间线动画

到目前为止,我们只用Ref来引用DOM元素,但是他们并不是专门为执行DOM元素。Ref内储存的数据不会被重复渲染影响,所以他们能够用来储存一些你希望在整个组件的生命周期内都会使用到的数据。

为了避免每次重复渲染的时候会产生新的时间线动画实例,我们可以在useEffect中创建一个时间线动画实例,然后通过一个Ref储存起来。

function App() {
  const el = useRef();
  const tl = useRef();

  useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      tl.current = gsap
        .timeline()
        .to(".box", {
          rotate: 360
        })
        .to(".circle", {
          x: 100
        });
    }, el);
  }, []);

  return (
    <div className="app" ref={el}>
      <Box>Box</Box>
      <Circle>Circle</Circle>
    </div>
  );
}

为了避免每次重复渲染的时候会产生新的时间线动画实例,我们可以在useEffect中创建一个时间线动画实例,然后通过一个Ref储存起来。

Controlling when React creates our animation

注意空数组依赖

当通过useLayoutEffect方法出来创建gasp动画,由于组件会在状态更新时重新渲染,如果我们没有给useLayoutEffect传入空数组依赖,就是一个[],那么会造成不必要的重新渲染,如果传入了一个空数组作为依赖,那么,这个useLayoutEffect就只会在组件第一次渲染的时候执行一次,不会重复执行,关于这个依赖的问题你可以点击这里查看React相关的文档

// 只第一次渲染的时候执行
useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    gsap.to(".box-1", { rotation: "+=360" });
  }, el);
}, []);

// 第一次渲染后执行 然后someprop的数据改变时会再次执行
useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    gsap.to(".box-2", { rotation: "+=360" });
  }, el);
}, [someProp]);

// 每次组件重新渲染时都会执行
useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    gsap.to(".box-3", { rotation: "+=360" });
  }, el);
});

Reacting to changes in state

根据某个数据变化重新执行

如果你需要当组件中的某个数据发生改变时重新执行useLayoutEffect方法,那么你要在他的第二个参数,传入相应的数据。这样的做法在某些数据从组件外部传入时经常会用到。

function Box({ children, endX }) {
  const boxRef = useRef();

  // 当 endX 变化时 重新执行
  useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      gsap.to(boxRef.current, {
        x: endX
      });
    });
    return () => ctx.revert();
  }, [endX]);

  return (
    <div className="box" ref={boxRef}>
      {children}
    </div>
  );
}