作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。
本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。
本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。
这个专栏的写作是为了记录在阅读 React 源码和 React 源码构造思想的时候的心得体会编写的,这个专栏将从 React 的怎么样将一个 jsx 组件生成出对应的 DOM 出发,讲解 React 源码的运行原理和设计思路,组件的生命周期如何绑定,React 的虚拟DOM 为什么比真实的DOM 高效,并且简要谈谈 React Hooks 的设计。下面我们开始我们的内容:
这篇教程基于的是 React 的 18.1.0 版本,源码地址:https://github.com/facebook/react。因为不同版本的源码和运行原理可能完全不一样,所以你需要注意你阅读的源码的版本。以下是一些主要的注意事项:
本文的一些原理和设计至少从 React 16 开始出现,因为 React 16 首次引入了 Fiber 架构,使得其相对于上一代发生了翻天覆地的变化。它使得 React 从组件渲染更新,任务调度,生命周期等方面发生的变化,整体也进行了大量的重构,这部分我们会在后续讲到。本教程的大部分内容将不适用于 React 16 之前用户阅读。
React 16.8 引入了 Hooks 的概念,通过 Hooks 解决嵌套问题,使得代码更加简洁。
React 17.0 引入了 concurrent mode 并发模式和 lanes 车道和,来调度任务和规划任务的优先级,这部分我们会在后续讲到。
React 18.0 则更新了 Root API 更新,可以为一个 React App 创建多个根节点。
我们从 Jsx 出发来讲 React 的运作和源码实现,首先这是一个我们常见的 React 组件结构:
function App() {
return <h1 class="test" key="122" >Hello World</h1>;
}
React 的处理原理是将 jsx 代码转换成自己的 createElement
函数进行处理,在 React(React16.x 及之前), 这个处理是由 React 实现的,以下是经过转化的内容,我们可以对照着 API 来看,第一个参数是类型,第二个是配置项,第三个是子元素
//function createElement(type, config, children){ }
function App() {
return React.createElement("h1", {
class: "test"
key:"122"
}, "Hello World");
}
在 React17.0 之后,Raect 和 babel 进行了合作,使用 babel 进行上述的处理,所以在 React17.0 我们不用引入 React 也可以运行我们的 jsx,在 React16.x 及之前则不行,即使我们的代码没用用到 React ,我们也需要引入 React 。React17.0 使用了一个新的结构,jsx 来处理我们的 jsx 元素,第一个参数是类型,第二个是配置项和子元素,第三个是 key 。
元素中的的配置项将以 key - value 的形式来配置,而子元素将会放在一个 children 项中,若子结构中只有一个子元素,那么 children 就是一个 jsx(),若有多个元素时,则会转为数组:
//export function jsx(type, config, maybeKey) {}
function App() {
return jsx("h1", {
class: "test",
children: "Hello World"
}, "122");
}
而之后的逻辑基本类似,createElement 和 jsx 两个函数的最终目的都是返回一个固定格式的 React 元素,我们可以通过打印一个元素来得到它。React 元素由 type、key、ref、props、_owner 和 一个标识 React 元素组成,_store, _self, _source 三个内部属性,在开发模式下才会被创建,这里我们按下不表。现在我们的目标就是生成一个这样的元素:
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE, //标识
type: type, //元素的类型, 可以为 HTML 元素或 React 组件
key: key, //元素在其同级兄弟中的的身份标志(唯一键值)
ref: ref, //对组件实例的引用
props: props, //组件或 HTML 元素的属性值
_owner: owner //创建该元素的起因
}
/** ... 此处省略开发模式的代码 ... */
return element;
}
在 React16.x 之前的时候,这个处理过程由 createElement 来实现,我们看看源代码如下。
首先我们从代码里取出 key 和 ref (如果存在的话),然后取出 self 和 source (开发模式),之后将其他元素放到 props 中,这里 RESERVED_PROPS 存放了 key 、 ref 、 self 和 source 四个属性,也就是把这四个属性过滤掉。
之后获取元素的孩子,这里的处理是,如果孩子长度为 1 ,那么children 是一个元素,否则返回一个数组。
之后是获取 defaultProps
,这里的逻辑是这样的,首先我们的 type 可能有两种取值
export function createElement(type, config, children) {
let propName;
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if(config != null) {
/** 获取 ref */
if(hasValidRef(config)) {
ref = config.ref;
}
/** 获取 key */
if(hasValidKey(config)) {
key = '' + config.key;
}
}
/** 获取 self 和 source */
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
/** 获取其他 props */
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
/** 获取 children */
const childrenLength = arguments.length - 2;
if(childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childrenArray = Array(childrenLength);
for(let i = 0; i < childrenLength; i++) {
childrenArray = Array(childrenLength);
}
props.children = childrenArray;
}
/** 获取 defaultProps */
if(type && type.defaultProps) {
const defaultProps = type.defaultProps;
for(propName in defaultProps) {
if(props[Name] in defaultProps) {
props[propName] = defaultProps[propName];
}
}
}
/** 创建 React 元素 */
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props
)
}
在 React17.x 之后的时候,这个处理过程由 jsx 来实现,实现的原理大同小异,我们就简要说一说:
export function jsx(type, config, maybeKey) {
let propName;
const props = {};
let key = null;
let ref = null;
// 若设置了key,则使用该key
if (maybeKey !== undefined) {
if (__DEV__) {
checkKeyStringCoercion(maybeKey);
}
key = '' + maybeKey;
}
// 若config中设置了key,则使用config中的key
if (hasValidKey(config)) {
if (__DEV__) {
checkKeyStringCoercion(config.key);
}
key = '' + config.key;
}
// 提取设置的ref属性
if (hasValidRef(config)) {
ref = config.ref;
}
// 剩余属性将添加到新的props对象中
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(type, key, ref, undefined, undefined, ReactCurrentOwner.current, props);
}
现在我们已经明白了从 jsx 到 ReactElement 的过程,我们看到,ReactElement 中有一个特别的字段 $$typeof
,它的值是一个 Symbol,那么为什么React元素需要这个 $$typeof
属性呢。
我们先从 Symbol 出发:
Symbol
是 ES6
新推出的一种基本类型,它表示独一无二的值,它可以接受一个字符串作为参数,带有相同参数的两个Symbol
值不相等,这个参数只是表示 Symbol
值的描述而已。由于Symbol值的唯一性,意味着它可以作为对象的属性名,避免出现相同属性名,产生某一个属性被改写或覆盖的情况。
而 Symbol.for()
是用于将描述相同的Symbol
变量指向同一个Symbol
值,Symbol.for()
定义相同描述的值时会被搜索到,描述相同则他们就是同一个值。
同时,React 提供了一种方式来将用户输入的内容当成html来渲染:
<div dangerouslySetInnerHTML={{ __html: message }}></div>
也就是说,那么我们可以构造一个类似 ReactElement 的结构,然后传入我们的页面中,比如下面的例子,这里是一个知识点,message 这样的插值可以传入基本类型,一个组件,一段 jsx,也可以直接传入一个 ReactElement ,因为最终他们都会被编译成 ReactElement 进行处理:
class App extends React.Component {
render() {
const message = {
type: "div",
props: {
dangerouslySetInnerHTML: {
__html: `HTML
link`
}
},
key: null,
ref: null,
$$typeof: Symbol.for("react.element")
};
return <>{message}</>;
}
}
但是很幸运,应该没有开发者希望将用户编写的数据封装成一个 ReactElement ,因为只要我们的逻辑代码不去创建一个 Symbol.for("react.element")
,用户将没有手段去创建它。但是如果我们使用类似 type : "React"
等方式来标识它,此时出现了一个问题,假设我们的数据来自后端,它使用 json 的方式传递过来,就能复刻出一个 ReactElement , 但是 json 中并没有 Symbol 这个类型,所以上述的方法就失效了。
其实 React 13 当时就存在着这个漏洞。之后,React 14 就修复了这个问题,修复方式就是通过引入$$typeof属性,并且用 Symbol 来作为它的值。所以用一句话概括就是 Symbol 属性的引入是为了预防 XSS 攻击 ,关于 XSS 这部分可以看我相关笔记:
https://blog.csdn.net/weixin_46463785/article/details/128750337
最后我们再来梳理一下从 jsx 到 ReactElement 的过程:
下一篇我们将学习 React 16.x 的一个重要的结构 fiber 结构