为什么要写这个系列?
2020年头给自己定下方针,本年要读懂React源码,最好能成为React Contributor(没想到很快就完结了,虽然提交的commit很细小)。
为什么要读React源码呢,由于假如单纯开发日常业务的话,前端的边界其实很窄。回想一下,你本年做的业务,换作是上一年的你,前年的你,换作是应届生; 9 Y f }甲乙丙,他们能替换你的位置么?我这么一想,就有迫切的希望拓宽自己的边界。
前端的边界许多——可视化、结构、东西链等,这些都能成为一个前端差异其他前端的当地,而我挑选从日常作业最熟悉的伙伴——React下手。即便不考虑这些功2 5 v N Q ^利的因素,全世界最优秀的一批前端(Facebook)耗费多年开发的结构,去学习Z ] ; R 7 * ; D下他们的代码,不香么?
已然定下了宏大的方针(笑),怎样下手呢?网上有些类h | V * K 7似《从0完结迷你React》的文章,他们提炼了React的一些关; $ , v ^键思路,用很少的代码完结了React的某项功用,阅览他们对了解React的思路很有帮助,尤其引荐这篇。但这不是我想要的,我想要的是真正7 M V的| M F ] A [ @React,辣个React。S a =
RectDOM.render(<A* F ( - H Y Y { vpp/>, d U ~ 3 5ocument.getElementById('app'));
假如你想读React源码,但又被React巨大的代码量劝退,我信任这个项目适合你起步。
npm start
这是这个系列榜首篇文章,对应 git tag v1,正餐开端~
调度器 + 烘托器 = React
-
输入JSX后,咱们怎样解析JSX,并决定哪些是需求终究烘托成DOM节点?
-
咱们怎样把需求烘托的DOM元素烘托到页面上?
为什么需求调度器?
-
咱们希望用户输入的字符能实时显示在输入框M 4 l d ! H内,不能有卡顿。
-
下拉框内容有个s Q i加载的过程一般是能够承受的。
这D U v R便是T i | 8咱们叫他调度器的原因——决定要处理什么,以1 ; k 8 5 y d v及调度他们的K A q y + 2 W优先级。
为什么需求烘托器?
-
DOM烘托器烘托到浏览器端
-
Native烘托器烘托App原生组件z N F
-
Test烘托器烘托出纯J : ts对象用于测试
-
Art烘托器烘托到Canvas, SVG 或 VML (IE8)
调度的最小单元——Fiber
-
由于调度器R G ] Q l能对应多个渠道的烘托器,那调度器调度的节点就不能是渠道相关的。假如调度器调度出的节点都是DOM节点,显然这些节点是没法在Native环境被烘托器烘托的。所以咱们需, m r X – `求一种渠道无关的节点结构。
-
方才讲到调度器的功用时,咱们希望低优先级的调度是能够被停止以重新开端一个更高优先级的调度的。那么被调度的节点粒度一定要够细,这样咱们才干彻底控制节点停止调度的位置并铲除之前调度发生的成果再重新开端。
当咱们测验烘托 <AP % o C b x ] Npp/> 时,会生成右侧的Fiber结构。FO % T Niber的完整结构看这儿。
咱们能够在Fiber节点中保存节点的类型(比o – O , @ d % G ~如AppT V q + T z X r节点是一个函数组件节点,div节点是一个原生DOM节点,I am节点是一个文本节点)p $ K ~ h 6,能够保存节点对应p + M * u的state,props,能够保存节点对应的值(比如Appa K ? b c节点对应X Q B左侧的函数,div节点对应div DOMElement)。
关于Fiber的结构其实咱们能够更进一步。咱们为Fiber增加如下字段:
- child:指向榜首个子Fiber
- sibling:指向右边的兄弟节点
这样咱们的父Fiber节点m 0 – : 1 % W $ a不需求用数组的形式保存多个子节点。所以咱们能够这么改进下:
同时由于Fiber是一层层向下遍历,当遍历到图中的div Fiber节点,咱们现已知道他的父节点是App Fiber节点,这时候能够赋R . x y y % p值 div Fiber.re3 D X Wturn = App Fiber; 即用return指向自己的父节点。
小朋友,此刻你是否有许多❓❓{ 0 F 5 M❓,为啥这个字段叫return,不叫parent,React[ ~ k % C 0 h V核心团队的Andrew Clark解释说:能够了解为return指向当时Fiber处理完后回来的那个Fiber,当子Fiber被处理完后会回来他的父Fiber。好吧♂️
所以咱们的完整Fiber结构是这样的P + O o ):
调度和烘托的全体流程
-
向下遍历JSX,为每个节点的子节点生成对应的Fiber,并赋值
-
Placement 刺进DOM节点
-
U] Z T 3pdate 更新DOM节点
-
Deletion 删除DOM节点
PS:这儿同学0 & z o } %可能会古怪,这一步为什么是“为每个节点的子节点生成对应的Fiber”而不是“为当时节点生成对应的Fiber”?还记得下面这行代码么:+ j | 7 w u f b 6
2. 为每个Fiber生成对应的DOM节点,保存在Fiber.stateNode。
-
哪些FiberK | Q O d w t !需求履行哪些操作(由Fiber.effectTag得知)
-
履行这些操作的Fiber他们对应的DOMc C Y . 6节点(由Fiber.stateNodJ m | ae得知)
术语小课堂:
咱们j = / D 2 O一直讲调度和烘托,在React中t l N,他们分别叫做render阶段和commit阶段,所以以后咱们在讲render阶段时便是在说调S ? d ? k } R @ `度T Q 5 A阶段,讲commit阶段便是在说烘托阶段; L t t 8 v a 4。
调度阶段要做的2件事
beginWork
向下遍历JSX,为每个节点的子节点生成对应的Fiber,并设置effectTag
completeWork
为每个Fiber生成对应的DOM节点
咱们经过workInProgreY z e K | E ` e nss这个~ j . ) &全局变量表明当时render阶段正在处理的Fiber,当首屏烘托初始化时, workInProgress === 根Fiber,接着咱们调用workLoopSync办法,他内部会循环调用performUnitOfWork办法,这个办法接% / H B w p l c收当时workInProgress传入,处理他,回来下一个需求处理的Fiber。
关于图中Demo来说,Q % W C – L w便是遍历到 “I am”文本节点或”KaKaSong”文本节点。此刻会履行completeUnitOfWork办法,这个办法内部会调用咱们方才讲的completeWorH p o v Uk,并测验回来其兄弟h . g : c BFiber节点。
整个流程虽然看起来繁琐,但就做了2件事:
-
采用深度优先遍历,从上往下生成子Fiber,向子Fiber遍历= , D(代码T ? @ U l m)
-
当遍历终究时,开~ X q y端从下往上遍历,为每个1r 7 9 E 1中现已创立的Fiber创立对应的DOM节点(代码)
优化烘托K – q b p Z s E c阶段
effectList
在u N 3咱们的规划中,烘托阶段会遍历找到一切含有effectTag的Fiber节点。假如Fiber树很巨大的话,这个遍历会很耗时。
但其实在调度阶段咱们现已知道哪些Fiber_ 8 j e @会被设置Fiber.effectTag, 所以咱们能够在调度阶段就提早标记好他们,将他们组织成链表的形式。
假设图中标红的Fiber代表本次调度该Fiber有effectTag,咱们用链表的指针将他们链接起来构成一, z s I :条单向链表,这条链表便是 effl k : 0 U wectList。
用Redux作者Dan Abramov的话来说,effectList相关于Fiber树,就像圣诞树上的彩蛋
那么烘托阶段只需求遍历这条链表就3 s & 2 j P #能知道一切有effectTag的Fiber了G O { s = / t ( W。这部分代码在complg 0 K c J deteUnitOfWork函数中。
首G l @ V [ H ! C屏烘托的特别之处
依照咱们的架构,咱们会给需求刺进到DOM的Fiber设置effectTag = Placement;这关于某次增量更新来说没有问题,但关于首屏烘托却太低效了,毕竟对首屏烘托来说,一O 3 f 8切Fiber节点对应的DOM节点都是需求烘托到页面上的。
难道咱们要给一切Fiber赋值effectTag = Placement;再在烘托阶段一次次的履行DOM刺进操作来生成一整棵DOM树?关于首屏烘托,咱们需求稍/ – M C微变通下。
当咱们在K 1 ) % R调度阶段履行completeW$ B = * F York创立Fiber对应的DOM节点时,咱们遍历一下这个Fiber节点的一切o 4 * ^ ]子节点,将子节点的Ds Q @OM节点刺进到创立的DOM节点下。7 l P p ? S , E .(子Fiber的complet| Y d zeWork会先于父Fiber履行,所以当履行到父Fiber时,子Fiber一定存在对应的DOM节点)。代码见这儿
这样当遍历到根Fiber节点时,咱们现已有一棵构建好的离屏DOM树,这时R ! 0 ^ ( I j a x候咱们只需求设置根节点一个节点effectTag = Pl& G vac0 K # ^ M W 2 hement; 就能在烘托f = j 6 H b M F阶段一次性将整课DOM树挂载。
调度阶段之前发生了什么
复习小课堂:workInProgress指当时调度阶段正在处理的Fiber,ReactDOM.ren* o X Y u ! rder会创立一个RootFiber,他会赋值给workInProgress
-
ReactDOM.render
-
this.setState
-
tihs.e . : c Z / SforceUpdate
-
useRedu~ # 4 + ^ ~ –cer hook
-
useState hook (PS:useState其实便是一种特别的useReducer)
{
// UpdateState | ReplaceStatr X 9 ( U 1 o k (e | ForceUpdate | CaptS ~ L M OureUpdate
tag: UpdateState,
// 更新的s7 O 4tate
pa* Y E N Tyload: nulT S R o $ 3 c & Al,
// 指向当时Fiber的下一个update
next: null
}
关于Reactc ! I J H a K ClassComponent的this. W O F x 2 R z.setState,会发生一个update,up6 R ( Y O & @ / Tdate.payload为需求更新的state,在对应ClassComponent的Fiber履行beginWork时会处理state的更新带来的组件状况改动,当然,在V1版别咱们还没有完结。
关于根Fiber初始化时,会发生一个update,update.payload为对应需求烘托的JSX(代码见这儿),在根Fiber的begiz B 4nWork中会触发这篇文章讲到的rend6 g ^ A { O 3 @er流程。
最终的最终
篇幅有限,咱们讲的许多都是微观的东{ x k ~西,要了解细节还需求多多debug代码,把咱们的Demo单步调试几遍。
这儿再给你引荐一篇极好的React原~ m / / ! 4理文章,合作本文食用作用极佳