在当下就业环境下,只会框架的使用是不够的,这是很多初级工程师面临的困境。想要脱颖而出,成为具备手写框架、源码贡献等技能的高手,才能拥有更多的机会。想要这种摆脱框架黑盒状态。本文是你的最佳选择,不仅可以深入学习React工作原理,还可以掌握源码调试技巧,手写框架的实践,以及成为源码Contributor的方法论,为你提供方位的指导和实践,助力成为一个真正有实力的高级技术人才 。
一、前置基础知识
Fiber架构
Fiber 是 React 16.x 开始新增的一个数据结构,React 将每个节点都封装到了一个 Fiber 中,使得整个 DOM 树的渲染任务被分成了一个一个小片,每个 Fiber 中通过这样的指针相互联系 ,最后形成一个链表树的结构
双缓冲架构
我们的 React 中,存在两颗上述的 Fiber 链表树,一颗是用于渲染页面的 current Fiber 树,一颗是 workInProgress Fiber 树,我们用于渲染当前页面的是 current Fiber 树,而我们在整个更新过程中会构建一颗叫做 workInProgress Fiber 树。
Lanes
上面提到了,我们的 Fiber 操作过程是可以中断的,当有紧急事进入时,React 会优先处理紧急任务,而判断任务的紧急程度就需要一套指标,他就是 Lanes 系统:
lanes 使用31位二进制来表示优先级车道,共31条, 位数越小(1的位置越靠右)表示优先级越高。在实际使用中,我们会有一个 31位的二进制数来标识我们的任务,,如果他的某一位对应的是 1 ,那么他的某个优先级就有任务了,反之就是空闲的,你可以理解成我们把任务看成我一辆辆汽车,他们需要在不同的车道上行驶才不会相互影响,但是右边的车道可以向左边超车(优先级高)。这就是为什么我们称之为 车道模型。
二、React 的更新流程
在 React 从我们的代码生成我们的页面和更新我们的页面的过程中,分为 Schedule【调度】、Reconcile【协调】、commit【提交】三个阶段:
Schedule 阶段用于通过 lane 调度任务的优先级,使高优先级任务优先进入Reconcile,并且提供中断和恢复机制。
reconcile 阶段用于 fiber 树结构的更新,它深度优先递归了整个 React 结构树,为每一个节点创建 Fiber,通过 diff算法 和当前的结构进行对比,判断对应节点需要进行了新增,修改,删除或者直接复用,并在 Fiber 上给他们打上对应的标记
commit 阶段又称 render【渲染】阶段,顾名思义它做的就是把刚刚在 Fiber 上做的标记内容同步到对应的真实 DOM 中去,这个过程又分为三个阶段:before mutation 阶段 (dom 操作之前)、mutation 阶段 (dom 操作)、layout 阶段 (dom 操作之后)
三、从 JSX 代码开始
在了解了一些基础知识和 React 的整体运行层次后,我们来完整的梳理一次从我们的代码到最后成为 HTML 的过程中发生了什么,我们还是从我们编写的代码开始讲起:
在 React 中,对于用户而言,我们一般都编写对应的 jsx 代码来生成我们的组件
但是在 React 中 jsx 代码是不能直接被处理的, jsx 代码需要被转变为对应的 jsx 结构(也就是之前版本的 React.createElement 函数)
四、createRoot
我们先看 createRoot 这个方法具体做了什么事情。这个方法来自 react-dom 这个包。我们可以在源码中 packages / react-dom / src / client / ReactDOMRoot.js 中找到 createRoot 的具体实现(前面在 ReactDOM.js 做了一些关于环境的条件判断,可先忽略)
createRoot 函数有两个参数 container 和 options,其中 options 是可选参数,本章为了简单起见先不讨论;
该函数大概实现的功能就是:
创建容器对象 FiberRootNode
事件委托处理
根据 FiberRootNode 对象返回 ReactDOMRoot 对象
其实在 React 17 版本之后,JSX 不会再自动转为 React.createElement 了。因为在转换这个函数之前,都必须导入 React 才行。如果你对 JSX 的工作原理不够了解的话,可能不能直观的感受这个变化是什么。这个更新可以允许构建工具使用没有附加到 React 对象不同的功能。
export function jsx(type, config, maybeKey) {
let propName
// Reserved names are extracted
const props = {}
let key = null
let ref = null
// ...
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName]
}
}
// ...
return ReactElement(
type,
key,
ref,
undefined,
undefined,
ReactCurrentOwner.current,
props
)
}
上面的 jsx 函数和 createElement 函数实现很像。最终它们都委托一个 ReactElement 工厂函数来创建真正的组件对象。这里其实就出现了一个重要的问题,什么时候应该提取一个通用函数?就像这个 ReactElement 函数一样。通常来说,抽取通用函数,可以在视觉上消除重复的代码。但是我们在一开始并不知道应该抽取哪些代码作为通用函数,往往都是在不断复制之后才知道应该复制哪些代码。重复的代码看起来很烦人,但是管理它们并不难。相反,错误的抽象就可能会制造复杂性。现在回想起来,这个问题曾经在我工作生涯的早期多次犯过,只是当时没有意识不到这个问题。我们再来看 ReactElement 这个函数。