点击上方 前端瓶子君,关注公众号

回复算法,加入前端编程面试算法每日一题群

首先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>);
}
复制代码

通过代码能够看出:

  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 的结构

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划分出两个阶段。

  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,看一下

Image

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

Image

那么通过动画我初步了解了整个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,实际上是这样的:

test.gif

(动画中“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信息

如该函数组件:

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的延续就如下图这样:

hooksList.jpg

通过链表的顺序去延续,如果其中的一个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>);
}
复制代码

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

QQ截图20211121210010.jpg

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

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

hooks整的那些活儿

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

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

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

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

先举个

强话一波hooks,这次咱们换个发力点相关推荐

  1. 强化一波 hooks,这次咱们换个发力点

    大厂技术  高级前端  Node进阶 点击上方 程序员成长指北,关注公众号 回复1,加入高级Node交流群 首先hooks已经推出很久,想必大家或多或少都使用过或者了解过hooks,不知是否会和我一样 ...

  2. foxmail皮肤_Foxmail 6.5正式版 可以换肤发明信片

    Foxmail 6.5正式版 可以换肤发明信片 2009年06月29日 13:28作者:陈涛编辑:陈涛文章出处:泡泡网原创 分享 Foxmail邮件客户端6.5正式版发布了,正在使用Foxmail的用 ...

  3. 分众传媒天天挂在嘴边的“饱和攻击”,原来只是最强话术

    前不久在商场看到一个很"奇葩"的事情,在一个办事写字楼地下停车场一层的电梯口,分众传媒安装了7个终端.这么小一块地方,还在停车场,这样做合理吗?不浪费? 分众传媒的这种点位布置方式 ...

  4. i58400升级可以换什么cpu_为什么明星经常换发型发质还那么好?只要学会这一点,你也可以...

    大家应该都很好奇,为什么娱乐圈很多女明星经常换发型,不是染就是烫,但是发质依旧那么好.其实,就算天生的发质再好,也经不起频繁折腾,那些发质好的女星只不过是日常注重护理头发罢了,只要你也跟她们一样用心护 ...

  5. Python水仙花数,鸡兔同笼问题,百钱买百鸡问题,斐波那契数列,模拟发微信红包

    一.题目: 1.求50以内能被7整除,但不能同时被5整除的所有整数. 2.如果一个3位数的各位数字的立方和等于该数自身,则该数称为"水仙花数". 例如,153 = 13 + 53  ...

  6. 手游换皮发海外,如果规避游戏侵权风险?

    原创:  约翰魏  约翰魏  1周前 换皮+买量成为中国手游出海的主力打法之一,中国手游企业出海优势明显,集中表现在以下几个方面: 手游运营经验丰富,懂得用户需求,尤其在变现和维护大R方面经验碾压国外 ...

  7. 要过年了,换个发微信红包新姿势

    苏生不惑第215篇原创文章,将本公众号设为星标,第一时间看最新文章. 关于微信之前写过好几篇文章了: 你刚才微信上撤回了什么?我都看到了 微信支付分开通了,来看看你有多少分 c 盘空间又满了?微信清理 ...

  8. R 语言ggplot 换颜色-发文章用的sci 色卡

    那当然就是ggsci 包啦 包含多种选择,先上图: Nature Publishing Group Journal of Clinical Oncology 还有很多选择,只要在ggplot2 基础上 ...

  9. 移 动 通 信 滤 波 器 技 术(转)

    [摘 要 ] 本 文 从 现 代 移 动 通 信 技 术 和 材 料 科 学 工 程 相 互 促 进 发 展 的 角 度 , 综 述 了 固 态 化 无 源 滤 波 器 件 的 产 生 和 发 展 进 ...

最新文章

  1. Codeforces 611D New Year and Ancient Prophecy DP
  2. Spring JdbcTemplate小结
  3. html文件上传数量限制,使用HTML中的input上传文件最多可以上传多少张?
  4. win7装xp双系统_联智通达什么系统装工控电脑好_搜狐汽车
  5. 方向向量转欧拉角_欧拉角、旋转向量和旋转矩阵的相互转换
  6. 产生随机数java_java产生随机数的几种方式
  7. python的字符串删除操作 有点简单
  8. git commit 规范校验配置和版本发布配置
  9. 深入搜索引擎——海量信息的压缩、索引和查询
  10. 10分钟教你写个商业计划书
  11. CSDN刷博 - 最简单有效的方法
  12. ssis sql oracle,[SQL][SSIS]透過 SSIS 連接 Oracle 的資料庫
  13. np.linspace函数用法
  14. css3做的好看的小便签,纯CSS3 便签条折角效果
  15. 了解计算机的配置及价格行情,最新电脑配置清单及价格的详细介绍
  16. 小勇机器人如何绑定_‎App Store 上的“小勇机器人”
  17. 数据分析——帆软report
  18. 酒浓码浓 - HTML5微数据/itemscope/itemtype/itemprop
  19. 移位寄存器SHIFT RAM IP之模拟图像卷积
  20. 【玩转c++】多态深度刨析

热门文章

  1. bash: 未预期的符号“newline”附近有语法错误
  2. 今天我们来谈谈【像素流送】到底是什么?!
  3. matlab 更改jdk版本,程序员怎么修改微信号
  4. win7安装好系统没网络连接网络连接网络连接服务器,Win7系统网络连接正常却不能上网怎么处理...
  5. 论思维能力的锻炼(6-12)
  6. 基于java的宠物用品店系统
  7. 一张图看懂黄奇帆对房地产的结构化分析
  8. codeforces problem 140E New Year Garland
  9. element ui 表格,通过下载按钮下载生成Excel表格
  10. 这个618别错过、值得入手的数码好物推荐