导语
React源码一直是许多初学者想看却觉得有门槛的内容。对于初学者来说,熟悉React的特性,了解React的设计是非常好的知识提升。下面的文章将会从一个简单的React组件开始,从初学者的角度来逐步深入React源码。
读懂以下的文章需要:
- 一些JavaScript(maybe TypeScript)基础
- 曾经编写过React代码
- 一些计算机基础
接下来,就让我们一步步探索React源码。
一个简单的React组件
在React中,我们使用JSX/TSX进行开发。目前最流行,也是逻辑感最强的写法是用class来描述一个组件:
import React from 'react';
interface IProps {
// some properties defined on props...
}
interface IState {
msg: string;
// some other properties defined on state...
}
class MyComponent extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
msg: 'Hello world.'
};
}
private handleClick = () => {
this.setState({
msg: 'Hello React.'
});
}
render() {
return <div onClick={this.handleClick}>{ this.state.msg }</div>
}
}
如上,便是一个最简单的React组件。听过有人说,“我的工作就是 flex + onClick + setState 一把梭”。其实这就像这个简单组件一样,包含了对数据的处理、显示与更新方法,这也是前端工作人员最主要的工作之一。但是刚开始上手React时,写完这个代码后会发现,其中有许多疑惑点我们是没明白的。
从目标代码来看这个组件做了什么
先梳理一下我们在源代码中写了什么逻辑:
定义这个类的时候,我们继承了React中的Component类,使用泛型为props和state定义了类型。在组件的state中定义了需要显示的msg变量,并为构造出的DOM挂载点击事件。
熟悉JavaScript / TypeScript的同学会知道,这门语言里的类本质上是一个构造函数。babel会将它进行对应的语法转义,使其返回一个所谓的JSX对象。先抛开事件handleClick的挂载(这个是独立的React事件模块,后面会单独挑出来讲),我们先看一从babel编译JSX返回的代码结构,可运行的最终代码(简化版)应该是这样的:
"use strict";
// 引入react
var _react = _interopRequireDefault(require("react"));
function _inherits(subClass, superClass) {
// 重新构建subClass.prototype,将subClass.prototype上的constructor指向自身
// 将subClass.__proto__指向superClass
}
function _createClass(Constructor, protoProps, staticProps) {
// 挂载一般(public/protected/private/default)成员函数
// 挂载static成员函数
}
function _createSuper(Derived) {
// 返回父类构造函数
}
var MyComponent = (function(_React$Component) {
// 处理下面的MyComponent,让它“继承”传入的参数
_inherits(MyComponent, _React$Component);
var _super = _createSuper(MyComponent);
// 下面这个就是真正的class MyComponent中的内容了
// 由于函数提升,它会被提升到作用域的顶端
function MyComponent() {
var _this;
// 执行父类构造函数
_this = _super.call(this, props);
// 完成在constructor中编写的内容
// 在返回的对象上继续挂载初始化的数据
_defineProperty(_this, "handleClick", function() {
// ...
})
return _this;
}
// 在构造器上挂载成员函数
_createClass(MyComponent, [{
key: "render",
value: function render() {
return /*#__PURE__*/_react.default.createElement("div", {
onClick: this.handleClick
}, this.state.msg);
}
}]);
return MyComponent;
})(_react.default.Component)
⬆️ 以上就是浏览器可运行版的JavaScript代码(简化版)。
由此可见,我们在JSX模板中编写的html部分本质上就是React.createElement的语法糖,解析器最终将把render函数的返回值解析成React.createElement("div")。
进一步看,我们发现了代码黑盒的部分,也就是React做了处理的部分有两处,这可以引导我们进入React源码的世界:
- 把
<div></div>解析成React.createElement("div") - 把我们的构造函数“继承”了
React.Component
React.createElement会构造出什么样的对象?
通过查看源码,可以发现,createElement有三个参数:type(想要构造DOM元素的种类),config(该对象的ref、key等等我们开发过程常用到的配置项)和children(子节点)。createElement主要针对开发环境不合理的代码给出一些warning,并将children挂载到了config.props上。为了simplify代码,这里用的基本上都是面向过程的写法。
最后,createElement返回了一个ReactElement,继续查看源码:
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// 该对象作为ReactElement的标识
?typeof: REACT_ELEMENT_TYPE,
// 不用多说,一些ReactElement固有的属性,开发中常见
type: type,
key: key,
ref: ref,
props: props,
// 记录创建这个ReactElement的组件
// 先猜测是用于区分同一个ReactElement在不同组件中不同表现使用的
_owner: owner,
}
return element;
}
可以看到,ReactElement其实只是一个简单对象,上面解构挂载了刚才我们提到了config和处理完的config.props之类的内容,仅此而已。
React.Component作为组件的父类,其中做了些什么处理?
查看源码,可以看到React.Component中挂载了以下内容:
function Component(props, context, updater) {
this.props = props;
// context和updater牵涉到了其它模块,因此放到后续深入
this.context = context;
// ReactNoopUpdateQueue是一个只提供warning的空updater
this.updater = updater || ReactNoopUpdateQueue;
this.refs = {};
}
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
}
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
function PureComponent() {
// ...
}
我们看到,Component上会挂载props,context和updater这些成员变量,同时会在原型上挂载公用的setState和forceUpdate方法。从变量名上看到,setState和forceUpdate应该是内部维护了一个更新队列,调用某个组件内的setState时,它被加入了这个更新队列,按顺序更新。
如果把React比作一个IDE,把页面从JavaScript到渲染上屏的过程比作运行一个C++程序,那么上面这些代码相当于为render做好了一些静态的准备。从ReactDOM.render开始,后面的过程就好比这个C++程序真正进入了运行时。那么下面开始才是真正的重点:React如何帮助我们render以及update一个组件?
React组件的创建与更新
在一个React项目的根目录处,我们通常会写下如下的代码:
// ...
ReactDOM.render(<App />, document.getElementById('app'));
// ...
这一行代码启动了react构造我们组件树,处理数据并渲染到屏幕的过程,查看它的源码:
function render(
element: React$Element<any>,
container: Container,
callback?: Function,
) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
我们使用React的初衷是为了“构建快速响应的用户界面”。所以我们最关注的就是React如何把静止的html“hydrate”成响应式的React组件,并能根据用户操作动态地更新。
从legacyRenderSubtreeIntoContainer函数开始,我们已经可以开始观察根结点是如何被hydrate成组件的:
function legacyRenderSubtreeIntoContainer(
// ...
container: Container
) {
// 要被hydrate的节点
let root: RootType = (container._reactRootContainer: any);
// React Fiber链表的头节点
let fiberRoot;
if (!root) {
// 开始hydrate
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
// 得到root的结果后,便得到React Fiber的头节点
fiberRoot = root._internalRoot;
// ...
}
// ...
}
沿着legacyCreateRootFromDOMContainer构造的链路,我们可以追踪到ReactDOMRoot.js,发现里面引入并调用的这两个正是创建与更新React组件的函数:
import {
createContainer,
updateContainer,
} from 'react-reconciler/src/ReactFiberReconciler';
我们在进入该函数前,首先需要补充关于React reconciler的知识,否则后面将很难读懂代码。
React中有许多功能,其中最核心的调度更新部分就是协调器(reconciler),它和渲染器(renderer)模块是独立的,并且都是可插拔的(pluginnable)。React所做的工作相当于一个编译器,它把开发者传递的数据修改指令翻译成可同步视图更新的代码,并交给机器运行。之所以没有说“交给浏览器主线程运行”,正是因为reconciler和renderer的可插拔性。React可以使用同一个reconciler和不同的renderer,让同一套代码运转在不同的平台上(硬件、VR、原生APP等)。
除了要明白reconciler在React中的位置,我们还需要知道React历史上的两套调度算法:
stack reconciler
曾经(React 15及以前),React使用的是一套名为stack reconciler的调度算法。
如何理解stack reconciler?在计算机里,函数是一个子程序,计算机主要使用栈来存放函数调用过程中的数据(参数、返回地址等)。当层层的函数嵌套地调用时,栈会越来越深,而当被调用的函数接连执行完毕时,栈不断地弹出顶部的内容,除了得到了返回值以外,其它的状态又回到了函数调用前。
stack reconciler也是这样的思路。当我们开始构建/更新组件树时,更新组件的过程就是一个个子程序,不断地被压入栈中,在子程序执行完毕后才会接连弹出。而这个调度的过程是一个同步的过程,也就是说,当组件树很深时,一次更新的耗时也会相应提高。
众所周知,浏览器的渲染进程为了保证页面显示和脚本内的数据保持一致,同时包含了JavaScript解释器(一条线程)和渲染器(一条线程),而这两条线程又是互斥的。因此,当stack reconciler的一次更新非常庞大时,JavaScript线程会一直阻塞渲染器线程,使得页面没办法得到更新,体现出来就是掉帧和卡顿。
不同于后台服务,UI的更新速度会大大影响到用户的体验,稍慢几毫秒,用户所体验到的掉帧感觉就会非常明显,同时也会影响到交互。因此,React提出了一种新思路,实现一个fiber,主动地分片调度,使页面能及时得到更新,保证帧率不会降低得太严重。
fiber
React 16版本以后,React fiber正式面世。Fiber并非React提出的新概念,理解React fiber,首先要理解fiber。
Fiber是在用户态下存在的,操作系统的内核态并不知道fiber的存在。Fiber是线程里更细粒度的存在,但就像React fiber一样,它是我们程序员自己实现的,并不是由操作系统调度的。在操作系统中,线程是由内核进行抢占式调度的,但fiber不同,它的调度算法是由我们程序员自己定义的,因此它会选择恰当的时机把操作权主动交还给线程(或者其他fiber)。
也许这个时候我们想到了coroutine,我们也可以这样来理解:coroutine是一种语言级别的构造(像go、kotlin里面就有coroutine),而fiber是coroutine的一种实现。
Fibers describe essentially the same concept as coroutines. The distinction, if there is any, is that coroutines are a language-level construct, a form of control flow, while fibers are a systems-level construct, viewed as threads that happen to not run concurrently. It is contentious which of the two concepts has priority: fibers may be viewed as an implementation of coroutines, or as a substrate on which to implement coroutines.
以上也是wikipedia中它们区别的描述,当然它这里提到的systems-level construct表示的应该是windows中实现的fiber。像React fiber就并非是systems-level的。过多的我们不展开,还是要回到React reconciler。
系列文章
React源码学习讨论(二):React reconciler调度算法(编写中……)
(更多……)