人人都是前端架构师:我来带你阅读 React18 源码!

简介: 人人都是前端架构师:我来带你阅读 React18 源码!

读源码的目的


提到阅读源码,很多人会很畏惧,也有很多人会很向往。

当我们能够熟练使用别人提供给我们的工具时,要想更进一步,难免要去研究工具背后的事情。

这也是每一个资深技术人都应该做的事情。

可是,我们不能忽视阅读源码的难度,因为编写这些工具的人通常都是业内顶级的工程师,他们的技术水平非常高。

所幸,你遇到了这篇文章。

我会带你由浅入深的阅读 React18 的源码。

首先 React 的源码非常庞大,我们不要详尽地去看它的所有东西。我们主要专注于 React 的设计,看看他都有哪些最佳实践和模式,并且把这些东西融入到自己的代码库中。


Monorepo


React 的代码是 monorepo 模式,它将多个不同的项目放到了一个存储库中。所以根目录中没有大家熟悉的 src 目录,取而代之的是 packages 目录,packages 是前端 monorepo 约定俗成的文件夹命名。

React 存储库中包含了 30 多个包。大家熟悉的有 react、react-dom、react-server 和 react-devtools。

monorepo 的优势是可以将多个独立的部分组成一个大型项目,并让本地的配置更加容易,而且这些独立部分之间的代码可重用性很好。

但是 monorepo 并不是一个完美的解决方案,在进行子包的拆分时,我们需要投入更多的设计和思考。

通常来说,我们在本地环境和生产环境是不一样的,所以增加了部署的复杂性。同时也增加爱了整个代码库的复杂性。

但是一旦你把 monorepo 的工作流程弄清楚之后,上面这些问题就没有那么明显了。


从哪儿开始?


其实大多数人在阅读陌生的代码时,都会感觉到非常困惑。如果没有人给你梳理流程,介绍模块,这种困惑感会更强。

所以我们要从某一个位置作为开始。

阅读任何一个库的源码,我们都可以从它的第一个 API 开始。

那么 React 的第一个 API 是什么呢?

一定是下面这段代码:


import ReactDOM from 'react-dom'
const root = ReactDOM.createRoot(container)
root.render(element)

这段代码是 React 18 中将组件渲染到 DOM 上面。

在 SPA 项目中通常只会运行一次,我们就从这里开始。

你可能发现了,react-dom 并不是核心包,它是和浏览器绑定一起使用的。react 的核心包通过某种方式,可以让它在不同的环境中使用。


createRoot


createRoot 函数包装了一个内部函数,并做了一些简单的验证逻辑。

它没有直接提供实现,而是分离了验证逻辑并且把真正的实现逻辑放到了一个单独的文件中。


function createRoot(
container: Element | Document | DocumentFragment,
 options?: CreateRootOptions
): RootType {
  if (__DEV__) {
    if (!Internals.usingClientEntryPoint && !__UMD__) {
      console.error(
        'You are importing createRoot from "react-dom" which is not supported. ' +
        'You should instead import it from "react-dom/client".'
      )
    }
  }
  return createRootImpl(container, options)
}

包装的内部函数名后面有 Impl 后缀,表示它是一个负责具体实现的私有函数。React 中有大量这种命名的习惯。

但我不认为这个命名是一个好的习惯,如果采用 createRootInstance 或者 cerateRootEntity 这种更具体的名称可能会更好理解。

进入这个单独的文件。


export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions
): RootType {
  if (!isValidContainer(container)) {
    throw new Error(
      'createRoot(...): Target container is not a DOM element.'
    )
  }
  // ...
  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks
  )
  // ...
  return new ReactDOMRoot(root)
}

我们发现这个函数实际上和外层的函数是同名的,只是外层使用了别名导出来避免命名冲突。我一般不会将函数名重名,除非我要实现多态。

createRoot 函数做的第一件事就是做出验证,如果不符合预期就退出。这种做法我用了很多年,是避免多层嵌套和复杂 if 语句的常规操作。

这个函数大概有 80 行,我移除了细节部分,现在我们可以专注于核心的部分。

它做的事情就是将容器和 React 的协调器建立连接。它使用了工厂函数 createContainer,同时也使用 new 来创建 ReactDOMRoot。这似乎有些奇怪。

再来看 ReactDOMRoot 这个函数。


function ReactDOMRoot(internalRoot: FiberRoot) {
  this._internalRoot = internalRoot
}

它非常简单,只有一行代码而已。它会将 internalRoot 挂载到 this 上面。

再回到 createContainer 函数。


ReactDOMHydrationRoot.prototype.render =
  ReactDOMRoot.prototype.render = function (
    children: ReactNodeList
  ): void {
    const root = this._internalRoot
    if (root === null) {
      throw new Error('Cannot update an unmounted root.')
    }
    if (__DEV__) {
      // ...
    }
    updateContainer(children, root, null, null)
  }

它使用了多重赋值,将一个方法挂载到了 ReactDOMHydrationRoot 和 ReactDOMRoot 的 prototype 的 render 方法中。

使用了 prototype 的好处是,我们可以通过 instanceof 来检查某个对象是否为某个类型。但是我不怎么会使用 prototype,我更喜欢工厂函数和闭包。

依赖原型的另一个好处是性能。使用闭包会造成一定的开销,但是原型并不会,所有被添加到原型上的方法只会被创建一次,但是闭包不会这样。


连接到协调器


创建完 ReactDOMRoot,接下来就是 updateContainer 方法,它是协调器的一部分。


export const updateContainer = enableNewReconciler
  ? updateContainer_new
  : updateContainer_old

使用条件导出的方式是很少见的。updateContainer 的逻辑是通过一个叫做 enableNewReconciler 的标志位来区分到底用哪一套逻辑。这种方式通常用于渐进式部署或者 AB 测试。

不同的方法也被放到了不同的文件夹中,分别是 .new.js 和  .old.js,它通过后缀名的不同来区分。React 中有大量这种命名方式的文件。

我们来看其中一个。


export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function
): Lane {
  // ...
  const eventTime = requestEventTime()
  const lane = requestUpdateLane(current)
  if (enableSchedulingProfiler) {
    markRenderScheduled(lane)
  }
  // ...
  const update = createUpdate(eventTime, lane)
  // Caution: React DevTools currently depends on this property
  // being called "element".
  update.payload = { element }
  // ...
  const root = enqueueUpdate(current, update, lane)
  // ...
  return lane
}

它的作用就是将下一个组件树通过 enqueueUpdate 更新到队列中。

值得注意的是,update 下面的那两行注释。这对一些从代码上看很不明显的操作进行解释,是一种非常好的例子。

接下来我们应该去看协调器的代码了,但是在这之前,我建议先去看看组件的内部结构。


什么是组件?


ReactDOMRoot.prototype.render 函数需要一个组件作为参数。

组件是一个对象,但是 React 不会让使用者用对象的形式表示 UI,那样的话,体验上来说实在是太糟糕了。所以我们通常会使用 JSX 来编写 React 代码。

然后在项目真正运行之前,通过转译器将 JSX 转换为创建对象的函数调用。

每个 JSX 元素最终都会被转换为 React.createElement 方法,当然我们也可以直接使用这个函数来创建 UI,只是在语法上非常抽象。


export function createElement(type, config, children) {
  let propName
  // Reserved names are extracted
  const props = {}
  let key = null
  let ref = null
  let self = null
  let source = null
  if (config != null) {
    // ...
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName]
      }
    }
  }
  const childrenLength = arguments.length - 2
  if (childrenLength === 1) {
    props.children = children
  } else if (childrenLength > 1) {
    // ...
  }
  // ...
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props
  )
}

其实在 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 这个函数。


const ReactElement = function (
  type,
  key,
  ref,
  self,
  source,
  owner,
  props
) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  }
  if (__DEV__) {
    element._store = {}
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    })
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    })
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    })
    // ...
  }
  return element
}

这个方法没有做什么特殊的事情,它只是为组件对象的属性分配了正确的值。

不过它有一个需要注意的地方,就是在开发环境中,使用了 defineProperty 方法定义了 N 个属性。这个方法可以让我们更加精细地控制每个属性的行为。

设置 writable 为 false,意味着以后不可以给它重新赋值或删除它。

设置 enumerable 为 false,意味着它不可以在 for...in 语法和 Object.keys 方法中被遍历到。

设置 configurable 为 false,意味着设置了这些选项之后不可以再修改。

这个方法在开发应用中很少会用到,因为对应用来说,不需要对对象的属性进行这种控制。但是在开发一个库时,可能会经常用到,因为它可以控制库对外开放的对象的内部结构。防止它们出现在错误的地方。

$$typeof 被设置为 REACT_ELEMENT_TYPE,这是一个 Symbol 类型的变量。因为 ReactElement 是一个工厂函数,所以无法使用 instanceof 来检测它,所以用 Symbol 是一个很好的方法。


渲染器和协调器的交互


现在我们知道了什么是组件,也知道了它们是怎么被创建的。现在我们更进一步,来看看元素是怎么样被渲染到屏幕上的。

我们需要阅读协调器的文档,它的源码在 react-reconciler 子包中。

渲染器有一个 diffing 算法,它可以找出组件的变化并且通知渲染器重新生成组件。渲染器定义了某些方法来处理组件的渲染,但是它不负责调用它们,因为它不知道什么时候调用它们。

这些方法由协调器调用。

react-reconciler 中的 README.md 详细介绍了它是如何与渲染器进行交互的。

本质上,每种渲染器都需要遵循目标环境的约定,它必须提供给协调器所要依赖的很多方法和属性。这意味着只要你的渲染器拥有这些方法和属性,就可以自己创建渲染器将 UI 呈现在你想要的任何位置,而不仅仅是浏览器或者原生应用,比如说嵌入式系统中。


const Reconciler = require('react-reconciler')
const HostConfig = {
  createInstance(type, props) {
    // e.g. DOM renderer returns a DOM node
  },
  // ...
  supportsMutation: true, // it works by mutating nodes
  appendChild(parent, child) {
    // e.g. DOM renderer would call .appendChild() here
  },
  // ...
}
const MyRenderer = Reconciler(HostConfig)
const RendererPublicAPI = {
  render(element, container, callback) {
    // Call MyRenderer.updateContainer() to schedule changes on the roots.
    // See ReactDOM, React Native, or React ART for practical examples.
  },
}
module.exports = RendererPublicAPIÏ

我们需要实现的接口是 HostConfig,它这样命名是因为渲染器正在将 React 连接到主机环境,当我看到 Config 这个词时,我能想象到一些环境变量相关的东西。

毕竟命名是计算机科学中最难的两大问题之一。


渲染器方法


渲染器具有很高的复杂度,我不会去讲解每个功能。相反我会专注于它最重要的功能:如何解决将内容渲染到屏幕上的难题。

需要注意,react-reconciler API 不保证稳定性。它会比渲染器或者 core 更加频繁地调整。

createInstance 是将每个组件可视化的方法。


export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object
): Instance {
  let parentNamespace: string
  if (__DEV__) {
    // ...
  } else {
    parentNamespace = hostContext
  }
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace
  )
  //...
  return domElement
}

现在看到的是处理 DOM 元素的渲染器,在理论上,渲染器可以在屏幕上绘制任何内容。

这个函数是在做一个条件赋值,它使用工程函数创建了一个 DOM 元素。

再来看对纯文本节点进行操作的 createTextInstance 函数。


export function createTextInstance(
  text: string,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object
): TextInstance {
  if (__DEV__) {
    const hostContextDev = hostContext
    validateDOMNesting(null, text, hostContextDev.ancestorInfo)
  }
  const textNode: TextInstance = createTextNode(
    text,
    rootContainerInstance
  )
  precacheFiberNode(internalInstanceHandle, textNode)
  return textNode
}

它和我们之前看到的很多模式都类似,在开发模式下进行必要的验证,然后将具体的创建任务委托给另外一个工厂函数。实际上 React 源码中存在了大量的这种模式。

上面提到的这两个函数会创建所有 DOM 元素,然后把这些元素添加到真正的 DOM 中去。

appendChild 是具体的实现。


export function appendChild(
  parentInstance: Instance,
  child: Instance | TextInstance
): void {
  parentInstance.appendChild(child)
}

它直接接收一个 DOM 元素的实例,并调用它的 appendChild 方法来添加渲染的组件。

因为我们很难确定插入组件的确切位置,所以这需要在父元素的帮助下完成。


总结


到这里,我们对 React 的渲染过程有了一个初步的理解,相信你已经搞懂了渲染器的工作原理和它们的实现方法,你应该也知道它们是如何连接到协调器上面,以及在 React 内部是如何表示组件的。

现在你应该明白了:阅读源码并不难。

接下来你可以自由发挥,大胆地去探索 React 源码的更多内容吧!



相关文章
|
JavaScript 前端开发 Java
制造业ERP源码,工厂ERP管理系统,前端框架:Vue,后端框架:SpringBoot
这是一套基于SpringBoot+Vue技术栈开发的ERP企业管理系统,采用Java语言与vscode工具。系统涵盖采购/销售、出入库、生产、品质管理等功能,整合客户与供应商数据,支持在线协同和业务全流程管控。同时提供主数据管理、权限控制、工作流审批、报表自定义及打印、在线报表开发和自定义表单功能,助力企业实现高效自动化管理,并通过UniAPP实现移动端支持,满足多场景应用需求。
1274 1
|
人工智能 安全 Java
智慧工地源码,Java语言开发,微服务架构,支持分布式和集群部署,多端覆盖
智慧工地是“互联网+建筑工地”的创新模式,基于物联网、移动互联网、BIM、大数据、人工智能等技术,实现对施工现场人员、设备、材料、安全等环节的智能化管理。其解决方案涵盖数据大屏、移动APP和PC管理端,采用高性能Java微服务架构,支持分布式与集群部署,结合Redis、消息队列等技术确保系统稳定高效。通过大数据驱动决策、物联网实时监测预警及AI智能视频监控,消除数据孤岛,提升项目可控性与安全性。智慧工地提供专家级远程管理服务,助力施工质量和安全管理升级,同时依托可扩展平台、多端应用和丰富设备接口,满足多样化需求,推动建筑行业数字化转型。
467 5
|
9月前
|
机器学习/深度学习 人工智能 缓存
面向边缘通用智能的多大语言模型系统:架构、信任与编排——论文阅读
本文提出面向边缘通用智能的多大语言模型(Multi-LLM)系统,通过协同架构、信任机制与动态编排,突破传统边缘AI的局限。融合合作、竞争与集成三种范式,结合模型压缩、分布式推理与上下文优化技术,实现高效、可靠、低延迟的边缘智能,推动复杂场景下的泛化与自主决策能力。
793 3
面向边缘通用智能的多大语言模型系统:架构、信任与编排——论文阅读
|
10月前
|
机器学习/深度学习 人工智能 资源调度
MicroNAS:面向MCU的零样本神经架构搜索——论文阅读
MicroNAS是一种专为微控制器单元(MCU)设计的零样本神经架构搜索(NAS)框架,无需训练即可通过理论驱动的性能指标评估网络架构。相比传统NAS方法,其搜索效率提升高达1104倍,同时兼顾精度与硬件效率,适用于边缘计算场景。该框架结合神经切线核(NTK)条件数、线性区域计数及硬件感知延迟模型,实现快速、高效的架构搜索,为资源受限设备上的AI部署提供了新思路。
580 2
MicroNAS:面向MCU的零样本神经架构搜索——论文阅读
|
10月前
|
机器学习/深度学习 人工智能 vr&ar
H4H:面向AR/VR应用的NPU-CIM异构系统混合卷积-Transformer架构搜索——论文阅读
H4H是一种面向AR/VR应用的混合卷积-Transformer架构,基于NPU-CIM异构系统,通过神经架构搜索实现高效模型设计。该架构结合卷积神经网络(CNN)的局部特征提取与视觉Transformer(ViT)的全局信息处理能力,提升模型性能与效率。通过两阶段增量训练策略,缓解混合模型训练中的梯度冲突问题,并利用异构计算资源优化推理延迟与能耗。实验表明,H4H在相同准确率下显著降低延迟和功耗,为AR/VR设备上的边缘AI推理提供了高效解决方案。
1565 0
|
移动开发 前端开发 JavaScript
Vue与React两大前端框架的主要差异点
以上就是Vue和React的主要差异点,希望对你有所帮助。在选择使用哪一个框架时,需要根据项目的具体需求和团队的技术栈来决定。
768 83
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
766 70
|
存储 前端开发 JavaScript
|
运维 安全 数据可视化
采用PHP+Vue技术架构的不良事件管理系统(源码)
本系统为医院安全(不良)事件管理工具,支持快速上报、流程化处理与多维度分析,助力识别风险、优化管理。采用PHP+Vue技术架构,功能涵盖事件上报、追踪整改、数据统计及PDCA改进等。
387 0
|
机器学习/深度学习 人工智能 自然语言处理
DeepSeek Artifacts:在线实时预览的前端 AI 编程工具,基于DeepSeek V3快速生成React App
DeepSeek Artifacts是Hugging Face推出的免费AI编程工具,基于DeepSeek V3,支持快速生成React和Tailwind CSS代码,适合快速原型开发和前端组件构建。
2998 39
DeepSeek Artifacts:在线实时预览的前端 AI 编程工具,基于DeepSeek V3快速生成React App