为什么一定要useEffect?
之前看到过,说不要把副作用直接写在函数式组件的函数体内,需要用useEffect把副作用包裹起来。我当时的理解就是,函数组件重新渲染的话,整个函数都会被执行一遍。如果我就想让一个副作用,每次组件渲染都重新执行,为什么一定要放在useEffect里面?
太长不看:为了不阻塞界面。(不过好像react18的新特性,useEffect也会阻塞了)
useEffect在界面绘制完毕后执行
后来我偶然看到一句话,意思是说useEffect里面的副作用,是在渲染完毕之后才执行的。那问题又来了啊,我如果要在useEffect里面去改state咋办,那你渲染完了我再改state,那不是又要重新渲染?为啥不在渲染之前执行呢?
这就涉及到react渲染机制的问题了。我们可以简单地把react的渲染分成两个阶段:render和commit。其中render就是创造vnode(也就是执行类组件的render方法,或者执行函数组件的函数体,去得到vnode),并且比较新旧vnode(其实是vnode转化的fiber tree)的过程。而commit就是将改动提交,将vnode转成真实dom,并且将真实dom绘制出来的过程,涉及到了浏览器的重排。
而为了不阻塞界面的渲染(用户感觉界面卡顿),useEffect就被放到了界面渲染完毕后执行,至少在react16中是这样的。不过这样就会导致前面提到的问题,在useEffect中修改state,会导致连续两次的渲染(浏览器两次重新绘制),可能出现闪屏的情况,因为第一次渲染的结果可能刚出现,就要被第二次渲染的结果替代掉。
如果有这种情况,或者需要在useEffect中获取最新dom信息,可以使用useLayoutEffect这个hook,这个hook和useEffect的唯一区别就是,useLayoutEffect是同步执行的。它的执行是在,得到真实dom之后 绘制到浏览器上之前。等到useLayoutEffect执行完毕之后,浏览器才开始绘制。并且如果执行useLayoutEffect的时候更改了state,浏览器会中止第一次绘制,直接去等第二次绘制的结果。我们用下面的代码,结合视频中界面的变化和控制台的输出,来更好地理解。
1 | const App = () => { |
代码的逻辑是,每次点击,都先将value设为0,再将value设为一个随机值。value的两次变化会导致界面被渲染两次。我们通过插入多个2秒的无条件循环,来拉长整个过程。我们可以发现,当useLayoutEffect执行完毕的时候,界面立刻发生了变化,之后才执行useEffect。也就是说,useEffect确实是发生在界面完全渲染之后的。
React18的新特性
然而事情并没有这么简单。React18的新特性,疑似将useEffect的执行时机也提到了渲染之前。如下面视频所示,同样的一段代码在React18中有着完全不同的表现。
我们发现,在useEffect执行完毕之前,界面都没有任何变化。说明useEffect也阻塞了界面的渲染。不过和useLayoutEffect不同的是,useEffect虽然阻塞了渲染,却并不会因为更改了state,让浏览器中止第一次绘制,我们还是能看到中间value为0的状态。
总结
React16中,渲染流程的执行顺序是,
函数组件函数体->useLayoutEffect(可阻塞并打断绘制)>浏览器绘制->useEffect
React18中,渲染流程的执行顺序是,
函数组件函数体->useLayoutEffect(可阻塞并打断绘制)->useEffect(可阻塞,但不能打断绘制)->浏览器绘制
以上为React初学者对React的自行探究,如果错误还望指出