网上对useEffect、useLayoutEffect的解释真是太差了这篇文章会是讲解useEffect与useLay
如果你不能将一个事物简单化,那说明你还没触碰到事物的本质。
这句话非常适用于今天要讨论的问题。
这个知识点我之前一直有关注,但是我一直没找到答案,一方面是React官网没解释清楚,另一方面是网上的资料真是太差劲了,99.9%的文章都是一样,相互抄袭,但源头都一样,摘抄自某几位大神,而这几位大神呢,都有一个共同的特点,直接给你展示源码,当时我的思想如下:
emmm,源码... 源码你既然都能读明白,而且读的还那么细,那你为啥不实现一个呢(比如实现一个react,实现一个.vue模版)?或者为啥不把某个知识点可视化出来呢? 后来我想明白了,无非以下3点:
- 对方找资源的能力比较强,因此他能比你先写出来看似华丽的源码解析。
- 似懂非懂,但如果能把自己营销出去,这也是一种能力。毕竟代码这东西,你硬读也是可以的,那就走量呗。
- 或许大神们不屑于写这类文章,观众懂不懂的,优先级靠后,自己知道就行。
回到我们这篇文章,请看下面的代码:
let dom = document.createElement('div');
dom.classList.add('div1');
dom.style.left = '200px';
dom.style.left = '100px';
document.body.appendChild(dom);
css如下:
.div1 {
width: 150px;
height: 150px;
background-color: cornflowerblue;
box-sizing: border-box;
position: relative;
left: 0px;
transition: all 3s;
display: flex;
justify-content: center;
align-items: center;
}
执行一下上面的这2段代码,给大家2个选项:
- div直接显示在距离左侧100px的位置,并且无过渡效果;
- div先从0运动200,再由200px运动到100px,整个过程都有过渡效果;
大家认为哪个选项是正确答案?
答案是第一个。
运行上面的代码后我们会发现,极大多数的情况下,不是说js引擎运行一行代码,浏览器就渲染一行代码的差异。那浏览器什么时候会渲染差异呢?这跟浏览器的事件循环有关,有关事件循环的运行机制,建议大家去chrome for developer
或者web.dev
平台上找找答案,就不要看国内的文章了,国内的也是看的国外。这个知识点你只能是无限接近,但你绝对不可能得到一个标准答案,因为事件循环是浏览器的内部实现,对外来讲是一个黑盒。
有了上面的认识下,我们看一下这段React代码:
import React, { useRef, useEffect, useLayout } from 'react';
export default function App(){
let ball1 = useRef(null);
let ball2 = useRef(null);
useEffect(
() => {
if (ball1.current){
ball1.current.style.left = `200px`;
}
},
[]
)
useLayoutEffect(
() => {
if (ball2.current){
ball2.current.style.left = `200px`;
}
},
[]
)
return <>
<h1>useEffect</h1>
<div className='ball1' ref={ball1}></div>
<h1>useLayoutEffect</h1>
<div className='ball2' ref={ball2}></div>
</>
}
ball1、ball2的样式如下:
.ball1, .ball2 {
width: 100px;
height: 100px;
box-sizing: border-box;
background-color: cornflowerblue;
transition: left 10s;
position: relative;
left: 0;
}
.ball1 {
background-color: green;
}
在执行上面这段代码之前,为了能够明显的看出差异,我们手动调整下cpu的运行效率,如下:
从英文名字上也能看出来,我们手动将cpu的运行效率调整为原先的1/6。
此时执行一下我们的代码,效果如下:
在mount阶段,为啥会出现这种现象?为啥useEffect会有过渡效果,而useLayoutEffect没有?
上面这个问题本质上是问你,useEffect、useLayoutEffect、appendChild之间的执行顺序。
不管你是否读过源码,如果读过更好,你在useEffect、useLayoutEffect里打个断点,然后在node_modules下的react-dom的包里,全局搜索下appendChild
,你会发现,它们三个其实都运行在commitRootImpl函数里。并且代码写法顺序如下:
useEffect();
document.body.appendChild();
useLayoutEffect();
那为啥在这段代码里,useEffect会有过渡效果呢?
我们知道useEffect是异步运行的,在js里,setTimeout、MessageChannel等等是能够实现这个需求的,而这些也是useEffect的底层实现。
我们来看下这段html代码:
<button onclick="changePosition1()">useLayoutEffect</button>
<script>
function changePosition1(){
let dom = document.createElement('div');
dom.appendChild(document.createTextNode('useLayoutEffect'));
dom.classList.add('div1');
document.body.append(dom);
dom.style.left = '100px';
}
</script>
我们运行下这段代码,点击useLayoutEffect
,我们会发现,页面上立即出现了距离左侧100px的div。
但是如果修改下代码,添加一些耗时操作呢,如下:
function changePosition1(){
let dom = document.createElement('div');
dom.appendChild(document.createTextNode('useLayoutEffect'));
dom.classList.add('div1');
document.body.append(dom);
let i = 0;
while(i < 1000000000){
i++;
}
dom.style.left = '100px';
}
我们运行后发现,虽然点击useLayoutEffect
后也出现了距离左侧100px的div,但是明显延迟了,有点卡顿的感觉。
反应过来了吗,这就是useLayoutEffect,append后面的操作都是useLayoutEffect里的callback,如果非要找对标的话,你可以把useLayoutEffect
比作requestAnimationFrame
,都是会在浏览器执行渲染前,同步去做一些事情。这也是为啥官方不建议你在这里做一些耗时操作的原因,因为useLayoutEffect
的执行必然发生在当前事件循环里,这会让主线程占据了太多的时间。
我们再看下useEffect,代码如下:
<style>
.div2 {
width: 150px;
height: 150px;
position: relative;
left: 0;
background: green;
}
</style>
<button onclick="changePosition2()">useEffect</button>
function changePosition2(){
setTimeout(
function (){
let i = 0;
while(i < 1000000000){
i++;
}
dom.style.left = '100px';
}
)
let dom = document.createElement('div');
dom.appendChild(document.createTextNode('useEffect'));
dom.classList.add('div2');
document.body.append(dom);
}
当我们点击useEffect
后,页面上会立即出现div,然后div开始向右移动,并伴随过渡效果。
这就是useEffect,它的执行必然不在当前事件循环里。如果非要找对标的话,从渲染与代码执行的顺序角度看的话,useEffect
与requestIdleCallback
倒是几分相似。
好啦,本期内容到这里也就结束啦,大概率这篇文章不会被平台推荐,因为行文风格是平台不太中意的,如果能够帮到你,真是万分荣幸。
那么,我们下期再见啦,拜拜~~
转载自:https://juejin.cn/post/7403658547718520851