阅读 261

React源码系列一:React相关API

在线编辑:codepen.io/pen?editors…
在线编辑:jsfiddle.net/reactjs/69z…

1. React顶层API 列表

URL: react.html.cn/docs/react-…
React的源码实际上并没有多少复杂的内容,它将mobile和pc公用的模块提取出来,而核心其实是在React-Dom和React-native中。在React的源码中,下面都是我们熟悉的API,但是我们这里只分析重点API。Hook相关API放入以后讨论。

- Children,
- Component,
- PureComponent,
- createElement,
- cloneElement,
- createFactory
- isValidElement,
- createRef,
- createContext,
- forwardRef,
- lazy,
- memo,
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useReducer,
- useRef,
- useState,
复制代码

1.1 了解什么是?typeof

React元素,会有一个?typeof,来表示该元素是什么类型。该属性是使用2中方式生成,当本地有symbol,则使用symbol生成,没有时使用16进制。

export let REACT_ELEMENT_TYPE = 0xeac7;
export let REACT_PORTAL_TYPE = 0xeaca;
export let REACT_FRAGMENT_TYPE = 0xeacb;
....
if (typeof Symbol === 'function' && Symbol.for) {
  const symbolFor = Symbol.for;
  REACT_ELEMENT_TYPE = symbolFor('react.element');
  REACT_PORTAL_TYPE = symbolFor('react.portal');
  REACT_FRAGMENT_TYPE = symbolFor('react.fragment');
  ....
}
复制代码

2. React.Children详解

children由map, forEach, count, toArray, only组成。看起来和数组的方法很类似,用于处理this.props.children这种不透明数据结构的应用程序。由于children几个方法的核心都是mapIntoArray,因此这里只对map做分析,其他的可以自己去查看。

2.1 React.Children.map

map的使用实例,虽然处理函数给的是多维数组,但是通过map处理后,返回的结果其实被处理成为了一维数组。

  • tip: 如果是fragment,将会被视为一个子组件,不会被遍历。
class Child extends React.Component {
  render() {
    console.log(React.Children.map(this.props.children, c => [[c],[c],[c]]));
    return (
      <div>{
        React.Children.map(this.props.children, c => [[c],[c],[c]])
      }</div>
    )
  }
}
class App extends React.Component {
  render() {
    return(
      <div>
        <Child><p>hello1</p><p>hello2</p></Child>
      </div>
    )
  }
}
// 渲染结果:
<p>hello1</p>  
<p>hello1</p> 
<p>hello1</p>
<p>hello2</p>
<p>hello2</p>
<p>hello2</p>
复制代码

打印dom结构,每隔节点都各自生成了一个key,下面会解析生成该key的步骤。


2.1.1 mapChildren

mapChildren就是map函数,但是其核心处理方法是mapIntoArray。下面我们先看map是如何实现的。

/**
 * 如果children是一个数组,则将遍历它,并且将为数组中的每个children调用function函数。
 *
 * @param children 需要遍历的children树。可以是对象,但是必须具备属性?type的reactChild对象,否则报错
 * @param func 对符合规定的children执行的函数,func会被传入两个参数,符合规定的children以及到当前children的数量。
 *             所有执行func返回的children都会添加到一个数组中,没有嵌套。
 * @param context 一般都为null,如果传入context则func运行中的this为context
 * @return 将children处理过后的有序的结果数组对象
 */
function mapChildren(children: ?ReactNodeList, func: MapFunc, context: mixed, ): ?Array<React$Node> {
  // 如果为null,则直接换回。
  if (children == null) {
    return children;
  }
  // 引用类型,会将对应的child添加到数组中,最终作为map函数的返回值。
  const result = [];
  // 存储当前child中的数量
  let count = 0;
  // 核心处理方法
  mapIntoArray(children, result, '', '', function(child) {
    return func.call(context, child, count++);
  });
  // 返回存储的一维数组
  return result;
}
复制代码

2.1.2 mapIntoArray

mapIntoArray其中会用到key相关的转换函数,我们先来看看其中内部会使用到的哪些函数。

2.1.2.1 escape

将key转换成一个安全的reactid来使用 ,传入的key中所有的'='替换成'=0',':'替换成 '=2',并在key之前加上'$'。

function escape(key: string): string {
  const escapeRegex = /[=:]/g;
  const escaperLookup = {
    '=': '=0',
    ':': '=2',
  };
  const escapedString = key.replace(escapeRegex, function(match) {
    return escaperLookup[match];
  });
  return '$' + escapedString;
}
复制代码

2.1.2.2 getElementKey

如果element存在不为null的key,则返回escape(element.key),否则返回index.toString(36)。

function getElementKey(element: any, index: number): string {
  if (typeof element === 'object' && element !== null && element.key != null) {
    return escape('' + element.key);
  }
  return index.toString(36);
}
复制代码

2.1.2.3 escapeUserProvidedKey

用于mapIntoArray深度遍历时,child的key值转换。一般会在第二层递归开始用到。配一个或者多个/,并用$&/替换。相当于 如果是/,那么后面再添加一个/

const userProvidedKeyEscapeRegex = /\/+/g;
function escapeUserProvidedKey(text: string): string {
  return text.replace(userProvidedKeyEscapeRegex, '$&/');
}

// 例如:
let a='aa/a/'
console.log(a.replace(/\/+/g, '$&/'));  // aa//a//
复制代码

2.1.2.4 isValidElement

严重对象是不是一个ReactElement,我们可以看到最终要的就是使用?typeof这个属性判断。

export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.?typeof === REACT_ELEMENT_TYPE
  );
}
复制代码

2.1.2.5 cloneAndReplaceKey

使用newKey, 克隆一个新的React元素。

export function cloneAndReplaceKey(oldElement, newKey) {
  const newElement = ReactElement(
    oldElement.type,
    newKey,
    oldElement.ref,
    oldElement._self,
    oldElement._source,
    oldElement._owner,
    oldElement.props,
  );

  return newElement;
}

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // 这个标签使我们能够唯一地将其标识为React Element
    ?typeof: REACT_ELEMENT_TYPE,
    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,
    // Record the component responsible for creating this element.
    _owner: owner,
  };
//   ...dev环境下的代码省略
  return element;
};

复制代码

2.1.2.5 mapIntoArray function

终于到了我们的mapIntoArray函数了,下面是对代码的解析。可以先瞅瞅。

  • 判断child是什么类型,如果是简单类型,那么直接放入 array数组中作为返回
  • 如果是数组,则进行深度遍历,直到解析出element元素为止
const SEPARATOR = '.'; // 第一次执行时, nameSoFar 为空,使用这个作为key的一部分传入
const SUBSEPARATOR = ':';

function mapIntoArray( children: ?ReactNodeList, array: Array<React$Node>, escapedPrefix: string, nameSoFar: string, callback: (?React$Node) => ReactNodeList,): number {
    // 判断传入的children是什么类型, 如果是 `undefined` || 'boolean' 处理为 null
    const type = typeof children;
    if (type === 'undefined' || type === 'boolean') {
    children = null;
    }
    // 是否调用callback函数  
    let invokeCallback = false;
    // 判断children是什么类型,当children为null或者非数组, 直接调用函数。
    if (children === null) {
    invokeCallback = true;
    } else {
    switch (type) {
        case 'string':
        case 'number':
        invokeCallback = true;
        break;
        case 'object':
        switch ((children: any).?typeof) {
            case REACT_ELEMENT_TYPE:
            case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
    }
    if (invokeCallback) {
    // 调用callback
    const child = children;
    let mappedChild = callback(child);
    // 深度遍历,第一次执行时,nameSoFar为空,SEPARATOR作为一部分传入。第一次生成 key为 `.0` 
    const childKey = nameSoFar === '' ? SEPARATOR + getElementKey(child, 0) : nameSoFar;
    // 如果执行后的数据是一个数组,则递归调用mapIntoArray,将数组变成一维数组,放在 `array` 中返回,也就是 map函数中的 result 变量
    if (Array.isArray(mappedChild)) {
      
      // 生成child 的 escapedPrefix, 第一次生成 `.0/`
      let escapedChildKey = '';
      if (childKey != null) {
        escapedChildKey = escapeUserProvidedKey(childKey) + '/'; 
      }
      mapIntoArray(mappedChild, array, escapedChildKey, '', c => c);
    } else if (mappedChild != null) {
      // 如果是有效的element,使用新的keyclone一个对象,放入array对象中,作为最终map函数返回的结果。
      if (isValidElement(mappedChild)) {
        mappedChild = cloneAndReplaceKey(mappedChild, 
            escapedPrefix + (mappedChild.key && (!child || child.key !== mappedChild.key) ? escapeUserProvidedKey('' + mappedChild.key) + '/'  : '') + childKey,);
      }
      array.push(mappedChild);
    }
    return 1;
  }

 // 递归调用mapIntoArray,无论你嵌套多少层数组,都将被展示位一个平铺数组。
  let child;
  let nextName;
  let subtreeCount = 0; // 当前树种有多少个子节点
  const nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;

  if (Array.isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getElementKey(child, i);
      subtreeCount += mapIntoArray(
        child,
        array,
        escapedPrefix,
        nextName,
        callback,
      );
    }
  } else {
      // 如果是可迭代的数据还是还是按照 数组方式 进行深度遍历
      ....
  }
  return subtreeCount;
}
复制代码

2.1.2 React.childen.map 流程图

上面我们看了代码的详细情况,下面我们对流程进行一个绘制,方便我们记住它的处理流程。代码我们是不可能一个字不漏的记住,但是主要是思想,我们需要进行吸收。


3. ReactBaseClasses

ReactBaseClasses中有两个我们时常使用的函数,分别是React.ComponentReact.PureComponent,其实这两个函数很类似,在React文件中,该函数很简单,复杂的逻辑全部都在react-dom文件夹中,react-dom其实就是React和UI之间的胶水层,可以兼容多个平台,例如 Web,RN,SSR等。

3.1 Component

下面,我们先来看一下源代码,已经对源代码进行了注释,还是比较清晰的。需要注意的是Component有refs和updater。updater部分会在react-dom中进行详细分析,此处先进行略过。

const emptyObject = {};
function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // 如果组件有字符串refs,后面会分配一个不同的对象。
  this.refs = emptyObject;
  // 我们初始化一个默认的updater,在真正渲染时会注入一个updater
  this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.isReactComponent = {};
// 可以认为this.state是不可变的。可以使用setState来更新state的状态
// 不能保证`this.state`立即更新,因此调用该方法可能会返回旧值
// 多次调用setState,不能保证setState的调用是同步进行,因为它们会分批次执行。你可以提供一个可选的callback,改回调会在setState的调用实际完成后执行
// 当函数提供给setState,将在后面的某个时刻被调用(不同步),它拥有最新的组件参数(state,props,context),因为该函数是在receiveProps 之后,shouldComponentUpdate之前调用,因此还未分配给当前的组件(此时的组件的参数还未更新)
Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
// 强制更新,当我们确认组件的某些深层次的数据修改,未调用setState时,我们可以直接调用forceUpdate来更新组件,该方法不会调用 shouldComponentUpdate,但是会调用componentWillUpdate和componentDidUpdate。
Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
复制代码

3.2 PureComponent

PureComponent使用了寄生式组合继承,并在自己身上标记了一个isPureReactComponent的标签。而Componment身上也有一个isReactComponent标签,其实可以看出来,每一个组件都有一个isXXX来表示自己是属于什么组件的。

function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
/**
 * 进行了浅层比较的组件
 */
function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;

export {Component, PureComponent};
复制代码

4. React.createElement

我们先看一下createElement的流程图。


4.1 React组件的各种形态

我们先来看一下React组件呈现的各种形态,首先我们的编写react组件时,最终都是通过Bable编译为JSX,而在JSX中调用了React.createElement。因此有三种状态:

  • 我们编写的组件形态
function User({name, addField}) {
    return (
        <div>
            <p>{name}</p>
            <Button addField={addField}></Button>
            hello,world
        </div>
    );
}
复制代码
  • 编译为JSX的形态,可以看到最终调用的还是React.createElement方法。
"use strict";

function User( { name, addField } ) {
  return React.createElement(
        "div",  
        null, 
        React.createElement("p", { id: "pid" }, name), 
        React.createElement(Button, { addField: addField }), 
        "hello,world"
    );
}
复制代码
  • ReactElement形态, 最终返回的User组件的React节点如下:

4.2React.createElement 源码分析

OK,那我们就可以来看一下编译为ReactElement的过程是怎样的。首先就是调用createElement方法了。

const RESERVED_PROPS = {
  key: true,
  ref: true,
  __self: true,
  __source: true,
};
// 跟踪当前的所有者,拥有当前正在被构造的组件
const ReactCurrentOwner = {
  current: (null: null | Fiber), // ReactComponent
};
/*
*创建并返回一个新的ReactElement
*/
export function createElement(type, config, children) {
  let propName;
  // 存储从config中传进来的props参数名称
  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 = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 出来key,ref,__self,__soource,其余属性保存在props中
    for (propName in config) {
      if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // 该部分是children的计算与存储,函数参数除了type 和config,就是children节点参数(可以有N个child)。
  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];
    }
    props.children = childArray;
  }
  // 返回ReactElement节点
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}
复制代码

4.3 ReactElement 源码

// self: 临时帮助对象,用于检测调用React.creatElement时的`this`和`owner`不同的地方...在开发环境中使用,因此此处不进行详解
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // 这个标签使我们能够唯一标识为React Element的类型
    ?typeof: REACT_ELEMENT_TYPE,
    // 属于元素的内置属性: 从上面图中打印看出,type是组件的jsx语法,但传入的参数看起来是标签(div,span等)
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner, // 记录负责创建此元素的组件。当前的操作React
  };
  return element;
};
复制代码

5 createRef

创建ref很简单,如果想使用ref,只需要拿current对象即可。另外,对于函数式组件不可以使用ref,可以参考 React官网

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

复制代码

如果要在函数式组件中使用ref,可以使用forwardRef,或者将组件转为class组件。

export function forwardRef<Props, ElementType: React$ElementType>(
  render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
  const elementType = {
    ?typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
  return elementType;
}
复制代码

forwardRef的使用详细使用可以参考掘金文章

6. 写在最后

React源码持续更新。如果有哪些写的不正确的地方,请大家一定给我指正,谢谢。

  1. React源码系列一:React相关API
  2. React源码系列二:React Render之 FiberRoot
  3. React源码系列三:React.Render流程 二 之更新
  4. React源码系列四:React Fiber 架构
  5. 等待中(React调度原理).....