最近购买了卡颂老师的《从零实现React18》的课程,希望通过这个课程可以学习到react的源码实现细节和加深对框架的理解,同时也购买了老师的书《React设计原理》。目前课程只学习到了12课时,书本也仅仅翻看了前面的几个章节,打算结合目前学习到的知识,梳理一下我对react的理解。
react是应用级框架
在跟着老师写代码的时候,我才很惊讶的发现,原来react的更新永远都是从FiberRootNode开始的,每次更新都是从头开始进行。这一点我是直到跟着老师写代码时才发现。说起来很惭愧,我写react也已经写了有1年半的时间了,但是直到现在才发现这点,这也再一次的提醒我过去的一年多里面自己是过得有多么的懈怠,技术没有一丁点的长进。
我也感觉到很好奇,react这样做性能不会很差吗?我以前是写vue的,vue和react很不同的一点就是它的更新是具体到组件的,其他没有改变的组件是不会触发更新的。我转react的时候就知道一些vue上不需要处理的性能优化,到了react里面是需要开发人员手动的去处理的,不过当时我也没有太理解,只是在用到函数的时候都包裹上一层useCallback
,现在想想之所以会有vue和react这种差异的原因(或者说原因之一)应该就是react每次更新都会遍历整颗树吧。
在看了《React设计原理》后我知道,react框架这种是应用级框架,每次更新时都会从应用的根节点开始遍历整个应用,而vue这种是组件级框架,更新的粒度是组件级的,相对react的更新来说,会减少很多无意义的遍历。
这样看起来,似乎是vue更胜react一筹,但是react是一个重运行时的框架,它的亮点是在优先级调度、时间切片等功能上,不过可惜,这些内容我还没有学习到,所以也还说不出个所以然来。
不过从书上我也大致了解到,react为了提高性能做出的一些努力。一个是突破CPU的瓶颈,减少页面掉帧。受限于架构,react难以AOT获益。为什么呢?
AOT就是预编译,他提供性能的原理就是通过分析描述ui的语法,将描述ui的部分分成静态部分和动态部分,从而可以让框架在更新时可以忽略静态的部分,只在动态的部分计算ui的变化,从而缩短计算出ui变化所需要的时间。这一点vue和Svelte以及Angular的模板语法是具有可操作性的,因为其更容易分析。而react是采用jsx来描述ui的,其相对于模板语法而言,具有很高的灵活性(所以以前写vue的时候,有时也会为了灵活而放弃使用模板语法而使用jsx),也让其很难在预编译时进行静态分析。不过书里也提到了一种叫ReactForget的编译器,这种技术在尝试减少jsx的灵活性,使AOT优化成为可能。
目前我虽然只学习了12个课时,对react的理解也还在一个基础的阶段,但是目前实现的代码也已经可以实现简单的mount和update了,基于我目前学习的代码,我也可以大体上对React进行一些描述了,当然这个描述无疑是残缺的,也可能是存在一些偏差的,毕竟我不是基于源码,而是由老师基于react源码简化的big-react轮子去认识react的。这虽然不够直接,但是也很大程度的简化了我学习源码的难度,毕竟让我去读源码真的太难了,而有了学习的基础,相信以后阅读源码时,理解的难度会大大的降低。
jsx与ReactElement
我们在写代码时,描述ui使用的jsx语法,但是实际上,经过babel编译,代码最终是通过嵌套调用createElement
来构建ui的结构的。createElement
方法返回的结果就是一个ReactElement
对象,ReactElement.props.children
里面又放入了子ReactElement
,从而形成一个树状结构,这个树状结构就是用来描述ui的。
首次渲染
在我们首次渲染应用的时候(例如执行ReactDom.createRoot(element).render(<App/>)
)。会生成应用的根节点FiberRootNode
,以及HostRoot FiberNode
,而FiberRootNode.current
指向了HostRoot FiberNode
(ps: FiberRootNode
和HostRoot FiberNode
这两个节点在一个应用中是唯一的,不过要注意在一个页面中是可以创建多个应用的,此时页面中这两种节点就不唯一(当执行ReactDom.createRoot(element).render(<App/>)
时其实就是创建了一个应用,并将组件挂载到了容器上,一个页面中可以创建多个应用)。)。然后开始从根节点开始进行mount。这个过程简略可以分为:
-
render阶段
- beginWork
- completeWork
-
commit阶段
- beforeMutation(还没学到)
- Mutation
- Layout(还没学到)
后面好像还有schedule阶段,不过这个还没学到,就略过了。
render阶段会遍历整个应用,从FiberRootNode
开始,经历从上到下的递阶段(beginWrok),以及从下到上的归阶段。
beginWork负责从上往下创建子fiberNode
,以及构建节点间的连接关系。
completeWork负责从下往上构建dom树,不过此时的dom树是存在于内存中的,还没插入到页面。在mount阶段,除了HostRoot Fiber
,其他的fiber
都是不跟踪副作用的,因此只会在HostRoot Fiber
标记Placement
。
render阶段走完,dom树就在内存中构建完成。而work in progress fiber tree
也构建完成,此时fiberRootNode.finishedWork
就会指向它。
然后就到了commit阶段。目前我只了解Mutation阶段,在这个阶段会将页面更新,更新完成后,fiberRootNode
根节点的fiberRootNode.current
指向会切换到当前的work in pregress fiber tree
。
update
update的流程会比首次渲染复杂一点,因为这时需要跟踪副作用:
更新是可以由任意fiber
节点发起的,接下来会从fiber节点往上遍历,直到找到根节点。后面一样是要走render阶段和commit阶段,只不过相关流程的处理会与mount时有一些不同。
fiber节点是update还是mount的区别其实就是work in progress fiber
有没有对应的current fiber
,只要存在,则这个fiber节点属于update。哪怕页面不是首次渲染,一些fiber仍然可以是出于mount阶段,因为该节点可以是首次渲染。
beginWork时仍然是从上往下创建子fiber
,但是这时需要比较原来的子fiber
和当前子ReactElement
,确定fiber
节点是否能复用,如果可以复用就返回复用的节点,不能就依据ReactElement
创建出新的节点,在这个过程中还需要标记节点是插入、移动、删除。
completeWork负责从下往上给节点标记Update
,不过在big-react
这个轮子中,应该是简化了HostComponent Fiber
的处理,直接将props
更新了,没有标记副作用。同时这时也会将副作用冒泡给父节点,这样做的目的是为了可以快速通过fiber.subtreeFlags
知道子节点存在哪些副作用操作。
至此,走完了render阶段,而work in progress fiber tree
也构建完成,此时fiberRootNode.finishedWork
就会指向它。
然后进入commit阶段,在这个阶段会处理节点的插入、移动、更新、删除,也就是在这个阶段就会涉及到diff算法,按照标记操作,直到最后(如果有更新)就可以将ui插入到容器中,完成页面的更新。
其实,对比着看,可以看出mount和update的流程还是有很多共通的地方。
hook
上面说的更新流程要触发,目前代码实现了两种方式,都是基于函数组件的useState
触发的,一种是通过事件触发修改dispatch
,一种是将dispatch
暴露到其他上下文,直接调用触发。因此想要走通更新流程,需要实现最低限度的hook以及事件系统。而在这个实现的过程,也让我对react的hook和事件系统有了更清晰的认识。
hook要解决几个问题:
- 识别hook调用时所处的阶段
方法:在不同的阶段各实现一套hook的集合,例如mount,update,以及在hook的上下文中,而不在这些上下文中
- 要识别hook是否在函数组件中,或者hook中调用
方法:在render函数组件时存下当前的fiber,在结束前将变量清空,这个阶段是同步的,因此通过这种方式,就可以判断当前hook是否在函数组件中被调用(hook要在函数组件中被调用,因此在hook中调用hook也算是在函数组件中调用),因为只有函数组件会在render中被调用,从而让变量记录下当前fiber
。而且可以在执行后续fiber
操作前重置。
- hook的实现是属于
react-reconciler
包的,但是要从react
这个包中引入。
方法:在react中实现一个内部数据共享层,并通过shared
包暴露给其他所有的包
- 数据共享层存在就要确保打包时
react
的代码不能重复打包,因为重复打包的话会导致不同的包会各自维护一个共享层,导致数据无非实现共享。
方法:打包时要将react
配置为第三方包,从而避免代码重复打包
事件系统
虽然事件系统实际上只实现了点击,但也大体了解了react的事件系统。当然这里说的是可以捕获冒泡的事件,一些不能冒泡的事件,react应该也是做了处理的,这块目前就没有涉及到。 react的事件系统,其实就是模仿原生的事件的捕获以及冒泡流程,依次触发。 具体实现就是在容器注册监听事件(例如点击事件),一旦事件触发,将目标到容器所有元素按照顺序查找元素有没有对应的事件名需要注册,如果有,将方法按照类型放到冒泡数组或者捕获数组中。需要留意放入方法时是冒泡的新方法是插入到尾部,而捕获则是首部,这是为了模拟事件触发时先从外到内的捕获,再到目标元素,再从内往外冒泡的流程。接着就是构建一个合成事件。然后就是依次触发捕获数组和冒泡数组(用合成事件作为事件参数)。
以上就是我在这12个课时里面学到的一些内容与心得,我觉得结合书本和课程来学习的话,确实是可以对react有一个比较深刻的了解,课程其实就是造react的轮子,通过开发的过程理解代码,这样理解的代码会比较深刻,但是你不知道你的实现和react的实现有什么区别,而书本则基于源码来写的,最后当然还有源码,想真的了解代码,总归是要回归源码的。