当前位置:   article > 正文

强话一波hooks,这次咱们换个发力点_hooks 动态生成 dom 耗时长

hooks 动态生成 dom 耗时长

首先hooks已经推出很久,想必大家或多或少都使用过或者了解过hooks,不知是否会和我一样都有一种感受,那就是hooks使用起来很简单,但总感觉像是一种魔法,并不是很清楚其内部如何实现的,很难得心应手,所以我觉得要想真正驾驭hooks,应该先从了解其内部原理开始,再讲使用,试着建立从原理到使用的一条细细的通路。

hooks扭转了函数组件的橘势

hooks 之前

函数组件的基因限制

函数组件可以粗略的认为就是类组件的render函数,即一个返回jsx从而创建虚拟dom的函数。

类组件有this,能够拥有自己的实例方法,变量,这样很容易就可以实现各种特性,比如state和生命周期函数,每一次渲染都可以认为是“曾经"的自己在不断脱变,有延续性。

反观函数组件就无法延续,每一次渲染都是“新”的自己,这就是函数组件的“基因限制”,有点像章鱼。

函数组件和类组件一个“小差异”

首先一个组件可以分别用类组件和函数组件写出两个版本,对吧

类组件:

  1. class CompClass extends Component {
  2. showMessage = () => {
  3. console.log("点击的这一刻,props中info为 " + this.props.info);
  4. };
  5. handleClick = () => {
  6. setTimeout(this.showMessage, 3000);
  7. console.log(`当前props中的info为${this.props.info},一致就说明准确的关联到了此时的render结果`)
  8. };
  9. render() {
  10. return <div onClick={this.handleClick}>
  11. <div>点击类组件</div>
  12. </div>;
  13. }
  14. }

函数组件:

  1. function CompFunction(props) {
  2. const showMessage = () => {
  3. console.log("点击的这一刻,props中info为 " + props.info);
  4. };
  5. const handleClick = () => {
  6. setTimeout(showMessage, 3000);
  7. console.log(`当前props中的info为${props.info},一致就说明准确的关联到了此时的 render结果`)
  8. };
  9. return <div onClick={handleClick}>点击函数组件</div>;
  10. }

那也就说这两者不同写法是等价的,对么?

答案是:通常情况下是等价的,但是有种情况二者不同,比如

  1. export default function App() {
  2. const [info, setInfo] = useState(0);
  3. return (
  4. <div>
  5. <div onClick={()=>{
  6. setInfo(info+1)
  7. }}>父组件的info信息>> {info}</div>
  8. <CompFunction info = {info}></CompFunction>
  9. <CompClass info = {info}></CompClass>
  10. </div>
  11. );
  12. }

通过代码能够看出:

  1. 在组件App中,有个状态info其初始值为0,并且可以通过点击修改

  2. CompFunctionCompClass是作为子组件显示,并且都接受父组件的info作为参数,

  3. 这两个组件都有一个点击回调,点击之后都会触发一个延迟3秒的setTimeout,然后把从父组件App中获得infolog出来

那就操作一下:

  1. 就是快速点击CompFunctionCompClass,以触发其内部的setTimeout,等待3秒之后,看看打印从父组件App中获得info信息

  2. 然后再点击父组件进而修改info,只要变了就行,假设变成了5。

(建议动手试一下。)

结果:

  1. 函数组件CompFunction会输出:0

  2. 类组件CompClass会输出:5

结果不同,按道理讲应该等价啊,为什么不同呢?

解释:

函数组件执行,就会形成一个闭包,可以形象地说成render结果,其中包括props,而点击事件的处理函数同样也包括在内,那它无论是立即执行还是延迟执行,都应该与触发执行的那一刻的render结果(你也可以理解为那一刻的快照)相关联。所以回调函数showMessage所应该log出的info,应该为事件触发的那一刻render结果中的info,也就是"1",无论外部的info怎么变。

而类组件就会输出info的最新值,也就是"5"。

结论:

这个“小差异”就叫做capture value

每次 Render 的内容都会形成一个快照并保留下来,因此当状态变更而 Rerender 时,就形成了 N 个 Render 状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State。[1]

class组件想做到这一点,多少有点难,毕竟this这个奶酪被React给动了。

capture value是一把双刃剑,不过没关系有办法解决(后面会讲)

hooks 之后

hooks让这个“render”函数成精了

如果说在hooks之前,函数组件有一些“硬伤”,其独特之处不足以支撑它与类组件分庭抗礼,但是当hooks的到来之后,橘势就不一样了,这个曾经的“render”函数一下就走起来了。

hooks帮函数组件打碎了基因锁。

我们之前聊了,函数组件最大的硬伤就是"次次重来,无法延续" ,很难让它具备跟类组件那样的能力,比如用状态和生命周期函数,而如今hooks的加持,很好的粉碎了被类组件克制的枷锁。

所以说在了解如何使用hooks之前,最好要先了解函数组件是怎么拥有了延续性,这样使用hooks就”有谱“,否则你就会觉得hooks到处都是黑魔法,这么整就不是很”靠谱“了。

 想要了解Hooks延续的奥秘,你可能得认识一下Fiber

没有延续性,遑论其他,真正让函数组件有延续性的幕后真大佬实际上是Fiber,为了能够很好的了解React怎么实现的这么多种hooks,那么Fiber你是绕不开的,不过学习Fiber不用太用力,点到为止,我会尽可能的浅出,我们的目标就是能够更好的理解和使用Hooks,毕竟吃饺子嘛,不用非得那么清楚怎么做的。

fiber 的结构

  1. type Fiber = {
  2. // 函数组件记录以链表形式存放的hooks信息,类组件存放`state`信息
  3. memoizedState: any,
  4. // 将diff得出的结果提交给的那个节点
  5. return: Fiber | null,
  6. // 单链表结构 child:子节点,sibling:兄弟节点
  7. child: Fiber | null,
  8. sibling: Fiber | null,
  9. ...
  10. // 每个workinprogress都维护了一个effect list(很复杂,不会也不耽误我们吃饺子)
  11. nextEffect: Fiber | null,
  12. firstEffect: Fiber | null,
  13. lastEffect: Fiber | null,
  14. ...
  15. }

Fiber 的由来

React到底是如何将项目渲染出来的。

首先这个过程称为“reconciler”,可以先粗略讲reconciler划分出两个阶段。

  1. reconciliation :通过diff获得变动的结果。

  2. commit:将变动作用到画面上(side effect即副作用,如dom操作)。

reconciliation是异步的,commit是同步的。

在fiber之前,React是如何实现的reconciliation

从头创建一个新的虚拟dom即vdom,与旧的vdom进行比对,从而得出diff结果,这个过程是递归,需要一气呵成,不能停的,这样JavaScript长时间的占用主线程,就会阻塞画面的渲染,就很卡。

因为JavaScript在浏览器的主线程上运行,恰好与样式计算、布局以及许多情况下的绘制一起运行。如果JavaScript运行时间过长,就会阻塞这些其他工作,可能导致掉帧。

(引自Optimize JavaScript Execution[2])

那么可以说,旧的方式暴露了两点问题:

  • 自顶向下遍历,不能停。

  • React长时间的执行耽误了浏览器工作。

vdom进化成为Fiber

Fiber可以理解为将上述整个reconciliation工作拆分了,然后通过链表串了起来,变成了一个个可以中断/挂起/恢复的任务单元。并且结合浏览器提供的requestIdleCallback API(有兴趣可以了解)进行协同合作。

Fiber核心是实现了一个基于优先级和requestIdleCallback的循环任务调度算法。(参考:fiber-reconciler[3])

 

直白的说:就一碗面条,一双筷子,以前React吃的时候,浏览器只能看着,现在就变成React吃一口换浏览器吃一口,一下就和谐了。

Fiber就是按照vdom来拆分的,一个vdom节点对应一个Fiber节点,最后形成一个链表结构的fiber tree,大体如图:

Image 

child:指向子节点的指针 sibling:指向兄弟节点指针 return:提交变动结果(effectList)到指定的目标节点(图中没标示,下文会有动态演示)

所以说Fiber tree就是可切片的vdom tree都不为过。

那么vdom还存在么?

这个问题我思考了很久,请原谅这方面的源码我还没看透,我现在通过查阅多篇相关的文章,得出了一个我能接受,逻辑能自洽的解释:

Fiber出来之后,vdom的作用只是作为蓝本进行构建Fiber树。

em~,龙珠熟悉吧,vdom就好像是超级赛亚人1之前够用了,现在不行了,进化到了超级赛亚人2,即Fiber

Fiber是如何工作的

首先我已经知道,Fiber tree是一个链表结构,React是通过循环处理每个Fiber工作单元,在一段时间后再交还控制权给浏览器,从而协同的合作,让页面变得更加流畅。

要弄清函数组件怎么有的延续性的答案就藏在了这个工作循环中。

探索一下workLoop

为了能够摆脱又困又长的源码分析,可以试着先简单的理解workLoop

首先Loop啥呢?

工作单元,即work

work又可以粗略的分为:

  • beginWork:开始工作

  • completeWork:完成工作

那么结合之前的Fiber tree,看一下

那么看下大体的运转过程:

 

那么通过动画我初步了解了整个workLoop的流转过程,简单描述下:

  1. 自顶root向下,流转子节点b1

  2. b1开始beginWork,工作目标根据情况diff处理,获得变动结果(effectList),然后判断是是否有子节点,没有那结束工作completeWork,然后流转到兄弟节点b2

  3. b2开始工作,然后判断有子节点c1,那就流转到c1

  4. c1工作完了,completeWork获得effectList,并提交给b2

  5. 然后b2完成工作,流转给b3,那么b3就按照这套路子,往下执行了,最后执行到了最底部d2

  6. 最后随着光标的路线,一路整合各节点的effectList,最后抵达Root节点,第1阶段-reconciliation结束,准备进入Commit阶段

再进一步,“延续”的答案就快浮出水面了

我们已经大致的了解了workLoop,但还不能解释函数组件怎么“延续”的,我们还要再深入了解,那么再细致一点分解workLoop,实际上是这样的:

 

(动画中“current”和“备用”是一体,为了看起来容易理解:“构建wip树是尽可能服用current树”,动画结束时,current再用备用来描述,以表达current树是作为备用的)

描述一下过程:

  1. 根据current fiber treeclone出workinProgress fiber tree,每clone一个workinProgress fiber都会尽可能的复用备用fiber节点(曾经的current fiber

  2. 当构建完整个workinProgress fiber tree的时候,current fiber tree就会退下去,作为备用fiber节点树,然后workinProgress fiber tree就会扶正,成为新的current fiber tree

  3. 然后就将已收集完变动结果(effect list)的新current fiber tree,送去commit阶段,从而更新画面

其中几个点我要注意:

  • current fiber tree为主决定屏幕上显示内容,workinProgress fiber tree为辅制作完毕成为下一个current fiber tree

  • 构建workinProgress fiber tree的过程,就是diff的过程,主要的工作都是发生在workinProgress fiber上,有变动就会维护一个effect list,当完成工作的时候就会提交格给return所指向的节点。

  • 要退位的current fiber tree作为备用,充当了构建workinProgress fiber tree的原料,最大程度节约了性能,这样周而复始,。

  • 收集到的effect list只会关注有改动的节点,并且从最深处往前排列,这也就对应上了,刷新顺序是子节点到父节点。

双fiber树就是问题关键

有两个阶段:

  • 首次渲染:直接先把current fiber tree构建出来

  • 更新渲染:延续current fiber tree构建workinProgress fiber tree

蜕变之中必有延续

更新阶段,两棵fiber树如双生一般,current fiberworkinProgress fiber之间用alternate这个指针进行了关联,也就是说,可以在处理workinProgress fiber工作的时候,能够获得current fiber的信息,除非是全新的,那就重新创建。

每构建一个workinProgress fiber,如果这个fiber对应的节点是一个函数组件,并且可以通过alternate获得current fiber,那么就进行延续,承载延续的精华的便是current fibermemoizedState这个属性

延续的精华尽在memoizedState

首次渲染时

依次执行我们在函数组件的hooks,每执行一个种类hooks,都会创建一个对应该种类的hook对象,用来保存信息。

  • useState 对应 state信息

  • useEffect 对应 effect对象

  • useMemo 对应 缓存的值和deps

  • useRef 对应 ref对象

  • ...

这些信息都会以链表的形式保存在current fibermemoizedState

更新渲染时

每次构建对应的是函数组件workinProgress fiber时,都会从对应的current fiber中延续这个以链表结构存储的hooks信息

如该函数组件:

  1. export default function Test() {
  2. const [info1, setInfo1] = useState(0);
  3. useEffect(() => {}, [info1]);
  4. const ref = useRef();
  5. const [info2, setInfo2] = useState(0);
  6. const [info3, setInfo3] = useState(0);
  7. return (
  8. <div>
  9. <div ref={ref}> {`${info1}${info2}${info3}`}</div>
  10. </div>
  11. );
  12. }

 那么hooks的延续就如下图这样:

通过链表的顺序去延续,如果其中的一个hooks写在条件语句中,代码如下:

  1. export default function Test() {
  2. const [info1, setInfo1] = useState(0);
  3. let ref;
  4. useEffect(() => {
  5. setInfo1(info1+1)
  6. }, [info1]);
  7. if(info1==0){
  8. ref = useRef();
  9. }
  10. const [info2, setInfo2] = useState(0);
  11. const [info3, setInfo3] = useState(0);
  12. return (
  13. <div>
  14. <div ref={ref}> {`${info1}${info2}${info3}`}</div>
  15. </div>
  16. );
  17. }

 那么就会破坏延续的顺序,获得信息就会驴唇不对马嘴,就像这样:

所以这就是不能把hooks写在条件语句中的原因

而这就是Hooks能够延续的奥秘,作为支撑其实现各种功能,从而与class组件相媲美的前提基础。

hooks整的那些活儿

了解一下capture value以及闭包陷阱

capture value顾名思义,“捕获的的值”,函数组件执行一次就会产生一个闭包,就好像一个快照, 这跟我们上面分析说的“关联render结果”或者“那一刻快照”呼应上了。

capture value遇上hooks出现了因使用“过期快照”而产生的问题,那就称为闭包陷阱

不过叫什么不重要,归根节点都是“过期闭包”的问题,而在useEffect中的暴露的问题最为明显。

先举个声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】

推荐阅读
相关标签