阅读 337

React源码解析 API概览 + React

最近闲来无事,研究一波React源码,一开始是以Vue源码起步的,结果发现我对Vue实在是不熟悉,看Vue源码还不够格,相比而言,我更喜欢React,可能是因为第一个学的框架学的就是React,所以对React更加的充满热情,也更加的熟练,个人观点,React还是要比Vue牛逼一点好看一点的。

React本身的源码是很少的,根据打包出来的Commonjs版本看来,React只有两千多行代码,但是ReactDom据说有两万多行,框架开发者实属伟大!致敬!!!

那么这一篇是React一些通用的API概况和React.Children方法的解析,如有不到位或错误的地方欢迎指教,我的邮箱 1103107216@qq.com 您也可以下方评论。

React源码获取

我发现有两种方式,一种呢就是从github上拉取react项目的源码,github地址大家可以自己找,git clone下来之后,在/packages/react下面就是react的源码了,可以看到下面是分成了很多个小文件的,这个我一般用来看的不是用来调试的。

另一个呢就是建一个项目,安装一下cnpm i react react-dom -S之后在node_modules里面找到react的源码,建一个项目,用webpack打包,装个babel一套,毕竟es6比es5好使多了,开个热更新,之后就直接修改这个node_modules里面的源码进行打印调试了,我个人喜欢console.log不解释,只有在调试一些算法问题时我才会开Debug模式。

通用API

首先先来一个简单的 React 应用,这边使用es6的class写法,个人建议多练练函数式编程,写函数组件比写class舒服多了,毕竟React16提供了这么多强大的Hook

import React from 'react';
class App extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return (
            <div>
                Hello World
            </div>
        )
    }
}
复制代码

OK, Hello World 致敬,我们可以开始干活了。首先看一下React的源码,在/packages/react/src/React.js这个文件里面,可以看到React的定义,你会发现和Vue的源码很不一样,这也是我更喜欢React的原因,慢慢的亲切感。

const React = {
  Children: {
    map,
    forEach,
    count,
    toArray,
    only,
  },

  createRef,
  Component,
  PureComponent,

  createContext,
  forwardRef,
  lazy,
  memo,

  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,

  Fragment: REACT_FRAGMENT_TYPE,
  StrictMode: REACT_STRICT_MODE_TYPE,
  Suspense: REACT_SUSPENSE_TYPE,

  createElement: __DEV__ ? createElementWithValidation : createElement,
  cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
  createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
  isValidElement: isValidElement,

  version: ReactVersion,

  unstable_ConcurrentMode: REACT_CONCURRENT_MODE_TYPE,
  unstable_Profiler: REACT_PROFILER_TYPE,
  // 这一行跳过
  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,
};
复制代码

这边定义了React里面的所有的通用方法,这边只做一个概览,每一个具体的用处会在后面进行详细的介绍。

Children

这个里面封装的是对一个组件的子组件进行遍历等的一些操作,我们一般不会用到,讲真我除了看源码会用他来试一试其他的真没见到有人用它。

  • forEach,map 类似于数组的遍历对象遍历啥的

  • count 用来计算子组件的数量

  • only 官方解释:验证 children 是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。 Tips:不可以使用React.Children.map方法的返回值作为参数,因为map的返回值是一个数组而不是一个React元素

  • toArray 将Children按照数组的形式扁平展开并返回

搞不懂没关系,后面会介绍,有一个印象就好

createRef

ref 属性是在开发中经常使用的,说白了就是用来获取真实Dom的,新版的React中使用ref的操作也变了

class MyComponent extends React.Component {
  constructor(props) {
    super(props);

    this.inputRef = React.createRef();
  }
  // 这是一种
  render() {
    return <input type="text" ref={this.inputRef} />;
  }
  // 这是另外一种
  render() {
    return <input type="text" ref={node => this.inputRef = node}>
  }
}
复制代码

Component, PureComponent

这两个大家应该都很熟悉,创建一个React组件,PureComponent在判断组件是否改更新的时候更加的方便。

createContext

创建一个上下文,返回一个Context对象,里面包含了Provider,Consumer属性,一般用来往组件树的更深处传递数据,避免一个组件一个组件的往下传,不方便解藕

forwardRef

创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中.React.forwardRef 接受渲染函数作为参数。React 将使用 props 和 ref 作为参数来调用此函数。此函数应返回 React 节点。

lazy

组件懒加载

const SomeComponent = React.lazy(() => import('./SomeComponent'));
复制代码

memo

用来创建一个HOC的

useState...

接下来这几个就是React16大名鼎鼎的Hook函数,功能强大,函数式组件的福音,亲切感倍足

Fragment StrictMode Suspense unstable_ConcurrentMode unstable_Profiler

这四个都是React提供的组件,但他们呢其实都只是占位符,都是一个Symbol,在React实际检测到他们的时候会做一些特殊的处理,比如StrictMode和AsyncMode会让他们的子节点对应的Fiber的mode都变成和他们一样的mode

createElement

createElement 这是React中最重要的方法了,用来创建ReactElement

cloneElement

顾名思义,克隆一个ReactElement

createFactory

创建一个工厂,这个工厂专门用来创建某一类ReactElement

isValidElement

用来检测是否是一个ReactElement

version

记录React的当前版本号

React.Children 详解

React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。

这一部分的代码在 packages/react/react/src/ReactChildren.js里面,主要分装了forEach map count only toArray,前两者用于遍历Reach Children。

  • count 用于返回该组件的children数量

  • only 用于判断该组件是不是只有一个子节点

  • toArray 将React.Children以扁平的形式返回出来,并附加key

React中,一段文本可以被称为一个子节点,一段标签也可以被成为一个节点。

class App extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        // Hello World
        console.log(this.props.children);
        return (
            <div></div>
        )
    }
}

ReactDom.render(
    <App>
        // 一段文本也是一个子节点
        Hello World
    </App> ,
    document.getElementById('root')
);
复制代码
class App extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        // 被标记为一个React.Element
        console.log(this.props.children);
        return (
            <div></div>
        )
    }
}

ReactDom.render(
    <App>
        // 一段标签也可以是一个子节点
        <div>Hello World</div>
    </App> ,
    document.getElementById('root')
);
复制代码

在上面的示例代码中,如果传递的子节点是一段html标签,那么打印出来的结果是这样的:

image_1dkp91mf4199d1kds1ul615rklpr9.png-36.3kB

我们也可以在App组件中显示我们传递的这个Children

class App extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        console.log(this.props.children);
        return (
            <div>{ this.props.children }</div>
        )
    }
}
复制代码

image_1dkp98tod16gl3u91q614tj1a7jm.png-2.4kB

如果传递的是多个节点,那么就会被解析成一个数组

<App>
    <div>Hello World</div>
    <div>Hello China</div>
</App>
复制代码

image_1dkp9bfcjqb5ptn1dt02dm8lp13.png-22.8kB

那么Reach.Children的方法应该就是在这里进行使用,因为我实际上也没有使用过,做个简单的示例,我们可以打印一下App这个组件的子节点�数,使用count方法

class App extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        // 2
        console.log(React.Children.count(this.props.children));
        return (
            <div>{ this.props.children }</div>
        )
    }
}

ReactDom.render(
    <App>
        <div>Hello World</div>
        <div>Hello China</div>
    </App> ,
    document.getElementById('root')
);
复制代码

这边会打印出来一个 2 因为我们传递的是两个节点

示例看完了我们可以来分析一下源码了,介绍一下map的源码

找到ReactChildren.js(这是在React源码里,不是在node_modules里),找到最下面模块导出语句

export {
  forEachChildren as forEach,
  mapChildren as map,
  countChildren as count,
  onlyChild as only,
  toArray,
};
复制代码

可以看到mapmapChildren的一个别名,下面找到这个函数

/**
 * Maps children that are typically specified as `props.children`.
 *
 * See https://reactjs.org/docs/react-api.html#reactchildrenmap
 *
 * The provided mapFunction(child, key, index) will be called for each
 * leaf child.
 *
 * @param {?*} children Children tree container.
 * @param {function(*, int)} func The map function.
 * @param {*} context Context for mapFunction.
 * @return {object} Object containing the ordered map of results.
 */
function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}
复制代码

方法接受三个参数,第一个参数是我们传递的this.props.children,也是必选参数,第二个参数是一个function,在遍历的过程中,会对每一个节点都使用这个function,这个function接受一个参数,参数就是当前遍历的节点,第三个参数是一个上下文,一般不用传。 可以看出重点是mapIntoWithKeyPrefixInternal这个方法。

使用示例

class App extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        React.Children.map(this.props.children, (item) => {
            console.log(item);
        })
        return (
            <div>{ this.props.children }</div>
        )
    }
}
复制代码
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  // 被忽视的前缀
  let escapedPrefix = '';
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }
  // 遍历上下文
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  releaseTraverseContext(traverseContext);
}
复制代码

首先是获取一下遍历的上下文,这个在后面的方法应该会用到,下面就是开始遍历所有的Children了,重点是traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);,第一个参数好理解就是我们传递的this.props.children,第二个参数是一个方法,第三个参数就是前面获取到的遍历上下文。

首先看一下这个getPooledTraverseContext方法

const POOL_SIZE = 10;
const traverseContextPool = [];

function getPooledTraverseContext(
  mapResult,
  keyPrefix,
  mapFunction,
  mapContext,
) {
  if (traverseContextPool.length) {
    const traverseContext = traverseContextPool.pop();
    traverseContext.result = mapResult;
    traverseContext.keyPrefix = keyPrefix;
    traverseContext.func = mapFunction;
    traverseContext.context = mapContext;
    traverseContext.count = 0;
    return traverseContext;
  } else {
    return {
      result: mapResult,
      keyPrefix: keyPrefix,
      func: mapFunction,
      context: mapContext,
      count: 0,
    };
  }
}
复制代码

用了一个闭包,外层有一个traverseContextPool记录者遍历上下文的一个pool,我脑海中蹦出来的词是连接池,所以暂且就这么理解他,这个连接池的容量为10,如果这个连接池里有东西的话,也就是说这个traverseContextPool.length !== 0的话,那么会弹出最后一个进行赋值然后返回,如果池里没有东西的话就直接返回一个新的对象。

下面看重点方法traverseAllChildren

/**
 * Traverses children that are typically specified as `props.children`, but
 * might also be specified through attributes:
 *
 * - `traverseAllChildren(this.props.children, ...)`
 * - `traverseAllChildren(this.props.leftPanelChildren, ...)`
 *
 * The `traverseContext` is an optional argument that is passed through the
 * entire traversal. It can be used to store accumulations or anything else that
 * the callback might find relevant.
 *
 * @param {?*} children Children tree object.
 * @param {!function} callback To invoke upon traversing each child.
 * @param {?*} traverseContext Context for traversal.
 * @return {!number} The number of children in this subtree.
 */
function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }

  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
复制代码

主要看这个方法的实现traverseAllChildrenImpl

/**
 * @param {?*} children Children tree container.
 * @param {!string} nameSoFar Name of the key path so far.
 * @param {!function} callback Callback to invoke with each child found.
 * @param {?*} traverseContext Used to pass information throughout the traversal
 * process.
 * @return {!number} The number of children in this subtree.
 */
function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext,
) {
  const type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    // All of the above are perceived as null.
    children = null;
  }

  let invokeCallback = false;

  if (children === null) {
    invokeCallback = true;
  } else {
    switch (type) {
      case 'string':
      case 'number':
        invokeCallback = true;
        break;
      case 'object':
        switch (children.$$typeof) {
          case REACT_ELEMENT_TYPE:
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
  }

  if (invokeCallback) {
    callback(
      traverseContext,
      children,
      // If it's the only child, treat the name as if it was wrapped in an array
      // so that it's consistent if the number of children grows.
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

  let child;
  let nextName;
  let subtreeCount = 0; // Count of children found in the current subtree.
  const nextNamePrefix =
    nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;

  if (Array.isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getComponentKey(child, i);
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  } else {
    const iteratorFn = getIteratorFn(children);
    if (typeof iteratorFn === 'function') {
      if (__DEV__) {
        // Warn about using Maps as children
        if (iteratorFn === children.entries) {
          warning(
            didWarnAboutMaps,
            'Using Maps as children is unsupported and will likely yield ' +
              'unexpected results. Convert it to a sequence/iterable of keyed ' +
              'ReactElements instead.',
          );
          didWarnAboutMaps = true;
        }
      }

      const iterator = iteratorFn.call(children);
      let step;
      let ii = 0;
      while (!(step = iterator.next()).done) {
        child = step.value;
        nextName = nextNamePrefix + getComponentKey(child, ii++);
        subtreeCount += traverseAllChildrenImpl(
          child,
          nextName,
          callback,
          traverseContext,
        );
      }
    } else if (type === 'object') {
      let addendum = '';
      if (__DEV__) {
        addendum =
          ' If you meant to render a collection of children, use an array ' +
          'instead.' +
          ReactDebugCurrentFrame.getStackAddendum();
      }
      const childrenString = '' + children;
      invariant(
        false,
        'Objects are not valid as a React child (found: %s).%s',
        childrenString === '[object Object]'
          ? 'object with keys {' + Object.keys(children).join(', ') + '}'
          : childrenString,
        addendum,
      );
    }
  }

  return subtreeCount;
}
复制代码

分步解析

let invokeCallback = false;

  if (children === null) {
    invokeCallback = true;
  } else {
    switch (type) {
      case 'string':
      case 'number':
        invokeCallback = true;
        break;
      case 'object':
        switch (children.$$typeof) {
          case REACT_ELEMENT_TYPE:
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
  }

  if (invokeCallback) {
    callback(
      traverseContext,
      children,
      // If it's the only child, treat the name as if it was wrapped in an array
      // so that it's consistent if the number of children grows.
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }
复制代码

这一块是用来判断 children 类型的,如果是string比如说传递一个文本,number,object比如说一个dom节点,那么表明 children 只是一个节点,那么就直接执行 callback 返回一个 1

if (Array.isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getComponentKey(child, i);
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  }
复制代码

如果我们传递的是多个节点,那么会遍历children数组,进行递归遍历,直到返回的是上面显示的几个类型。

上边提到的callback就是传递的mapSingleChildIntoContext,这边就是利用到之前的traverseContextPool被我称之为连接池的东西.

function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;

  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    if (isValidElement(mappedChild)) {
      mappedChild = cloneAndReplaceKey(
        mappedChild,
        // Keep both the (mapped) and old keys if they differ, just as
        // traverseAllChildren used to do for objects as children
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      );
    }
    result.push(mappedChild);
  }
}
复制代码

这边的mappedChild就是我们传递的funcion的返回值,function呢就是调用React.Children.map(children,callback)这里的callback了,如果这个返回值返回的是一个数组的话,那么就进行递归调用,这个时候就需要用到之前的连接池了。

采用这个连接池的目的我也是在其他的地方看到了

因为对Children的处理一般在render里面,所以会比较频繁,所以设置一个pool减少声明和gc的开销

这就是React.Children.map的实现。