react源码系列 — 创建元素组件

Posted 茂树24

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react源码系列 — 创建元素组件相关的知识,希望对你有一定的参考价值。

文章目录

react 源码版本为 v16.13.1,可以下载下来结合者一起看,本节涉及到 packages/react 中的代码。
文章的源在:https://www.yuque.com/wmaoshu/blog/gcg1ix

react 一个很重要的设计原则是根据业务维度的关注点分离,将每个关注点或者说变化的轴线作为一个个组件划分,每个组件是单一职责的。这与 html 和 js 逻辑分离的观念不同,虽然将逻辑和 UI 逻辑耦合在一起,但是更能从业务角度带来很大的收益,更甚者最近流行的 css in js 也是同样的思维方式,所以 react 更能适用应对复杂的业务场景。

react 的设计思想基于函数式编程, 其中很重要的概念是 元素、组件。元素是组成组件渲染结果的基本单元,组件更像是一个函数根据用户交互输入一定的参数 props 返回一定的元素集合 component。相同的 props 返回相同的 component,每次更新 props 都会产生全新的 component,在 DOM diff 阶段才会判断是否更新以及需要更新多少。所以 react 降低了用户关注的维度,从关注 DOM 底层操作,到只关注数据变化和数据视图映射关系,甚至视图如何变更都不需要关心,使得开发更关注逻辑本身。那么如果数据非常复杂那又需要 redux 新的管理数据模式了,所以写 react 业务需要逐步提高抽象维度。

那我们就从元素、组件最基本开始看看 react 源码是如何处理的。react 是一个运行时框架,但首先经过 babel 将 JSX 转化成 createElement。然后开始介绍 react 实现,先介绍第一参数元素类型的种类,包括字符串、内置组件、内置对象、函数、自定义对象等。然后介绍第二参数 props,包括 ref、key、children 等。 至此 packages/react 中核心 react DSL 代码介绍完毕。

Babel 处理 JSX

假如有这么一段 jsx 代码:

<div id="root" className="style" message=a: 1, b: 2>
  Demo
  null ? undefined: true
   
    ['h1','h2','h3'].map(Item => <Item key=Item className=Item+'-color'/>)
  
  <MyDemo ref="myRef" />
</div>

经过 babel ( https://www.babeljs.cn/repl ) 编译后为:

"use strict";

/*#__PURE__*/
React.createElement(
  "div",
  
    id: "root",
    className: "style",
    message: 
      a: 1,
      b: 2
    
  ,
  // 下面是 children
  "Demo",
  null ? undefined : true,
  ['h1', 'h2', 'h3'].map(function (Item) 
    return /*#__PURE__*/React.createElement(Item, 
        key: Item,
        className: Item + '-color'
    );
  ),
  /*#__PURE__*/
  React.createElement(MyDemo, 
    ref: "myRef"
  )
);

babel 将易读的 JSX 编译成 react createElement 函数调用,从而得到react可以使用处理的组件对象。对于首字母小写的元素,比如

,会将该字符串作为对象的第一参数,而首字母大写的则将看作是一个对象。所以如果想实现该组件的定制化需要使用首字母大写的中间变量作为中转。然后,bebel 会将组件上所有的属性作为组成一个大对象作为第二参数传入(即使是 ref、key 等内置的属性都是一样的处理)。然后函数的第三个参数第四个参数……都是该组件的孩子结点,孩子结点可以是 表达式、 字符串、设置是新的元素组件。

createElement

源码位置 packages/react/src/ReactElement.js,cloneElement 和 createElement 代码大致相同,只介绍 createElement

如图展示了,createElement 传入和最终生成的描述 react 元素的对象,react 正式使用自描述对象的方式脱离平台的限制。
createElement 函数 type 元素参数,可能是如下几种类型:

  • 原生元素:使用小写字母开头的字符串描述浏览器内置的 HTML 节点,比如 p、div 等。

  • 类元素:通过继承 Component、PureComponent 类 创建的元素,会在下面具体介绍。

  • 方法元素:使用函数创建的元素组件。比如 使用 hook 相关的函数,在下面会有介绍。

  • 内置元素:react 提供的具有不同功能的,在各个生命周期有关键作用的元素,比如 Fragment、``AsyncMode 等,会在下面具体介绍。

config 是 Babel 通过将JSX 元素的属性及合成对象而来,也就是说,对于内置的属性比如 ref、key 都可以通过属性的方式传入到 config 中,children 作为其他的参数。对于 ref、key、children 属性会作为单独属性存在, 而其他的属性统一放在 props 对象中:

  • ref 属性:如果不传则默认为 null。

  • key 属性:如果有值则将值转为字符串,否则为 null,不过在后续 生成 虚拟 DOM 树的时候会默认添加 key。

  • children 属性:children 可以通过设置元素的子节点方式或者传入 children 属性的方式设置孩子节点,设置子节点的优先级最高,其次是使用 chidlren属性方式, 其次是默认值。对于设置子节点的方式,props.children 可能是 一个子节点的对象, 或者多个子节点的数组。所以,props.children可能存在很多种类型, 比如 undefined、null、单一子节点(所有可能 type 类型)、数组等。所以最好使用 React 提供的 Children.toArray 处理子节点,具体 Children 对象下这些方法的处理逻辑在下面 Children处理中介绍。

  • 其他 props 属性:除了通过 JSX 属性设置 作为 props外, 还可以设置 type 元素的 defaultProps 属性设置默认值,对于没有设置属性的 props,会使用默认值设置 props。相当于 用户传入的props 和 defaultProps 取了 并集,但 props 中不存在或者值为 undefined 的属性会使用默认值。

最终,生成的对象 type、props、ref、key属性,children 作为了 props 一部分。对于 owner 描述创建该元素的组件,在后续流程中会使用到。比较重要的是 t y p e o f 这 个 字 段 描 述 了 该 元 素 使 用 c r e a t e E l e m e n t 创 建 而 来 的 , 值 为 ‘ R E A C T E L E M E N T T Y P E ‘ 。 在 后 续 构 建 更 新 元 素 结 构 的 时 候 会 使 用 到 。 值 得 注 意 的 时 候 , t y p e 属 性 也 有 可 能 是 一 个 包 含 typeof 这个字段描述了该元素使用 createElement 创建而来的,值为 `REACT_ELEMENT_TYPE`。在后续构建更新元素结构的时候会使用到。值得注意的时候,type 属性也有可能是一个包含 typeof使createElementREACTELEMENTTYPE使typetypeof 字段的对象, 这两个类型是不一样的,前者表示创建的方式,后者便是创建的是什么类型的元素。在后面 type 几种类型中有具体提到。

类元素

源码位置 packages/react/src/ReactBaseClasses.js

一开始可能觉得 react 中的类实现会很复杂, 其实并不然,在 16 版本以后 react 就对平台的依赖进行了拆分,核心的 react 代码非常简洁的实现统一的 DSL,对于不同平台实现不同的接口。其中在类中体现非常明显。
如下就是 React.Component 全部的代码了,最为重要的就是 updater,React 核心 实现了 DSL, 而对于不同的平台,比如 DOM, 要去实现 具体的 updater 下的方法,比如 enqueueSetState 等。在 组件渲染的时候注入 updater 平台对象。

const emptyObject = ;

function Component (props, context, updater) 
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
;

Component.prototype.isReactComponent = ;

Component.prototype.setState = function(partialState, callback) 
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
;

Component.prototype.forceUpdate = function(callback) 
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
;

对于 PureComponent 是 Component 的子类 ,只不过会在 原型链中 增加 isPureReactComponent 表明是 PureComponent, 在渲染的时候会对 props 进行 比较。
当然,官方文档或者在事件过程中创建组件对象已经不太大量的使用 类组件的方式,因为存在着功能不单一,无法公用部分逻辑等因素开始大量的推荐使用方法元素结合 Hooks 的方式创建对象。

方法元素

使用函数创建的元素都可以看成无状态的PureComponent, 如果想要实现状态,并且某些状态的服用就要 在方法中使用 hooks 了。hook 源码实现也是和类一样非常简单的, 都是调用 ReactCurrentDispatcher.current 中的对应方法。ReactCurrentDispatcher这个对象是 react core 和 对应平台具体实现的接口桥梁,在具体的平台实现中去定义。

function resolveDispatcher() 
  const dispatcher = ReactCurrentDispatcher.current;
  return dispatcher;


export function useContext<T>(
  Context: ReactContext<T>,
  unstable_observedBits: number | boolean | void,
): T 
  const dispatcher = resolveDispatcher();
  return dispatcher.useContext(Context, unstable_observedBits);


export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] 
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);

......

内置元素

这一篇主要描述 react 源码是怎么实现的, 具体如何在业务中使用请参考官方文档。

Suspense 与 lazy

lazy可以通过动态的 import 加载组件,但是 动态加载的组件要在 Suspense 内部,Suspense组件可以制定在组件没有加载到的时候的 lodaing样式,这种组件的组合对于模块加载提供很好的帮助。将页面维度的优化降低到组件维度的优化。两个的源码分别是:

lazy:

export function lazy(ctor)
  const payload = 
    _status: -1, // ctor 异步函数的状态
    _result: ctor,
  ;

  const lazyType= 
    $$typeof: REACT_LAZY_TYPE,
    _payload: payload,
    _init: lazyInitializer, // 对于 不同的promise 状态获取结果
  ;

  return lazyType;

这里值得注意的是, 对于 React.lazy 的组件,createElement type 入参就是 lazyType, 而生成的组件的 t y p e o f 为 R E A C T E L E M E N T T Y P E , 但 是 t y p e 属 性 的 typeof 为 REACT_ELEMENT_TYPE,但是 type属性的 typeofREACTELEMENTTYPEtypetypeof 为REACT_LAZY_TYPE,前者描述是从什么创建的,后者是创建的类型是什么。

suspense的type 值就是一个 symbol,为 REACT_SUSPENSE_TYPE 所以通过 createElement 创建的对象中 type 属性为 symbol。

memo

memo 是处理 函数元素因为重复的入参带来的重复渲染问题,并且可以认为的设置比较方法。

export function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) 
  
  const elementType = 
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  ;

  return elementType;

和 lazy 很像, 使用 createElement 创建的对象 type 也是一个对象只不过 这个对象的 $$typeof 是 REACT_MEMO_TYPE,请记住,这里在后续更新对比的过程中会用到。

其他的 symbol 元素

  Fragment 的 symbol 是 REACT_FRAGMENT_TYPE
 	Profiler 的 symbol 是 REACT_PROFILER_TYPE
  StrictMode 的 symbol 是 REACT_STRICT_MODE_TYPE

Children 处理

源码位置 packages/react/src/ReactChildren.js

因为 createElement 产出的元素对象的props 中 children 属性可能是单一的孩子对象或者是多个孩子数组,所以处理的时候需要分别考虑, react 提供了处理孩子节点的方法,分别提供了多个工具函数比如:toArray、forEach、map、count、only。 这里只介绍 map 实现,其他的函数是该实现的子集。

调用 React.Children.map 方法,先执行的是 mapChildren,这个方法判断 传入的孩子结点是否是非严格的null,也就是 undefined或者 null,如果是的话直接结束,这种情况是单节点元素没有孩子的情况。
mapIntoArray 方法,先判断 传入的children 是否是 null、undefined、boolean、string、number、或者 $$typeof 是 REACT_ELEMENT_TYPE 类型, 因为这些类型是非可遍历的, 所以直接调用 map 传入的回调函数。如果 children 是 数组、迭代器 等可以遍历的类型,则让每一个 children 分别再去执行 mapIntoArray。
调用 map 回调函数后, 如果得到的是非数组类型, 那么放入到 map执行完毕返回的List 中,否则,讲这个数组中每一个孩子结点继续调用 mapIntoArray。
注意的是从源码结构中可以看出, 如果使用 map 函数返回的是一个元素数组, 那么得到的一个打平的单层的孩子数组。

以上是关于react源码系列 — 创建元素组件的主要内容,如果未能解决你的问题,请参考以下文章

React 源码分析:调用ReactDOM.render后发生了什么

React源码分析组件通信refskey和ReactDOM

React 系列导航

[React 基础系列] React 中的 元素 vs 组件

React 深入系列1:React 中的元素组件实例和节点

React 源码剖析系列 - 生命周期的管理艺术