2021SC@SDUSC
React简介
正如React项目官网所说,React是一个「用于构建用户界面的 JavaScript 库」。人们往往也会将React与Vue、Angular作为前端三大框架进行对比(但如今Angular的热度也越来越小了,而前两者的社区规模和热度仍在不断发展壮大)。React相对其他前端框架(说前端框架其实并不准确,因为React早已不再单纯地面向“前端”,本身也并不是所谓的“框架”)来说,不仅写法上更灵活,语法也更贴近ES标准,对Typescript也有着更好的支持。
同时,React本身在整体架构设计上更有优势,React日益庞大而成熟的技术栈便是得益于此。在浏览器端有我们有 ReactDOM 把React应用渲染在WEB页面,React 还可以使用 Node 进行服务器渲染(SSR),在小程序端也有Taro直接将React代码编译成对应的小程序,在原生 IOS 和 Android 平台更有React Native来帮助我们使用 React 来创建 Android 和 iOS 的原生应用。可以说,React已经真正做到了「一次学习,随处编写」。在之后的分析中我们也可以深入体会到这一点。
让我们先看一看React的代码,体会一下神奇的React是如何工作的:
class HelloMessage extends React.Component { render() { return ( <div> Hello World! </div> ); } } ReactDOM.render( <HelloMessage />, document.getElementById('root') );
这样的代码可以在html页面中id为root的节点上挂载我们的React应用,在其中显示 Hello World! 的文本。
上面的 HelloMessage 被称为类组件,它通过继承 React.Component 类,使用render()方法返回需要渲染的内容。当然还有另外一种更简洁的写法,我们是用函数式组件:
const App = ()=>{ return( <div> Hello world! </div> ) }; ReactDOM.render( <App />, document.getElementById("root") );
这两个组件是等价的。同时,组件之间还能构成嵌套关系,并且每个组件都拥有自己的生命周期,可以在自己生命周期的各个阶段执行不同的任务。并且,每个组件只会在自己本身的状态发生改变时进行部分更新,不会影响其他组件,性能非常高效。
总之,声明式、组件化、一次学习,跨平台编写 是React的三大特性。
主要架构
React 是一个相当庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染ReactDomServer等,其代码抽象化程度很高,嵌套层级非常深,理解起来是要费很大的力气的。今后的代码分析工作我们也会多多参考官方文档的源码概述以及其他大佬的分析文章,希望能站在巨人的肩膀上得到自己的一些粗浅的理解。
根据官方的源码概述,React项目主要包含以下四个模块:
1. React Core 核心API
React Core 中定义了所有全局 React API,比如:
- React.createElement()
- React.Component
- React.Children
因此 React Core 只包含定义组件必要的 API。它不包含协调算法或者其他平台特定的代码。它同时适用于 React DOM 和 React Native 组件。
React 核心代码对应源码的 packages/react
目录中。最终以react包发布在npm上,并被build为 react.js 运行在浏览器环境中,它会导出一个称为 React 的全局对象。
2. Renderers 渲染器
React 最初只是服务于 DOM,但是这之后被改造成能同时支持原生平台的 React Native。因此,在 React 内部机制中引入了“渲染器”这个概念。
渲染器用于管理一棵 React 树,使其根据底层平台进行不同的调用。
React DOM Renderer 将 React 组件渲染成 DOM。它实现了全局 ReactDOM API,这在npm上发布为 react-dom 包。这也最终被build为 react-dom.js 在浏览器环境中使用,导出一个 ReactDOM 的全局对象。它对应于源码的 packages/react-dom
。
React Native Renderer 将 React 组件渲染为 Native 视图。此渲染器在 React Native 内部使用。它对应于源码的packages/react-native-renderer
。
我们将主要分析 React DOM Renderer,对 React Native Renderer 可能只会略有涉及而不会深入分析。
3. Reconcilers 协调器
即便 React DOM 和 React Native 渲染器的区别很大,但也需要共享一些逻辑。特别是协调算法需要尽可能相似,这样可以让声明式渲染,自定义组件,state,生命周期方法和 refs 等特性,保持跨平台工作一致。
为了解决这个问题,不同的渲染器彼此共享一些代码。我们称 React 的这一部分为 “reconciler”。当处理类似于 setState() 这样的更新时,reconciler 会调用树中组件上的 render(),然后决定是否进行挂载,更新或是卸载操作。
在React 15 之前,主要使用的 Reconciler 是 Stack reconciler,目前官方已经停止了对其的使用。从 React 16 开始,React采用了 Fiber reconciler 作为默认的协调器,同时解决了 stack reconciler 中固有的问题。
它的主要目标是:
- 能够把可中断的任务切片处理。
- 能够调整优先级,重置并复用任务。
- 能够在父元素与子元素之间交错处理,以支持 React 中的布局。
- 能够在
render()
中返回多个元素。 - 更好地支持错误边界。
Fiber reconciler 对应于源码的 packages/react-reconciler
目录。
4. Event System 事件系统
React对原生事件(onclick() addEventListener()
之类)进行了封装以磨平不同平台、浏览器之间的差异。其源码在packages/react-dom/src/events
目录下。
小组分工安排
我们小组共三人,基于React的主要架构,我们决定各自负责相应模块的分析工作:
小组成员郭嘉伟(博客地址)负责React Core 核心API的相关代码分析。
我本人作为小组队长,负责 Renderers 渲染器的相关代码分析。
小组成员邢钟毓(博客地址)负责Reconcilers 协调器的相关代码分析。
此外,如果有小组成员提前结束了自己负责的部分的代码分析工作的话,将转而继续分析 Event System 事件系统 的相关代码。
源码拉取和概览
在拉取源码之前,需要在系统中配置好Nodejs环境,并配置npm源、yarn源和electron源为国内镜像源以加快依赖安装速度。
首先使用Git拉取react项目源码:git pull https://github.com/facebook/react.git
我们可以看到拉取下来的源码是这样的:
随后,在项目目录使用yarn包管理工具安装源码依赖:yarn
安装过程涉及到原生模块的编译和electron相关资源的下载,根据网速情况的不同,大概需要10-30分钟不等的时间。
(如果出现autoreconf 命令不存在
之类的报错问题,需要手动安装automake
和autoconf
工具)
当项目依赖全部安装完毕后,就可以正式开始分析工作了。
根据官方文档的源码概览介绍,寻找各个模块的对应源码位置,以React Core为例:
React Core 核心API:
且每个模块目录下都有自己的README.md说明文件,还是以 React Core 为例:
可见这样组织良好、文档详细的源码对于 contributor 还是相当友好的。
对于每个模块,其模块根目录下的index.js导出了主要的API,我们在分析过程中可以基于其导出的API进行分析,以 React Core 中的index.js为例:
/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ // Keep in sync with https://github.com/facebook/flow/blob/main/lib/react.js export type StatelessFunctionalComponent< P, > = React$StatelessFunctionalComponent<P>; export type ComponentType<-P> = React$ComponentType<P>; export type AbstractComponent< -Config, +Instance = mixed, > = React$AbstractComponent<Config, Instance>; export type ElementType = React$ElementType; export type Element<+C> = React$Element<C>; export type Key = React$Key; export type Ref<C> = React$Ref<C>; export type Node = React$Node; export type Context<T> = React$Context<T>; export type Portal = React$Portal; export type ElementProps<C> = React$ElementProps<C>; export type ElementConfig<C> = React$ElementConfig<C>; export type ElementRef<C> = React$ElementRef<C>; export type Config<Props, DefaultProps> = React$Config<Props, DefaultProps>; export type ChildrenArray<+T> = $ReadOnlyArray<ChildrenArray<T>> | T; // Export all exports so that they're available in tests. // We can't use export * from in Flow for some reason. export { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, act as unstable_act, Children, Component, Fragment, Profiler, PureComponent, StrictMode, Suspense, SuspenseList, cloneElement, createContext, createElement, createFactory, createMutableSource, createRef, forwardRef, isValidElement, lazy, memo, startTransition, unstable_Cache, unstable_DebugTracingMode, unstable_LegacyHidden, unstable_Offscreen, unstable_Scope, unstable_getCacheForType, unstable_useCacheRefresh, unstable_useOpaqueIdentifier, useCallback, useContext, useDebugValue, useDeferredValue, useEffect, useImperativeHandle, unstable_useInsertionEffect, useLayoutEffect, useMemo, useMutableSource, useSyncExternalStore, useSyncExternalStore as unstable_useSyncExternalStore, useReducer, useRef, useState, useTransition, version, } from './src/React';
其中export {xxx}
的部分就可以作为我们分析代码的索引进一步深究了。
关于React……想法
React实在是一个非常伟大的项目,作为我大一初刚步入大学校园时加入的第一个学生组织时所学习的第一个技术栈,我依稀记得自己写的第一个react app是一个类似于打地鼠的简单小网页:一个50×50大小的button在页面上随机游走,并根据点击次数不断加快变化速率。我从那个时刻就坚信React灵活的语法特性能够实现几乎任何页面需求。
react在大学两年里给我带来了相当多的项目经验,我曾和学生组织里志同道合的朋友们一起开发上线过许多大大小小的项目,从中真的积累了好多好多的实践经验。我从当初连类组件都搞不懂是怎么回事的react小白,到现在不光是课设系统、接的外包小程序还是桌面electron应用,都是使用react前端+koa后端自己一个人做的前后端全栈,过程中还学习了许多大佬的最佳实践,也摸索总结出了自己的一套开发流。在这门课中,我也很幸运能够说服老师增加React项目的源码分析题目。我希望能借此机会深入了解React内部的工作流程和原理,也希望能为更多像这样的开源项目贡献出自己的力量!