欢迎订阅 React技术揭秘 代码参照React 16.13.1 。
假设React是你平常开发的框架,在日复一日的开发中,你萌生了学习React源码的念头,在网上一顿搜索后,你发现这些教程能够分为2类:javascript
《xx行代码带你实现迷你React》,《xx行代码实现React hook》这样短小精干的文章。若是你只是想花一点点时间了解下React的工做原理,我向你推荐 这篇文章,很是精彩。html
《React Fiber原理》,《React expirationTime原理》这样摘录React源码讲解的文章。若是你想学习React源码,当你都不知道Fiber
是什么,不知道expirationTime
对于React的意义时,这样的文章会给人“你讲解的代码我看懂了,但这些代码的做用是什么”的感受。java
我要写的这个系列文章和对应仓库的存在就是为了解决这个问题。react
简单来讲,这个系列文章会讲解React为何要这么作,以及大致怎么作,但不会有大段的代码告诉你怎么作。git
当你看完文章知道咱们要作什么后,再来看仓库中具体的代码实现。github
同时为了防止堆砌不少功能后,代码量太大影响你理解某个功能的实现,我为仓库每一个功能的实现打了一个git tag
。web
RectDOM.render(<App/>, document.getElementById('app'));复制代码
没有state、没有Hooks、没有函数组件和类组件,只能渲染首屏元素,可是全部目录架构、文件名、方法都和React同样,代码片断彻底同样(由于就是一边debug一边抄的)。npm
若是你想读React源码,但又被React庞大的代码量劝退,我相信这个项目适合你起步。react-native
npm start复制代码
这是这个系列第一篇文章,对应 git tag v1,正餐开始~数组
咱们知道,React是一个声明式的UI库,咱们经过组件的形式声明UI,React会为咱们输出DOM并渲染到页面上。
在React中,对UI的声明是经过一种称为JSX的语法糖来实现。JSX在编译时会被Babel转换为React.createElement方法。
在运行时咱们获取到的实际上是
// 输入JSX const a = <div>Hello</div> // 在编译时,被babel编译为React.createElement函数 const a = React.createElement('div', null, 'Hello'); // 在运行时,执行函数,返回描述组件结构的对象 const a = { ?typeof: Symbol(react.element), "type": "div", "key": null, "props": { "children": "Hello" } }复制代码
咱们能够看到,在运行时描述组件结构的对象离渲染到页面上的DOM还相去甚远,为了能渲染DOM到页面上,React内部确定有2个模块:
在React中,咱们把模块1作的工做叫render,把模块2作的工做叫commit。
为何叫这个名字呢,想一想你写的ClassComponent的render方法,在render阶段要作的一件事就是执行render方法。
至于commit,可能你会想到 git commit 。事实上,React的工做流程和Git多分支开发很是类似。
因此,更新下咱们的架构:
到目前为止,咱们简单介绍了render和commit,有了这2个阶段,咱们已经能够实现除了异步模式(Concurrent)外React的大部分功能。
可是,设想如下场景:
甚至极端的考虑,咱们已经触发了2,在计算2须要改变的DOM节点的过程当中用户又触发了1,这时候若是能搁置2转而优先处理1,这种体验是符合预期的。
因此咱们须要一种机制来处理更新的优先级,决定哪一个状态变化带来的更新应该被优先执行。
为了达到这个目的,咱们知道须要为现有架构增长一个schedule阶段:
基于咱们如今的设计,commit阶段负责把须要渲染的DOM元素渲染到页面上。
可是React的野心历来不只限于web端,理论上当render阶段决定了哪些JSX须要被渲染后,咱们对应不一样的commit,就能实如今不一样平台的渲染。
当咱们尝试渲染 <App/> 时,在render阶段会生成右侧的Fiber结构。Fiber的完整结构看这里。
在React中,咱们的组件会造成一棵组件树,一样的,有了Fiber的结构后,咱们须要将他们连接在一块儿组成Fiber树。咱们为Fiber增长以下字段:
小朋友,此时你是否有不少???
为啥这个字段叫return,不叫parent,React核心团队的Andrew Clark解释说:能够理解为return指向当前Fiber处理完后返回的那个Fiber,当子Fiber被处理完后会返回他的父Fiber。好吧
因此咱们的完整Fiber结构是这样的:
如今咱们有了描述组件的节点类型(Fiber),能够愉快的开始首屏渲染了。
须要注意的是,因为执行ReactDOM.render产生的首屏渲染并不涉及到其余更高优先级的更新,因此对于首屏渲染,咱们掠过schedule阶段。
好比刚才介绍schedule阶段举的地址输入框的例子,首屏渲染了输入框,更高优先级的更新是后续在输入框中输入文字产生的。
当咱们首次进入render阶段时,咱们传入JSX:
整个render阶段须要作2件事:
PS:这里同窗可能会奇怪,这一步为何是“为每一个节点的子节点生成对应的Fiber”而不是“为当前节点生成对应的Fiber”?还记得下面这行代码么:
2. 为每一个Fiber生成对应的DOM节点,保存在Fiber.stateNode
作完这2件过后咱们进入commit阶段,此时咱们知道
有了这些信息,Commit阶段只须要遍历全部有Placement反作用的Fiber,依次执行DOM插入操做就完成了首屏的渲染。
这就是首屏渲染render+commit的整个过程。机智如你,是否是理解起来彻底没压力呢。
咱们刚才讲了render阶段会作2件事(会调用的2个函数),如今咱们给他们起个名字吧:
向下遍历JSX,为每一个JSX节点的子JSX节点生成对应的Fiber,并设置effectTag
咱们叫他beginWork,这是每一个节点render阶段开始工做的起点。
为每一个Fiber生成对应的DOM节点
咱们叫他completeWork,这是每一个节点render阶段完成工做的终点。
咱们经过workInProgress这个全局变量表示当前render阶段正在处理的Fiber,当首屏渲染初始化时, workInProgress === 根Fiber。
调用workLoopSync方法,他内部会循环调用performUnitOfWork方法。
performUnitOfWork每次接收一个Fiber,调用beginWork或CompleteWork,处理完该Fiber后返回下一个须要处理的Fiber。
当performUnitOfWork返回null时,就表明全部节点的render阶段结束了。
整个流程虽然看起来繁琐,但就作了2件事:
在这个过程当中若是遇到兄弟节点,又重复步骤1,直到最终又回到根Fiber,完成整棵树的建立与遍历。
在咱们的设计中,commit阶段会遍历找到全部含有effectTag的Fiber节点。若是Fiber树很庞大的话,这个遍历会很耗时。
但其实在render阶段咱们已经知道哪些Fiber会被设置Fiber.effectTag, 因此咱们能够在render阶段就提早标记好他们,将他们组织成链表的形式。
假设图中标红的Fiber表明本次调度该Fiber有effectTag,咱们用链表的指针将他们连接起来造成一条单向链表,这条链表就是 effectList。
用Redux做者Dan Abramov的话来讲,effectList相对于Fiber树,就像圣诞树上的彩蛋
有了effectList,commit阶段只须要遍历这条链表就能知道全部有effectTag的Fiber了。这部分代码在completeUnitOfWork函数中。
按照咱们的架构,咱们会给须要插入到DOM的Fiber赋值
fiber.effectTag = Placement;复制代码
这对于某次增量更新来讲没有问题,但对于首屏渲染却过低效了,毕竟对首屏渲染来讲,全部Fiber节点对应的DOM节点都是须要渲染到页面上的。
难道咱们要给全部Fiber赋值effectTag = Placement;再在commit阶段一次次的执行DOM插入操做来生成一整棵DOM树?对于首屏渲染,咱们须要稍微变通下。
当咱们在render阶段执行completeWork建立Fiber对应的DOM节点时,咱们遍历一下这个Fiber节点的全部子节点,将子节点的DOM节点插入到建立的DOM节点下。
(子Fiber的completeWork会先于父Fiber执行,因此当执行到父Fiber时,子Fiber必定存在对应的DOM节点)。代码见这里
这样当遍历到根Fiber节点时,咱们已经有一棵构建好的离屏DOM树,这时候咱们只须要赋值根节点的effectTag就能在commit阶段一次性将整课DOM树挂载。
// 仅赋值根fiber一个节点effectTag
RootFiber.effectTag = Placement; 复制代码
// 赋值根fiber
workInProgress = Rootfiber;复制代码
这中间发生了什么?
复习小课堂:workInProgress指当前render阶段正在处理的Fiber,ReactDOM.render会建立一个RootFiber,他会赋值给workInProgress
{
// UpdateState | ReplaceState | ForceUpdate | CaptureUpdate
tag: UpdateState,
// 更新的state
payload: null,
// 指向当前Fiber的下一个update
next: null
}复制代码
调用React ClassComponent的this.setState,会产生一个update,update.payload为须要更新的state,在该ClassComponent对应的Fiber执行beginWork时会处理state的更新带来的组件状态改变,固然,在V1版本咱们尚未实现。
对于调用ReactDOM.render使根Fiber初始化时,会产生一个update,update.payload为对应须要渲染的JSX(代码见这里),在根Fiber的beginWork中会触发这篇文章讲到的render流程。
篇幅有限,咱们讲的不少都是宏观的东西,要了解细节还须要多多debug代码,把咱们的Demo单步调试几遍。
这里再给你推荐一篇极好的React原理文章,配合本文食用效果极佳