8

前言

这篇文章主要通过源码的方式去讲述reactAPI,主要是在react(v16.12.0)这个包中的源码,不涉及react-dom的代码,react-dom会在后面去讲,而在讲述API的时候也不会过多的去讲解这个API的使用方式。而在react这个包里面的API其实更多的只是定义一些组件的type去供给react-dom执行真正更新逻辑的时候使用,因此可能会缺少一些API源码并没有包含到真正执行逻辑的讲解。

React-API

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,
  Profiler: REACT_PROFILER_TYPE,
  StrictMode: REACT_STRICT_MODE_TYPE,
  Suspense: REACT_SUSPENSE_TYPE,

  createElement: createElement,
  cloneElement:  cloneElement,
  createFactory: createFactory,
  isValidElement: isValidElement,

  version: ReactVersion,

  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,
};

以上就是所有react包所暴露出来的API

React.Children

forEach

children.forEach.png

从上图可以清晰大致的了解该API的调用站栈流程

const POOL_SIZE = 10; // 对象池最大数量
const traverseContextPool = []; // 缓存对象,重复使用,减少内存开销以及重复生明对象
  • 创建一个空数组作为traverseContextPool,主要为了减少对象重复声明的开销,对性能产生影响。
  • 预设poll size的大小
function forEachChildren(children, forEachFunc, forEachContext) {
  // 判断children是否存在
  if (children == null) {
    return children;
  }

  // 从context pool中获取对象
  const traverseContext = getPooledTraverseContext(
    null,
    null,
    forEachFunc,
    forEachContext
  );

  // 开始遍历
  traverseAllChildren(children, forEachSingleChild, traverseContext);
  // 释放缓存
  releaseTraverseContext(traverseContext);
}
  • 开始调用forEachChildren
  • 首先判断children是否不存在,是则直接返回
  • 调用getPooledTraverseContext获取context对象
  • 调用traverseAllChildren开始遍历
  • 调用releaseTraverseContext释放缓存

以上是这个api的一个大致流程,下面再看下几个调用栈分别做了什么

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中是否有可以用的对象,如果有则取出第一个,并进行赋值返回,如果没有则返回一个全新的对象

function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext
) {
  const type = typeof children;

  if (type === "undefined" || type === "boolean") {
    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);
    return 1;
  }

  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 + getComponentKey(child, i);
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext
      );
    }
  } else {
    const iteratorFn = getIteratorFn(children);
    if (typeof iteratorFn === "function") {
      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 {
    }
  }

  return subtreeCount;
}
  • 判断children是否为单个节点,简单的理解为非数组
  • 如果是则将invokeCallback设置为true,不是则为false
  • 只有当invokeCallback为true的时候则进行callback()这里的callback指的是forEachSingleChild后面再介绍,这个函数比较简单
  • 如果invokeCallback为false则继续遍历,再调用traverseAllChildrenImpl,整个函数其实只是一个递归扁平化数组
function forEachSingleChild(bookKeeping, child) {
  const { func, context } = bookKeeping;
  func.call(context, child, bookKeeping.count++);
}
  • 这个函数只是通过call函数去进行调用func(forEach的第二个参数)
// 释放context缓存
function releaseTraverseContext(traverseContext) {
  traverseContext.result = null;
  traverseContext.keyPrefix = null;
  traverseContext.func = null;
  traverseContext.context = null;
  traverseContext.count = 0;
  // 如果Context Pool小于最大值,则保留对象,防止对象重复声明
  if (traverseContextPool.length < POOL_SIZE) {
    traverseContextPool.push(traverseContext);
  }
}
  • 最后调用releaseTraverseContext清空对象,并通过判断是否小于POOL_SIZE,如果小于则将清空后的对象进行缓存。
map

map.png

从上图上可以看到 其实map和forEach差不多,唯一的区别只是在最外层多了一个大的递归,为了扁平化map的返回值,如果已经了解了forEach,下面很多重复的步骤可以跳过

function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}
  • 判断children是否存在,是则直接返回
  • 声明result,用来保存最终返回的结果
  • 调用mapIntoWithKeyPrefixInternal
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);
}
  • 生成key值
  • 调用getPooledTraverseContext获取context对象
  • 调用traverseAllChildren开始遍历
  • 调用releaseTraverseContext释放缓存

这里的几个步骤几乎和forEach一摸一样就不重复说明了,唯一不同的是forEach调用的是forEachSingleChild,而这边调用的是mapSingleChildIntoContext下面看下这个函数

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) {
    // 验证是否是react对象,主要是通过对象上的$$typeof属性
    if (isValidElement(mappedChild)) {
      // 返回一个全新的reactElement
      mappedChild = cloneAndReplaceKey(mappedChild);
    }

    result.push(mappedChild);
  }
}
  • 调用func(map函数的第二个参数)
  • 拿到func函数调用后的返回结果
  • 进行判断是否为Array
  • 如果是Array则递归调用mapIntoWithKeyPrefixInternal
  • 如果不是,则将结果push到result中作为最终结果进行返回
count

count 函数特别简单,就下面三行代码

function countChildren(children) {
  return traverseAllChildren(children, () => null, null);
}
  • 直接调用了traverseAllChildren这个函数,可以回到上面看一下这个函数最终的返回值是什么,应该就知道了吧
toArray
function toArray(children) {
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, child => child);
  return result;
}
  • 还是重复使用了mapIntoWithKeyPrefixInternal这个函数,这个函数会把最终结果放到一个result数组中进行返回
only

这个API感觉并没有什么用

function onlyChild(children) {
  invariant(
    isValidElement(children),
    'React.Children.only expected to receive a single React element child.',
  );
  return children;
}
  • 仅仅只是通过isValidElement判断是否为单个的节点,是则返回本身

React.createRef

这个API估计一看代码就懂,就不解释了,直接贴

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

  return refObject;
}

累了先写到这里了,我会尽量把这个包的内容尽快写完,然后开始写react-dom中的内容


accord
1.3k 声望187 粉丝

希望遇到一个公司,遇到一个团队,大家都愿意把code当作一种艺术去书写