赞
踩
首先hooks
已经推出很久,想必大家或多或少都使用过或者了解过hooks
,不知是否会和我一样都有一种感受,那就是hooks
使用起来很简单,但总感觉像是一种魔法,并不是很清楚其内部如何实现的,很难得心应手,所以我觉得要想真正驾驭hooks
,应该先从了解其内部原理开始,再讲使用,试着建立从原理到使用的一条细细的通路。
函数组件可以粗略的认为就是类组件的render
函数,即一个返回jsx
从而创建虚拟dom
的函数。
类组件有this
,能够拥有自己的实例方法,变量,这样很容易就可以实现各种特性,比如state
和生命周期函数,每一次渲染都可以认为是“曾经"的自己在不断脱变,有延续性。
反观函数组件就无法延续,每一次渲染都是“新”的自己,这就是函数组件的“基因限制”,有点像章鱼。
首先一个组件可以分别用类组件和函数组件写出两个版本,对吧
类组件:
- class CompClass extends Component {
-
- showMessage = () => {
- console.log("点击的这一刻,props中info为 " + this.props.info);
- };
-
- handleClick = () => {
- setTimeout(this.showMessage, 3000);
- console.log(`当前props中的info为${this.props.info},一致就说明准确的关联到了此时的render结果`)
- };
-
- render() {
- return <div onClick={this.handleClick}>
- <div>点击类组件</div>
- </div>;
- }
- }
函数组件:
- function CompFunction(props) {
-
- const showMessage = () => {
- console.log("点击的这一刻,props中info为 " + props.info);
- };
-
- const handleClick = () => {
- setTimeout(showMessage, 3000);
- console.log(`当前props中的info为${props.info},一致就说明准确的关联到了此时的 render结果`)
- };
-
- return <div onClick={handleClick}>点击函数组件</div>;
- }
那也就说这两者不同写法是等价的,对么?
答案是:通常情况下是等价的,但是有种情况二者不同,比如
- export default function App() {
- const [info, setInfo] = useState(0);
- return (
- <div>
- <div onClick={()=>{
- setInfo(info+1)
- }}>父组件的info信息>> {info}</div>
- <CompFunction info = {info}></CompFunction>
- <CompClass info = {info}></CompClass>
- </div>
- );
- }
通过代码能够看出:
在组件App
中,有个状态info
其初始值为0,并且可以通过点击修改
CompFunction
和CompClass
是作为子组件显示,并且都接受父组件的info
作为参数,
这两个组件都有一个点击回调,点击之后都会触发一个延迟3秒的setTimeout
,然后把从父组件App
中获得info
,log
出来
那就操作一下:
就是快速点击CompFunction
和CompClass
,以触发其内部的setTimeout
,等待3秒之后,看看打印从父组件App
中获得info
信息
然后再点击父组件进而修改info
,只要变了就行,假设变成了5。
(建议动手试一下。)
结果:
函数组件CompFunction
会输出:0
类组件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
之前,最好要先了解函数组件是怎么拥有了延续性,这样使用hooks
就”有谱“,否则你就会觉得hooks
到处都是黑魔法,这么整就不是很”靠谱“了。
没有延续性,遑论其他,真正让函数组件有延续性的幕后真大佬实际上是Fiber
,为了能够很好的了解React怎么实现的这么多种hooks
,那么Fiber
你是绕不开的,不过学习Fiber
不用太用力,点到为止,我会尽可能的浅出,我们的目标就是能够更好的理解和使用Hooks
,毕竟吃饺子嘛,不用非得那么清楚怎么做的。
- type Fiber = {
-
- // 函数组件记录以链表形式存放的hooks信息,类组件存放`state`信息
- memoizedState: any,
-
- // 将diff得出的结果提交给的那个节点
-
- return: Fiber | null,
-
- // 单链表结构 child:子节点,sibling:兄弟节点
-
- child: Fiber | null,
-
- sibling: Fiber | null,
-
- ...
-
-
- // 每个workinprogress都维护了一个effect list(很复杂,不会也不耽误我们吃饺子)
-
- nextEffect: Fiber | null,
-
- firstEffect: Fiber | null,
-
- lastEffect: Fiber | null,
-
- ...
-
- }
React到底是如何将项目渲染出来的。
首先这个过程称为“reconciler”,可以先粗略讲reconciler划分出两个阶段。
reconciliation :通过diff获得变动的结果。
commit:将变动作用到画面上(side effect
即副作用,如dom
操作)。
reconciliation
是异步的,commit
是同步的。
从头创建一个新的虚拟dom即vdom
,与旧的vdom
进行比对,从而得出diff
结果,这个过程是递归,需要一气呵成,不能停的,这样JavaScript长时间的占用主线程,就会阻塞画面的渲染,就很卡。
因为JavaScript在浏览器的主线程上运行,恰好与样式计算、布局以及许多情况下的绘制一起运行。如果JavaScript运行时间过长,就会阻塞这些其他工作,可能导致掉帧。
(引自Optimize JavaScript Execution[2])
那么可以说,旧的方式暴露了两点问题:
自顶向下遍历,不能停。
React长时间的执行耽误了浏览器工作。
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 tree
是一个链表结构,React是通过循环处理每个Fiber
工作单元,在一段时间后再交还控制权给浏览器,从而协同的合作,让页面变得更加流畅。
要弄清函数组件怎么有的延续性的答案就藏在了这个工作循环中。
为了能够摆脱又困又长的源码分析,可以试着先简单的理解workLoop
。
首先Loop啥呢?
工作单元,即work
。
work
又可以粗略的分为:
beginWork:开始工作
completeWork:完成工作
那么结合之前的Fiber tree,看一下
那么看下大体的运转过程:
那么通过动画我初步了解了整个workLoop
的流转过程,简单描述下:
自顶root
向下,流转子节点b1
b1开始beginWork
,工作目标根据情况diff处理,获得变动结果(effectList
),然后判断是是否有子节点,没有那结束工作completeWork
,然后流转到兄弟节点b2
b2
开始工作,然后判断有子节点c1
,那就流转到c1
c1
工作完了,completeWork
获得effectList
,并提交给b2
然后b2
完成工作,流转给b3
,那么b3
就按照这套路子,往下执行了,最后执行到了最底部d2
最后随着光标的路线,一路整合各节点的effectList
,最后抵达Root
节点,第1阶段-reconciliation
结束,准备进入Commit
阶段
我们已经大致的了解了workLoop
,但还不能解释函数组件怎么“延续”的,我们还要再深入了解,那么再细致一点分解workLoop
,实际上是这样的:
(动画中“current”和“备用”是一体,为了看起来容易理解:“构建wip树是尽可能服用current树”,动画结束时,current再用备用来描述,以表达current树是作为备用的)
描述一下过程:
根据current fiber tree
clone出workinProgress fiber tree
,每clone一个workinProgress fiber
都会尽可能的复用备用fiber
节点(曾经的current fiber
)
当构建完整个workinProgress fiber tree
的时候,current fiber tree
就会退下去,作为备用fiber
节点树,然后workinProgress fiber tree
就会扶正,成为新的current fiber tree
然后就将已收集完变动结果(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
只会关注有改动的节点,并且从最深处往前排列,这也就对应上了,刷新顺序是子节点到父节点。
有两个阶段:
首次渲染:直接先把current fiber tree
构建出来
更新渲染:延续current fiber tree
构建workinProgress fiber tree
更新阶段,两棵fiber
树如双生一般,current fiber
与workinProgress fiber
之间用alternate
这个指针进行了关联,也就是说,可以在处理workinProgress fiber
工作的时候,能够获得current fiber
的信息,除非是全新的,那就重新创建。
每构建一个workinProgress fiber
,如果这个fiber
对应的节点是一个函数组件,并且可以通过alternate
获得current fiber
,那么就进行延续,承载延续的精华的便是current fiber
的memoizedState
这个属性
memoizedState
首次渲染时
依次执行我们在函数组件的hooks
,每执行一个种类hooks
,都会创建一个对应该种类的hook
对象,用来保存信息。
useState 对应 state信息
useEffect 对应 effect对象
useMemo 对应 缓存的值和deps
useRef 对应 ref对象
...
这些信息都会以链表的形式保存在current fiber
的memoizedState
中
更新渲染时
每次构建对应的是函数组件的workinProgress fiber
时,都会从对应的current fiber
中延续这个以链表结构存储的hooks信息。
如该函数组件:
- export default function Test() {
- const [info1, setInfo1] = useState(0);
- useEffect(() => {}, [info1]);
- const ref = useRef();
- const [info2, setInfo2] = useState(0);
- const [info3, setInfo3] = useState(0);
- return (
- <div>
- <div ref={ref}> {`${info1}${info2}${info3}`}</div>
- </div>
- );
- }
那么hooks
的延续就如下图这样:
通过链表的顺序去延续,如果其中的一个hooks
写在条件语句中,代码如下:
- export default function Test() {
- const [info1, setInfo1] = useState(0);
- let ref;
- useEffect(() => {
- setInfo1(info1+1)
- }, [info1]);
- if(info1==0){
- ref = useRef();
- }
- const [info2, setInfo2] = useState(0);
- const [info3, setInfo3] = useState(0);
- return (
- <div>
- <div ref={ref}> {`${info1}${info2}${info3}`}</div>
- </div>
- );
- }
那么就会破坏延续的顺序,获得信息就会驴唇不对马嘴,就像这样:
所以这就是不能把hooks
写在条件语句中的原因
而这就是Hooks能够延续的奥秘,作为支撑其实现各种功能,从而与class组件相媲美的前提基础。
capture value
顾名思义,“捕获的的值”,函数组件执行一次就会产生一个闭包,就好像一个快照, 这跟我们上面分析说的“关联render结果”或者“那一刻快照”呼应上了。
当capture value
遇上hooks
出现了因使用“过期快照”而产生的问题,那就称为闭包陷阱。
不过叫什么不重要,归根节点都是“过期闭包”的问题,而在useEffect
中的暴露的问题最为明显。
先举个声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。