前言
之前在写抽屉组件时,通常在数据请求回来的时候会加一个 loading
的 state
去设置加载动画,这是一个比较常见的操作,但是有一天开发的时候我突然发现一个场景,初始化完成数据后,后面的动态请求不会再有加载动画,当我继续请求时并响应时,我发现抽屉内的交互被阻塞了,大概在响应的 1-3s
后才能够交互,这个问题的原因很容易发现,因为响应的数据太大,图表组件的计算占用了主线程,导致无法进入到处理 DOM
的部分
发现原因后,我又发现,打开抽屉的初始化的时候没有这个问题,初始化的数据也很大,为什么加载动画结束后不会出现大数据计算占用主线程导致无法交互的问题,而是结束动画后马上就可以交互了,通过这个上下文,我们可以得到抽屉加载数据动画的两个问题
- 为什么加载动画结束后就可以交互了?初始化也存在大量占用主线程的计算,为什么没有阻塞交互?
- 假设初始化计算是和加载动画一起结束的,为什么计算占用主线程这段时间加载动画不会被阻塞?
实现加载前的动画
该章节主要复现前言里的加载动画部分,你可以在这里找到文章里所有的代码
jsx
const SearchBtn = () => {
const [isClick, setIsClcik] = useState(false);
return (
<div>
<span>search btn: </span>
<button
onClick={() => {
console.log("clicked!");
setIsClcik(true);
}}
>
{isClick ? "clciked!" : "click"}
</button>
</div>
);
};
const ListItem = ({ children }) => {
let t = 0;
for (let i = 0; i < 10000; i++) {
for (let i = 0; i < 2000; i++) {
t++;
}
}
return <div className="list-item">{children}</div>;
};
const List = ({ data }) => {
return (
<div>
<SearchBtn />
{data.map((key) => (
<ListItem key={key}>{key}</ListItem>
))}
</div>
);
};
export default function App() {
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
return (
<div className="drawer">
<button
className="drawer-op-btn"
onClick={() => {
setLoading(true);
setTimeout(() => {
setData(Array.from({ length: 20 }).map((_, index) => index));
setLoading(false);
}, 1000);
}}
>
reset to loading
</button>
<button
className="drawer-op-btn"
onClick={() => {
setTimeout(() => {
setData(Array.from({ length: 21 }).map((_, index) => index + 100));
}, 1000);
}}
>
update data
</button>
<div className={classNames("drawer-body", loading && "loading")}>
{loading && <div className="loader"></div>}
{!loading && data.length !== 0 && <List data={data} />}
</div>
</div>
);
}
下面是初始化的效果

可以看到,当加载动画结束以后,<SearchBtn />
马上响应了交互,并打印信息
如果说没有加载动画,效果是怎么样呢?比如点击 update data
jsx
<button
className="drawer-op-btn"
onClick={() => {
setTimeout(() => {
setData(Array.from({ length: 60 }).map((_, index) => index + 100));
}, 0);
}}
>
update data
</button>

点击 update data
后,响应了几次点击,但是过了一会儿就不响应了,GIF
里我一直在点击,但是点击事件一直累计了一段时间才随着 button
的响应点击交互后才执行
让我们简单分析一下为什么会这样,当点击 udpate data
以后,开始发请求,请求使用的 setTimeout
模拟,它的时间为 1000ms
,而 4
次的 clicked
打印就是在 1000ms
触发的,当数据请求回来以后,组件 <ListItem />
的初始化计算开始,而且数量很多,导致主线程被长时间的占用,渲染被阻塞了
jsx
const ListItem = ({ children }) => {
let t = 0;
for (let i = 0; i < 10000; i++) {
for (let i = 0; i < 2000; i++) {
t++;
}
}
...
};
下面是一张主线程被阻塞的可视化图

组件内的初始化计算让主线程停留在了黄色区域,无法进行到绿色区域,也就是 paint
所在的区域
PS: S: Style 样式计算, L: Layout 布局计算, P: paint 绘制也可以理解为最终的渲染
同时点击事件的回调也是在主线程执行的,也就是说初始化计算时间太长也会导致回调执行阻塞,这就是为什么 <SearchBtn />
在等待一段时间响应交互后突然打印一堆 clicked
好,现在我们知道主线程长时间被占用会导致渲染/点击事件回调执行的阻塞,那就来到了我们第一个问题
为什么加载动画结束后就可以交互了?初始化也存在大量占用主线程的计算,为什么没有阻塞交互?
在我们初始化 GIF
结束后,我们点击 <SearchBtn />
是不会经历像 update data
点击后的情况的,马上就会响应交互,难道这个时候 <ListItem />
没有进行计算吗?
这个问题好解释,因为 react
是批量更新状态的,初始化计算在 data
和 loading
状态更新到浏览器前完成
jsx
export default function App() {
...
return (
<div className="drawer">
<button
className="drawer-op-btn"
onClick={() => {
...
setTimeout(() => {
setData(Array.from({ length: 20 }).map((_, index) => index));
setLoading(false);
}, 1000);
}}
>
reset to loading
</button>
...
);
但是仔细一想,又不太对劲,初始化计算是在 loading
状态更新前完成的,也就是说加载动画执行的时候,主线程是被占用的,那么就和我们之前给的可视化有冲突了,加载动画也算渲染呀,为啥没被阻塞呢?
所以就来到我们的第二个问题
假设初始化计算是和加载动画一起结束的,为什么计算占用主线程这段时间加载动画不会被阻塞?
我们来看下我们的加载动画的实现
css
.loader {
width: 30px;
height: 30px;
border: 4px solid #3498db;
border-top: 4px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
注意我们的实现圆环旋转用的是 transform
,关键就在于 transform
,答案是
整个 transform 动画被浏览器丢给 GPU 执行(GPU 加速),不会被阻塞
如果补充一点小知识会让答案更容易理解,JS
线程和 GUI
渲染线程同属渲染进程,它们是互斥的,互斥可以回顾之前的主线程被阻塞的可视化图,但是 transform
的动画不需要经过绿色的 paint
阶段,也就是说它是独立与主线程的,是给 GPU
执行的
但是有些动画是需要经过 paint
的,比如下面这个打点的加载动画
css
/* JSX */
/* {loading && <div className="loader"></div>} */
@keyframes dot-animation {
0% {
content: ".";
}
33% {
content: "..";
}
66% {
content: "...";
}
100% {
content: ".";
}
}
.dot::after {
content: ".";
animation: dot-animation 1s steps(4) infinite;
}
如果用它来作为我们的加载动画,就会被阻塞了,效果如下

这个动画只运行了 setTimeout
模拟的 1000ms
,当 <ListItem />
计算开始时就被阻塞了,就符合我们之前的可视化了

什么?你问我什么 css property 应用到动画上不会被 js 阻塞?
这是两个问题
- 什么 css property 应用到动画 ,能应用到动画上的
css property
实在是太多了,但是不能应用到动画上的css property
也太多了,比如visibility
,所以写动画的时候先跑起来,再研究是不是因为js
阻塞导致的动画停滞
css
/* PS: opacity 可以生效 */
@keyframes change-visibility {
0% {
visibility: hidden;
}
25% {
visibility: visible;
}
100% {
visibility: hidden;
}
}
- 会不会被 js 阻塞 ,试试就知道了
O(∩_∩)O
(不想试就直接上transform
吧!)