为什么要写这个系列?

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,辣个ReactS a =

于是,开端debug React 。假如Reacu | Q {t是A . 9一个毛线团的话,那么他的线头一定是
RectDOM.render(<A* F ( - H Y Y { vpp/>, d U ~ 3 5ocument.getElementById('app'));
经过这个线头,我梳理出React首屏烘托会做的作业,将他们从React代码中抽离出来,加了许多注释,这便是v1版别的React。没有state、没有H6 N Hooks、没有函数组件和类组件,只能烘托首屏元素,可是一切目录架构、文件名、d q g H Z #办法都和React相同,代码片段彻底相同(由于便是一边da [ ) h 0 @ f 4ebug一边抄的)。

假如你想读React源码,但又被React巨大的代码量劝退,我信任这个项目适合你起步。

这个系列的每篇文章,都是对f g Z a v , t S 7应仓库的一个版别的学习笔记。假如你想跟着我一同学习,能够找到8 z u o D 2对应版别的git tag ,clone到本地,安装依赖后
npm start
会打开当时版别的示例,合作文章 + debu1 + 1 ` G R 0g 服用。同时经过create-reo P Cact-ap+ A j o p I 5 )p创立一个React使用,跑相同的示例代码作为对照。你会发现,咱们项目的烘托流程和React是共z : S / [ y 8 `同的。

这是这个系列榜首篇文章,对应 git tag v1,正餐开端~

ps:项目代码参照React 16.13.1

调度器 + 烘托器 = React

让咱们站在结构规划者的视点,首先咱们现已决定使用JSX来体现咱们的UI组件(假如你还不清楚JSX,能够看这儿),有2个首要问题需求处理:
  1. 输入JSX后,咱们怎样解析JSX,并决定哪些是需求终究烘托成DOM节点?
  2. 咱们怎样把需求烘托的DOM元素烘托到页面上?
React给出的解答是:将整个流程分为调度和烘托2部分。
🔥从0实现React 📖PART1 React的架构设计

为什么需求调度器?

设想以下场景:
有一个地址搜索框,在输入字符时会实时请求当时已输入内容的地址匹配成果。

🔥从0实现React 📖PART1 React的架构设计

这儿包括2个状况改动:
  1. 咱们希望用户输入的字符能实时显示在输入框M 4 l d ! H内,不能有卡顿。
  2. 下拉框内容有个s Q i加载的过程一般是能够承受的。
所以当同时触发这两个状况改动时1的优先级假如能高于2那用户体会想必是更好的。乃至极点的考虑,咱们现已触发了2,在计算2需求改动的DOM节点的过程中用户又触发了1,这时候假如能放置2转而优先处理场景1,这种体会是符合预期的。

D U v R便是T i | 8咱们叫他调度器的原因——决定要处理什么,以1 ; k 8 5 y d v及调度他们的K A q y + 2 W优先级。

当调度器现已处理好需求V e D P W *烘托的节点,为什么不直接烘托呢,而需求烘托器?

为什么需求烘托器?

由于React的野心从来不仅限于web端,理论上当调度器整理出的节点使用于不同烘托器,就能完结在不同渠道的烘托。
🔥从0实现React 📖PART1 React的架构设计
  • DOM烘托器烘托到浏览器端
  • Native烘托器烘托App原生组件z N F
  • Test烘托器烘托出纯J : ts对象用于测试
  • Art烘托器烘托到Canvas, SVG 或 VML (IE8)

调度的最小单元——Fiber

要完结React的雄伟愿景,还有2个小问题:
  1. 由于调度器R G ] Q l能对应多个渠道的烘托器,那调度器调度的节点就不能是渠道相关的。假如调度器调度出的节点都是DOM节点,显然这些节点是没法在Native环境被烘托器烘托的。所以咱们需, m r X – `求一种渠道无关的节点结构。
  2. 方才讲到调度器的功用时,咱们希望低优先级的调度是能够被停止以重新开端一个更高优先级的调度的。那么被调度的节点粒度一定要够细,这样咱们才干彻底控制节点停止调度的位置并铲除之前调度发生的成果再重新开端。
为了处R ^ ] ] 4 J 1 l理这2个问题,React提出了一种名叫Fiber的结构,如下图:
🔥从0实现React 📖PART1 React的架构设计

当咱们测验烘托 <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)。

这样的结构也解释了为什么函数组件(React Hooks)能够保存state。由于state并不是保存在函数上, . A P P而是保存在函数组件对应的Fiber节点上。

关于Fiber的结构其实咱们能够更进一步。咱们为Fiber增加如下字段:

  • child:指向榜首个子Fiber
  • sibling:指向右边的兄弟节点

这样咱们的父Fiber节点m 0 – : 1 % W $ a不需求用数组的形式保存多个子节点。所以咱们能够这么改进下:

🔥从0实现React 📖PART1 React的架构设计

同时由于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 )

🔥从0实现React 📖PART1 React的架构设计
你能够在这篇文章看到Reactj Y N ] h团队当初规划Fiber架构时的心路历程。

调度和烘托的全体流程

现在咱们有了可供调度的节点类型(Fiber),能够愉快的开端榜首次调度辣。
这儿咱们以项目V1版别的Demo为例:
🔥从0实现React 📖PART1 React的架构设计
当咱们初次进入调度流程时,咱们传入JSX:
🔥从0实现React 📖PART1 React的架构设计
整个调度阶段需求做2件事:
  1. 向下遍历JSX,为每个节点的子节点生成对应的Fiber,并赋值
🔥从0实现React 📖PART1 React的架构设计
effectTag字段表明当时Fiber需求履行的副作用,最常见的副作用是:
  • Placement 刺进DOM节点
  • U] Z T 3pdate 更新DOM节点
  • Deletion 删除DOM节点
当然,首屏烘托只会涉及到Placement。(一切effectTag见这儿)

PS:这儿同学0 & z o } %可能会古怪,这一步为什么是“为每个节点的子节点生成对应的Fiber”而不是“为当时节点生成对应的Fiber”?还记得下面这行代码么:+ j | 7 w u f b 6

🔥从0实现React 📖PART1 React的架构设计
履行这行初始化的代码首先会创立一个根Fiber节点,所以当从根Fiber向下创立Fiber时,咱们始终是为子节点创立Fiber。这是要做的榜首件事。

2. 为每个Fiber生成对应的DOM节点,保存在Fiber.stateNode。

🔥从0实现React 📖PART1 React的架构设计
做完这2件事后咱们告诉烘托器,此刻烘托器知道
  1. 哪些FiberK | Q O d w t !需求履行哪些操作(由Fiber.effectTag得知)
  2. 履行这些操作的Fiber他们对应的DOMc C Y . 6节点(由Fiber.stateNodJ m | ae得知)
有了这些数据,烘托器只需求遍历一切有PlacemS B d d k 4 2 X Jent副作用的Fiber,依次履行DOM刺进操作就完结了首屏的烘托。
这便是首屏调度+烘托的整个Y , 6 s w过程。机敏如你,是不g a ,是了解起来彻底没压力o ! 6呢。
‍术语小课堂:
咱们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件事

咱们方才讲了5 X C V { t调度阶段会做2件事(会调用的2个函数),现在咱们给他们起个名字吧:

beginWork

向下遍历JSX,为每个节点的子节点生成对应的Fiber,并设置effectTag
咱们叫他beginWork,这是每个节点调度阶段开端作业的起点。

completeWork

为每个Fiber生成对应的DOM节点
咱们叫他completeWork,这是每个节点调度阶段完结作业的结尾。

咱们经过workInProgreY z e K | E ` e nss这个~ j . ) &全局变量表明当时render阶段正在处理的Fiber,当首屏烘托初始化时, workInProgress === 根Fiber,接着咱们调用workLoopSync办法,他内部会循环调用performUnitOfWork办法,这个办法接% / H B w p l c收当时workInProgress传入,处理他,回来下一个需求处理的Fiber。

🔥从0实现React 📖PART1 React的架构设计
当这个循环完毕时,就代表. 2 9 p $ B v一切节点的调度阶段完毕了。
🔥从0实现React 📖PART1 React的架构设计
在performUnitOfWork函数内e z N E % c 7 H *部,会履行方才咱们讲到的beginWork,创立并回来当时Fiber的子Fiber。当没有回来F ? g _ f $子Fiber,意味着遍历到最内层的节点,如图:
🔥从0实现React 📖PART1 React的架构设计

关于图中Demo来说,Q % W C – L w便是遍历到 “I am”文本节点或”KaKaSong”文本节点。此刻会履行completeUnitOfWork办法,这个办法内部会调用咱们方才讲的completeWorH p o v Uk,并测验回来其兄弟h . g : c BFiber节点。

整个流程虽然看起来繁琐,但就做了2件事:

  1. 采用深度优先遍历,从上往下生成子Fiber,向子Fiber遍历= , D(代码T ? @ U l m
  2. 当遍历终究时,开~ X q y端从下往上遍历,为每个1r 7 9 E 1中现已创立的Fiber创立对应的DOM节点(代码)
在这个过程中假如遇– j L到还未处理的兄弟节点,又重复1,直到终究又回到根节点,完结整棵树的创立与遍历。

优化烘托K – q b p Z s E c阶段

到目前为止咱们的现已很接近React了,只需再优化2点几乎便是React本act了。

effectList

u N 3咱们的规划中,烘托阶段会遍历找到一切含有effectTag的Fiber节点。假如Fiber树很巨大的话,这个遍历会很耗时。

但其实在调度阶段咱们现已知道哪些Fiber_ 8 j e @会被设置Fiber.effectTag, 所以咱们能够在调度阶段就提早标记好他们,将他们组织成链表的形式。

🔥从0实现React 📖PART1 React的架构设计

假设图中标红的Fiber代表本次调度该Fiber有effectTag,咱们用链表的指针将他们链接起来构成一, z s I :条单向链表,这条链表便是 effl k : 0 U wectList。

🔥从0实现React 📖PART1 React的架构设计

用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树挂载。

调度阶段之前发生了什么

到这儿咱们现已接近完结React的首屏烘A p w – % J K E S托了,还差最终一步,那便是从
🔥从0实现React 📖PART1 React的架构设计
到赋值workInProgress这中间发生了什么?
复习小课堂‍:workInProgress指当时调度阶段正在处理的Fiber,ReactDOM.ren* o X Y u ! rder会创立一个RootFiber,他会赋值给workInProgress
为了了解这个问题,咱们需求知道,排除SSR相关,都有哪些办法能触发React组件的烘托?
  1. ReactDOM.render
  2. this.setState
  3. tihs.e . : c Z / SforceUpdate
  4. useRedu~ # 4 + ^ ~ –cer hook
  5. useState hook (PS:useState其实便是一种特别的useReducer)
已然有这么多办法触发烘托,那么咱们需求一种一致的机制来i v ! 8 ` 3 _ _ t表明组件需求更新。在React中,这种机制叫update,代码见这儿| C e ` c。现在咱们能够只关注update的如下参数
{
// 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流程。

最终的最终

至此咱们跑通了React的首屏烘托流程。假如你看到了这儿,为自I ] Q ~ = o s己鼓鼓掌吧。

篇幅有限,咱们讲的许多都是微观的东{ x k ~西,要了解细节还需求多多debug代码,把咱们的Demo单步调试几遍。

这儿再给你引荐一篇极好的React原~ m / / ! 4理文章,合作本文食用作用极佳

Inside Fibet a C sr: in-k } 8 [ m udepth overview of the new reconcili! 5 8 Z K 7 n nation algorithm in Re[ / I D S b Tact