react源码解析一

419 阅读5分钟

0 前言

本React系列文章是基于React最新的代码仓库进行分析,也许和老版本有些许不同,大佬们可酌情观看。

1 Component && Pure Component

Component和PureComponent作为所有学习React开发的人的入口,其对我们来说还是比较重要的,所以本篇文章选择从此处作为切入进行分析。

源码位于:ReactBaseClasses.js文件。

该文件更多的类似于抽象类的作用,对很多进行定义,然后提供给外部调用,真正的细节实现并不在此处。

1.1 Component

组件更新的基类装饰器,主要工作就是初始化参数。

具体代码如下:

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

主要是对props,context,updater这三个参数进行初始化。

其中最重要的就是初始化装饰器,因为react所有的操作都在存在updater的前提下继续下去的。我们在常规新建的时候是不会传入updater参数的,所以一般都是初始化的默认的ReactNoopUpdateQueue

refs默认设置为emptyObject的空对象。

1.2 PureComponent

唯一的区别就是在PureComponent的prototype上带有了类型标示,会判断isPureReactComponent。

pureComponentPrototype.isPureReactComponent = true;

TIPS:React中对比一个ClassComponent是否需要更新,只有两个地方。一是看有没有shouldComponentUpdate方法,二就是这里的PureComponent判断。

关于updater将在专门的文章中进行叙述。

2 React Context

相关代码如下所示,已经删除了dev有关的逻辑。

export function createContext<T>(
  defaultValue: T,
  calculateChangedBits: ?(a: T, b: T) => number,
): ReactContext<T> {
  if (calculateChangedBits === undefined) {
    calculateChangedBits = null;
  }

  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    _calculateChangedBits: calculateChangedBits,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    _threadCount: 0,
    Provider: (null: any),
    Consumer: (null: any),
  };

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  context.Consumer = context;
  return context;
}

_currentValue:作为支持多个并发渲染器的解决方法,我们将一些渲染器归类为主要渲染器,而将其他渲染器归类为次要渲染器。我们预计最多只有两个并发渲染器:Reaction Native(主要)和Fabric(次要);Reaction DOM(主要)和Reaction ART(次要)。辅助渲染器将其上下文值存储在单独的字段中。

_threadCount:用于记录当前渲染器中支持并发渲染器的数量。

Provider,Consumer:这些是循环调用过的。

typeof:因为JSON不支持Symbol类型。所以即使服务器存在用JSON作为文本返回安全漏洞,JSON里也不包含Symbol.for(react.element)React会检测element.typeof:因为JSON不支持Symbol 类型。所以即使服务器存在用JSON作为文本返回安全漏洞,JSON 里也不包含Symbol.for('react.element')。React 会检测 element.typeof,如果元素丢失或者无效,会拒绝处理该元素。特意用 Symbol.for() 的好处是 Symbols 通用于 iframes 和 workers 等环境中。因此无论在多奇怪的条件下,这方案也不会影响到应用不同部分传递可信的元素。同样,即使页面上有很多个 React 副本,它们也 「接受」 有效的 $$typeof值。

如果浏览器不支持 Symbols 怎么办?

唉,那这种保护方案就无效了。React仍然会加上 $$typeof 字段以保证一致性,但只是设置一个数字而已 —— 0xeac7

为什么是这个数字?因为 0xeac7 看起来有点像 「React」。

3 React.createRef

这是一个具有单个可变值的不可变对象。实现原理也超级简单。

export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  return refObject;
}

就创建一个refOject,然后return即可。

用法通过dom的ref实现绑定:

class App extends React.Component{
  constructor() {
    this.ref = React.createRef()
  }
  render() {
    return <div ref={this.ref} />
    // or
    return <div ref={(node) => this.funRef = node} />
  }
}

4 createElement

4.1 总体概述

创建一个ReactElement元素的接口。

export function createElement(type, config, children) {}

type:表示要创建的节点的类型,

config:表示创建节点的一些属性,比如id,class之类的。

children:创建节点中包含的内容,这个children可以又是一个ReactElement。

4.2 分段解析

    if (hasValidRef(config)) {
      ref = config.ref;
			// dev环境下的类型警告
      if (__DEV__) {
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // Remaining properties are added to a new props object
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }

解析:

大概就是对传递过来的config对象进行解析。


如果config不为空就开始进行解析,首先解析的就是ref属性。

如果存在Key,同样把key绑定到要生成的dom上去。

将传递进来的这些config,赋值到对应的props上去。

  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    if (__DEV__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }

传递过来的children对象可以存在多个参数,上述代码的意义就是将这些参数赋值到this.props.children上去。

  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  

如果传入的当前JSX.Element存在默认props,再把默认的props赋值到当前这个已经创建好的Element对象中去。

最后调用ReactElement完成元素创建。

5 cloneElement

cloneElement的步骤和createElement的步骤十分相似,但是也存在一些不同的。

比如说,在clone之前会对传入的element进行类型校验,如果不符合条件将会直接报出错误。

  invariant(
    !(element === null || element === undefined),
    'React.cloneElement(...): The argument must be a React element, but you passed %s.',
    element,
  );

在创建元素之前首先会把原props进行复制操作。

const props = Object.assign({}, element.props);

下面的操作就几乎是相同的了,比如说解析config,解析默认的props,解析children。

6 createFactory && isValidElement

createFactory是用来创建专门用来创建某一类ReactElement的工厂的,我们几乎不会涉及到。

isValidElement顾名思义就是用来验证是否是一个ReactElement的,基本也用不太到。

7 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,
  };
  return element;
};

在前端一堆操作完成之后,我们此时只需要将已经准备好的参数传入进行创建即可。

其中$$typeof代表的该对象作为react element的唯一表示,如果没有就无法被react对象识别。

_owner:记录负责创建此元素的组件(这里比较重要,在后面会做进一步分析)。

type,key,ref,props等由createElement处理完成之后传入,具体加工过程可以看关于createElement的源码。