2020年初给自己定下目标,今年要读懂React源码,最好能成为React Contributor(没想到很快就实现了,虽然提交的commit很微小)。
为什么要读React源码呢,因为如果单纯开发日常业务的话,前端的边界其实很窄。回想一下,你今年做的业务,换作是去年的你,前年的你,换作是应届生甲乙丙,他们能替换你的位置么?我这么一想,就有迫切的愿望拓展自己的边界。
前端的边界很多——可视化、框架、工具链等,这些都能成为一个前端区别其他前端的地方,而我选择从日常工作最熟悉的伙伴——React下手。即使不考虑这些功利的因素,全世界最优秀的一批前端(Facebook)耗费多年开发的框架,去学习下他们的代码,不香么?
既然定下了宏大的目标(笑😊),如何下手呢?网上有些类似《从0实现迷你React》的文章,他们提炼了React的一些关键思路,用很少的代码实现了React的某项功能,阅读他们对了解React的思路很有帮助,尤其推荐这篇。但这不是我想要的,我想要的是真正的React,辣个React。
RectDOM.render(<App/>, document.getElementById('app'));复制代码
如果你想读React源码,但又被React庞大的代码量劝退,我相信这个项目适合你起步。
npm start复制代码
这是这个系列第一篇文章,对应 git tag v1,正餐开始~
这就是我们叫他调度器的原因——决定要处理什么,以及调度他们的优先级。
当我们尝试渲染 <App/> 时,会生成右侧的Fiber结构。Fiber的完整结构看这里。
我们可以在Fiber节点中保存节点的类型(比如App节点是一个函数组件节点,div节点是一个原生DOM节点,I am节点是一个文本节点),可以保存节点对应的state,props,可以保存节点对应的值(比如App节点对应左侧的函数,div节点对应div DOMElement)。
对于Fiber的结构其实我们可以更进一步。我们为Fiber增加如下字段:
这样我们的父Fiber节点不需要用数组的形式保存多个子节点。所以我们可以这么改进下:
小朋友,此时你是否有很多❓❓❓,为啥这个字段叫return,不叫parent,React核心团队的Andrew Clark解释说:可以理解为return指向当前Fiber处理完后返回的那个Fiber,当子Fiber被处理完后会返回他的父Fiber。好吧🤷♂️
所以我们的完整Fiber结构是这样的:
PS:这里同学可能会奇怪,这一步为什么是“为每个节点的子节点生成对应的Fiber”而不是“为当前节点生成对应的Fiber”?还记得下面这行代码么:
2. 为每个Fiber生成对应的DOM节点,保存在Fiber.stateNode。
👨🏫术语小课堂: 我们一直讲调度和渲染,在React中,他们分别叫做render阶段和commit阶段,所以以后我们在讲render阶段时就是在说调度阶段,讲commit阶段就是在说渲染阶段。
向下遍历JSX,为每个节点的子节点生成对应的Fiber,并设置effectTag
为每个Fiber生成对应的DOM节点
我们通过workInProgress这个全局变量表示当前render阶段正在处理的Fiber,当首屏渲染初始化时, workInProgress === 根Fiber,接着我们调用workLoopSync方法,他内部会循环调用performUnitOfWork方法,这个方法接收当前workInProgress传入,处理他,返回下一个需要处理的Fiber。
对于图中Demo来说,就是遍历到 "I am"文本节点或"KaKaSong"文本节点。此时会执行completeUnitOfWork方法,这个方法内部会调用我们刚才讲的completeWork,并尝试返回其兄弟Fiber节点。
整个流程虽然看起来繁琐,但就做了2件事:
在我们的设计中,渲染阶段会遍历找到所有含有effectTag的Fiber节点。如果Fiber树很庞大的话,这个遍历会很耗时。
但其实在调度阶段我们已经知道哪些Fiber会被设置Fiber.effectTag, 所以我们可以在调度阶段就提前标记好他们,将他们组织成链表的形式。
假设图中标红的Fiber代表本次调度该Fiber有effectTag,我们用链表的指针将他们链接起来形成一条单向链表,这条链表就是 effectList。
用Redux作者Dan Abramov的话来说,effectList相对于Fiber树,就像圣诞树上的彩蛋
那么渲染阶段只需要遍历这条链表就能知道所有有effectTag的Fiber了。这部分代码在completeUnitOfWork函数中。
按照我们的架构,我们会给需要插入到DOM的Fiber设置effectTag = Placement;这对于某次增量更新来说没有问题,但对于首屏渲染却太低效了,毕竟对首屏渲染来说,所有Fiber节点对应的DOM节点都是需要渲染到页面上的。
难道我们要给所有Fiber赋值effectTag = Placement;再在渲染阶段一次次的执行DOM插入操作来生成一整棵DOM树?对于首屏渲染,我们需要稍微变通下。
当我们在调度阶段执行completeWork创建Fiber对应的DOM节点时,我们遍历一下这个Fiber节点的所有子节点,将子节点的DOM节点插入到创建的DOM节点下。(子Fiber的completeWork会先于父Fiber执行,所以当执行到父Fiber时,子Fiber一定存在对应的DOM节点)。代码见这里
这样当遍历到根Fiber节点时,我们已经有一棵构建好的离屏DOM树,这时候我们只需要设置根节点一个节点effectTag = Placement; 就能在渲染阶段一次性将整课DOM树挂载。
复习小课堂👩🎓:workInProgress指当前调度阶段正在处理的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版本我们还没有实现。
对于根Fiber初始化时,会产生一个update,update.payload为对应需要渲染的JSX(代码见这里),在根Fiber的beginWork中会触发这篇文章讲到的render流程。
篇幅有限,我们讲的很多都是宏观的东西,要了解细节还需要多多debug代码,把我们的Demo单步调试几遍。
这里再给你推荐一篇极好的React原理文章,配合本文食用效果极佳😊