强话一波hooks,这次咱们换个发力点
点击上方 前端瓶子君,关注公众号
回复算法,加入前端编程面试算法每日一题群
首先hooks
已经推出很久,想必大家或多或少都使用过或者了解过hooks
,不知是否会和我一样都有一种感受,那就是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
的到来之后,橘势就不一样了,这个曾经的“render
”函数一下就走起来了。
hooks帮函数组件打碎了基因锁。
我们之前聊了,函数组件最大的硬伤就是"次次重来,无法延续" ,很难让它具备跟类组件那样的能力,比如用状态和生命周期函数,而如今hooks
的加持,很好的粉碎了被类组件克制的枷锁。
所以说在了解如何使用hooks
之前,最好要先了解函数组件是怎么拥有了延续性,这样使用hooks
就”有谱“,否则你就会觉得hooks
到处都是黑魔法,这么整就不是很”靠谱“了。
想要了解Hooks延续的奥秘,你可能得认识一下Fiber
没有延续性,遑论其他,真正让函数组件有延续性的幕后真大佬实际上是Fiber
,为了能够很好的了解React怎么实现的这么多种hooks
,那么Fiber
你是绕不开的,不过学习Fiber
不用太用力,点到为止,我会尽可能的浅出,我们的目标就是能够更好的理解和使用Hooks
,毕竟吃饺子嘛,不用非得那么清楚怎么做的。
fiber 的结构
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,...}
复制代码
Fiber 的由来
React到底是如何将项目渲染出来的。
首先这个过程称为“reconciler”,可以先粗略讲reconciler划分出两个阶段。
reconciliation :通过diff获得变动的结果。
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
,大体如图:
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
的流转过程,简单描述下:
自顶
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
只会关注有改动的节点,并且从最深处往前排列,这也就对应上了,刷新顺序是子节点到父节点。
双fiber树就是问题关键
有两个阶段:
首次渲染:直接先把
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组件相媲美的前提基础。
hooks整的那些活儿
了解一下capture value以及闭包陷阱
capture value
顾名思义,“捕获的的值”,函数组件执行一次就会产生一个闭包,就好像一个快照, 这跟我们上面分析说的“关联render结果”或者“那一刻快照”呼应上了。
当capture value
遇上hooks
出现了因使用“过期快照”而产生的问题,那就称为闭包陷阱。
不过叫什么不重要,归根节点都是“过期闭包”的问题,而在useEffect
中的暴露的问题最为明显。
先举个
强话一波hooks,这次咱们换个发力点相关推荐
- 强化一波 hooks,这次咱们换个发力点
大厂技术 高级前端 Node进阶 点击上方 程序员成长指北,关注公众号 回复1,加入高级Node交流群 首先hooks已经推出很久,想必大家或多或少都使用过或者了解过hooks,不知是否会和我一样 ...
- foxmail皮肤_Foxmail 6.5正式版 可以换肤发明信片
Foxmail 6.5正式版 可以换肤发明信片 2009年06月29日 13:28作者:陈涛编辑:陈涛文章出处:泡泡网原创 分享 Foxmail邮件客户端6.5正式版发布了,正在使用Foxmail的用 ...
- 分众传媒天天挂在嘴边的“饱和攻击”,原来只是最强话术
前不久在商场看到一个很"奇葩"的事情,在一个办事写字楼地下停车场一层的电梯口,分众传媒安装了7个终端.这么小一块地方,还在停车场,这样做合理吗?不浪费? 分众传媒的这种点位布置方式 ...
- i58400升级可以换什么cpu_为什么明星经常换发型发质还那么好?只要学会这一点,你也可以...
大家应该都很好奇,为什么娱乐圈很多女明星经常换发型,不是染就是烫,但是发质依旧那么好.其实,就算天生的发质再好,也经不起频繁折腾,那些发质好的女星只不过是日常注重护理头发罢了,只要你也跟她们一样用心护 ...
- Python水仙花数,鸡兔同笼问题,百钱买百鸡问题,斐波那契数列,模拟发微信红包
一.题目: 1.求50以内能被7整除,但不能同时被5整除的所有整数. 2.如果一个3位数的各位数字的立方和等于该数自身,则该数称为"水仙花数". 例如,153 = 13 + 53 ...
- 手游换皮发海外,如果规避游戏侵权风险?
原创: 约翰魏 约翰魏 1周前 换皮+买量成为中国手游出海的主力打法之一,中国手游企业出海优势明显,集中表现在以下几个方面: 手游运营经验丰富,懂得用户需求,尤其在变现和维护大R方面经验碾压国外 ...
- 要过年了,换个发微信红包新姿势
苏生不惑第215篇原创文章,将本公众号设为星标,第一时间看最新文章. 关于微信之前写过好几篇文章了: 你刚才微信上撤回了什么?我都看到了 微信支付分开通了,来看看你有多少分 c 盘空间又满了?微信清理 ...
- R 语言ggplot 换颜色-发文章用的sci 色卡
那当然就是ggsci 包啦 包含多种选择,先上图: Nature Publishing Group Journal of Clinical Oncology 还有很多选择,只要在ggplot2 基础上 ...
- 移 动 通 信 滤 波 器 技 术(转)
[摘 要 ] 本 文 从 现 代 移 动 通 信 技 术 和 材 料 科 学 工 程 相 互 促 进 发 展 的 角 度 , 综 述 了 固 态 化 无 源 滤 波 器 件 的 产 生 和 发 展 进 ...
最新文章
- Codeforces 611D New Year and Ancient Prophecy DP
- Spring JdbcTemplate小结
- html文件上传数量限制,使用HTML中的input上传文件最多可以上传多少张?
- win7装xp双系统_联智通达什么系统装工控电脑好_搜狐汽车
- 方向向量转欧拉角_欧拉角、旋转向量和旋转矩阵的相互转换
- 产生随机数java_java产生随机数的几种方式
- python的字符串删除操作 有点简单
- git commit 规范校验配置和版本发布配置
- 深入搜索引擎——海量信息的压缩、索引和查询
- 10分钟教你写个商业计划书
- CSDN刷博 - 最简单有效的方法
- ssis sql oracle,[SQL][SSIS]透過 SSIS 連接 Oracle 的資料庫
- np.linspace函数用法
- css3做的好看的小便签,纯CSS3 便签条折角效果
- 了解计算机的配置及价格行情,最新电脑配置清单及价格的详细介绍
- 小勇机器人如何绑定_App Store 上的“小勇机器人”
- 数据分析——帆软report
- 酒浓码浓 - HTML5微数据/itemscope/itemtype/itemprop
- 移位寄存器SHIFT RAM IP之模拟图像卷积
- 【玩转c++】多态深度刨析
热门文章
- bash: 未预期的符号“newline”附近有语法错误
- 今天我们来谈谈【像素流送】到底是什么?!
- matlab 更改jdk版本,程序员怎么修改微信号
- win7安装好系统没网络连接网络连接网络连接服务器,Win7系统网络连接正常却不能上网怎么处理...
- 论思维能力的锻炼(6-12)
- 基于java的宠物用品店系统
- 一张图看懂黄奇帆对房地产的结构化分析
- codeforces problem 140E New Year Garland
- element ui 表格,通过下载按钮下载生成Excel表格
- 这个618别错过、值得入手的数码好物推荐