React16.8源码解析----API概览 + React.Children 解析

最近闲来无事,研究一波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标签,那么打印出来的结果是这样的:

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

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

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

<App>
    <div>Hello World</div>
    <div>Hello China</div>
</App>

那么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的实现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容

  • 文章首发于个人博客 这是我 Deep In React 系列的第二篇文章,如果还没有读过的强烈建议你先读第一篇:详...
    勿忘巛心安阅读 1,049评论 1 2
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,835评论 1 18
  • 今天的React题没有太多的故事…… 半个月前出了248个Vue的知识点,受到很多朋友的关注,都强烈要求再出多些R...
    浪子神剑阅读 10,082评论 6 106
  • react刚刚推出的时候,讲react优势搜索结果是几十页。 现在,react已经慢慢退火,该用用react技术栈...
    zhoulujun阅读 5,201评论 0 11
  • 在阴冷的教室里,同学们齐刷刷的拿着手机刷着微博,朋友圈,QQ。老师在照着书本敷衍着我们大学的岁月。而我,带...
    Sunshinebloom阅读 154评论 3 2