基于ReactV16.13.1架构,从零实现React 🎉🎉🎉
👋👋👋 文章有任何不清楚的地方,欢迎给我提PR
我假设React是你日常开发的框架,在日复一日的开发中,你萌生了学习React源码的念头,在网上搜各种源码解析后,你发现这些教程可以分为2类:
-
《xx行代码带你实现迷你React》,《xx行代码实现React hook》这样短小精干的文章。如果你只是想花一点点时间了解下React的工作原理,我向你推荐 这篇文章,非常精彩。同时,这个仓库可能不适合你,因为他会花掉你很多时间。
-
《React Fiber原理》,《React expirationTime原理》这样摘录React源码讲解的文章。如果你想学习React源码,当你都不知道
Fiber是什么,不知道expirationTime对于React的意义时,这样的文章会给人“你讲解的代码我看懂了,但这些代码的作用是什么”的感觉。
这个仓库的存在就是为了解决这个问题。
简单来说,这个仓库有对应的一系列文章,文章会讲解React为什么要这么做,以及大体怎么做,但不会有大段的代码告诉你怎么做。
当你看完文章知道我们要做什么后,再来看仓库中具体的代码实现。
同时为了防止堆砌过多功能后,代码量太大影响你理解某个功能的实现,我为每个功能的实现打上一个git tag。
通过切换git tag浏览不同完成度的项目,执行npm start启动该版本下的Demo
v6实现了React的异步调度器Scheduler(也就是说我们实现了requestIdleCallback polyfill),并使用Scheduler实现了异步render,也就是React ConcurrentMode。
之前的版本中,我们都是同步执行render流程。在v6中,我们会为产生的update赋予一个优先级,高优先级的update会优先进入render流程。甚至当低优先级的update在render过程中我们触发了高优先级update,这时会搁置低优先级render转而处理高优先级render,这很酷,不是么😄
相对应的,v6相对v5增加了大量代码和一些全局变量。不过没关系,我会在之后的文章介绍这一切是如何做到的。新增功能如下:
- Scheduler模块
- fiber的优先级冒泡机制
- ConcurrentMode
这真是React内部最复杂的机制了,让人头秃👨🦲
在v3中我们实现了状态更新,直接在FunctionComponent函数体内触发更新会造成死循环,所以我们用计时器来触发。在业务中,我们一般是通过:
- 回调函数(ex:onClick)
useEffect hookClassComponent生命周期钩子
来触发。既然我们已经实现了useState hook,这一版我们就实现useEffect hook,新增功能如下:
-
useEffect hook首屏及再次渲染的完整逻辑
之前只能更新单一节点,这次实现了大名鼎鼎的React Diff算法,可以更新多个兄弟子节点了😄,新增功能如下:
- 节点支持
keyprop -
commit流程支持Deletion effectTag处理 -
reconcileChildrenArray支持非首次渲染的diff算法
ps:支持Deletion effectTag处理是为了应对:
// 首屏渲染的组件
[a, b, c]
// 再次渲染的组件
[a, null, c] 在这种情况下b fiber被标记为Deletion effectTag,对应的DOM节点需要删除
之前的版本只实现了首屏渲染的逻辑,即使在v2中实现了useState也只实现了useState(initialValue)带来的首屏渲染,在v3中我们终于实现状态更新啦,撒花🎉,新增功能如下:
-
useState hook对单一HostComponent的状态更新
ps:之所以只支持单一HostComponent,是因为还没有实现key以及diff算法,所以无法支持多个兄弟组件的更新
🐛当一个组件中使用多个useState hook且他们的更新函数同时触发,如示例中:
// 会造成页面逐渐卡顿并最终崩溃的例子
function App({name}) {
const [even, updateEven] = useState(0);
const [odd, updateOdd] = useState(1);
setTimeout(() => {
updateEven(even + 2);
updateOdd(odd + 2);
}, 2000);
return (
<ul>
<li key={0}>{even}</li>
<li key={1}>{odd}</li>
</ul>
)
}react-on-the-way会造成页面逐渐卡顿并最终崩溃。原因是updateEven和updateOdd方法会分别开始一次新的更新流程。
在其中每次更新流程执行到updateFunctionComponent时会调用App函数,在函数内部会调用计时器并在2000ms后又调用这2个更新函数,从而又开启新的更新流程。更新流程的数量会指数增加并最终崩溃。
造成这个问题的原因是我们还没有实现React的任务优先级机制与任务的批处理。在React中,
- 同步模式下同一个事件函数内的同步更新会被批处理,只产生一次更新流程
- 异步模式下所有更新都会经过优先级调度
为了实现React的页面更新逻辑,需要改变状态(state),我们有2条路可选:
- 实现
ClassComopnent setState - 实现
FunctionComopnent useState
考虑hook是React的趋势,我们优先实现useState,所以v2我们在第一版基础上增加了FunctionComponent相关首屏渲染,新增功能如下:
-
FunctionComponent类型组件的首屏渲染 -
hook架构体系 -
useState hook首屏渲染做的工作
我们的首要目标是实现React的页面更新逻辑,基于这个目标,我们首先实现了HostComponent的首屏渲染,新增功能如下:
- Render-Commit整体架构体系
-
HostComponent的首屏渲染
🙋♂️小讲堂:HostComponent是指原生DOM组件对应的JSX,在React执行时产生的组件
// 比如这样
<div>Hello</div>