阅读 132

React传-1

原文

写在前面

计划用半年的时间去深入 React 源码并记录下来,本文是系列文章第一章,前面大多数会以功能为主,不会涉及太多事务机制与流程,后半部分以架构、流程为主。这个是一个水到渠成的事情。看的越多,对其理解的广度就越大,深度也随之沉淀,在深入的同时站在作者的角度去思考,能够脱离源码照葫芦画瓢,才能算理解,读懂本身源码并不重要。可能没有什么休息时间,但是会尽量挤出来完善文章,也算是一种兴趣与习惯。

起源

2011 年,前端工程师 Jordan Walke 创建了ReactJS的早期原型FaxJS。时间轴传送门

从入口开始

时至今日(2019.9.28),五个小时前React已经将版本更新到 16.10.0 了,预计大半年内将步入17大版本。希望在系列文章完结之后更新(免得我又得看一遍)。

React 与 Vue 的源码相同的使用 Facebook 开源的 Flow 静态类型检查工具,为什么要用 Flow 而不用 Typescript ? 原因可能是 React 诞生的时间较早,那时候还没有 Typescript,后来也由于 Typescript 15年被社区广泛接受才火起来。还一个原因是 Flow 没有 Typescript 那么“严格”,所有的检查都是可选的。

fork/clone/open三部曲,找到 packages/react/src/React.js,剔除注释和空白行的源码还不到一百行,这个入口文件集成了所有的api暴露出去。

React中的源码与React-DOM分离,所以在packages/React内很多只是“形”上的API

import ReactVersion from '../../shared/ReactVersion';
import {
  REACT_FRAGMENT_TYPE,
  REACT_PROFILER_TYPE,
  REACT_STRICT_MODE_TYPE,
  REACT_SUSPENSE_TYPE,
  REACT_SUSPENSE_LIST_TYPE,
} from '../../shared/ReactSymbols';
复制代码

最顶部导入了React当前版本号,ReactSymbols 文件管理着全局的 React功能组件 Symbol 标志

Component

import {Component, PureComponent} from './ReactBaseClasses';
复制代码

ComponentPureComponent 组件都是经常用的, 猜也能猜到都是定义一些初始化方法。

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

Component.prototype.isReactComponent = {};

Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');// 加入更新队列
};

Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate'); // 强制加入更新队列
};
复制代码

定义了Component类的 setStateforceUpdate 方法,以便在组件实例化后调用,将当前的props,context,refs进行绑定,并初始化更新。

每个组件内部都有一个 updater ,被用来驱动state更新的工具对象,执行更新队列,没传入updater时,this.updater 默认为 ReactNoopUpdateQueue,但是它没什么意义,只是做警告用的。

const ReactNoopUpdateQueue = {
  isMounted: function(publicInstance) {
    return false;
  },
  enqueueForceUpdate: function(publicInstance, callback, callerName) {
    warnNoop(publicInstance, 'forceUpdate');
  },
  enqueueReplaceState: function(publicInstance, completeState, callback, callerName) {
    warnNoop(publicInstance, 'replaceState');
  },
  enqueueSetState: function(publicInstance, partialState, callback, callerName) {
    warnNoop(publicInstance, 'setState');
  },
};
复制代码

isMounted 在组件未挂载的情况下isMounted 一直会返回 false,例如在 constructor 里调用 setState或者组件已卸载/未使用,其他方法的作用是在开发环境下警告用户不要在constructor 内调用this原型上的方法。因为实际上真正的 updater 都是在 renderer 后注入的。真正的updater:

const classComponentUpdater = {
  isMounted, // fn() => true
  enqueueSetState(inst, payload, callback) {
     // 获取fiber 也就是inst._reactInternalFiber
    const fiber = getInstance(inst);

    // 根据 msToExpirationTime(performance.now()) 得到一个时间,后续涉及到 ExpirationTime
    const currentTime = requestCurrentTimeForUpdate();

    // 获取suspense配置,与即将新增的 withSuspenseConfig Api强相关,默认情况下都是null
    const suspenseConfig = requestCurrentSuspenseConfig();

    // 根据开始实际计算任务过期时间
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );
    //创建update对象
    const update = createUpdate(expirationTime, suspenseConfig);

    //setState的更新对象
    update.payload = payload;

    // setState的callback
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }
    // 放入队列
    enqueueUpdate(fiber, update);

    // 开始调度
    scheduleWork(fiber, expirationTime);
  },
  // other ...
}
复制代码

setState的任务调度以这种形式发出的,另外 与forceUpdate 、 replaceState 也差不多。

那上面提到的ExpirationTime是什么?

ExpirationTime

ExpirationTime是一个“保险”,为防止某个update因为优先级的原因一直被打断而未能执行。React会设置一个ExpirationTime,当时间到了ExpirationTime的时候,如果某个update还未执行的话,React将会强制执行该update,这就是ExpirationTime的作用。它有两种计算方法,一种computeInteractiveExpiration同步更新,与 computeAsyncExpiration 返回异步更新的expirationTime

//整型最大数值,V8中针对32位系统所设置的最大值 Math.pow(2,30) - 1;
export const Sync = MAX_SIGNED_31_BIT_INT; // 1073741823
export const Batched = Sync - 1; // 1073741822

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = Batched - 1; //1073741821

function msToExpirationTime(ms) {
  return MAGIC_NUMBER_OFFSET - (ms / UNIT_SIZE | 0); // 1073741821 - now()/10|0
}

function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision; // 取整,本次区间分类最大值
}

// 计算过期时间
function computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs): ExpirationTime {
  /* LOW 任务 => 1073741821 - ceiling(1073741821 - currentTime + 5000 / 10, 250 / 10)
                1073741821 - (((1073741821 - currentTime + 500) / 25) | 0) * 25 - 25
  */

  /* HIGH任务 => 1073741821 - ceiling(1073741821 - currentTime + (__DEV__ ? 500 : 150) / 10, 100 / 10)
          DEV   1073741821 - ceiling(1073741821 - currentTime + 50, 10)
                1073741821 - (((1073741821 - currentTime + 50) / 10) | 0) * 10 - 10

          !DEV  1073741821 - ceiling(1073741821 - currentTime + 15, 10)
                1073741821 - (((1073741821 - currentTime + 15) / 10) | 0) * 10 - 10
  */
  return (
    MAGIC_NUMBER_OFFSET - ceiling(MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE, bucketSizeMs / UNIT_SIZE)
  );
}


// LOW 低优先级任务
export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;
export function computeAsyncExpiration(currentTime: ExpirationTime): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION,
    LOW_PRIORITY_BATCH_SIZE,
  );
}

// HIGH 高优先级任务
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,
    HIGH_PRIORITY_BATCH_SIZE,
  );
}

export function computeSuspenseExpiration(
  currentTime: ExpirationTime,
  timeoutMs: number,
): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    timeoutMs,
    LOW_PRIORITY_BATCH_SIZE,
  );
}
复制代码

通过performance.now()生成 currentTime,当不支持performance时转利用Date.now(),这个值不用太过关注,只需要理解成时间戳即可。

const now1 = performance.now(); // 53028380
const now2 = performance.now(); // 53028389  ↑ 9
const now3 = performance.now(); // 53028391  ↑ 11
const now4 = performance.now(); // 53028405  ↑ 25
const now5 = performance.now(); // 53028420  ↑ 40
const now6 = performance.now(); // 53028430  ↑ 50
const now7 = performance.now(); // 53028444  ↑ 55
const now8 = performance.now(); // 53028468  ↑ 79

// LOW 任务
1073741821 - (((1073741821 - now1 + 500) / 25) | 0) * 25 - 25; // 53027871
1073741821 - (((1073741821 - now2 + 500) / 25) | 0) * 25 - 25; // 53027871 ↑ 0
1073741821 - (((1073741821 - now3 + 500) / 25) | 0) * 25 - 25; // 53027871 ↑ 0
1073741821 - (((1073741821 - now4 + 500) / 25) | 0) * 25 - 25; // 53027896 ↑ 25
1073741821 - (((1073741821 - now5 + 500) / 25) | 0) * 25 - 25; // 53027896 ↑ 25
1073741821 - (((1073741821 - now6 + 500) / 25) | 0) * 25 - 25; // 53027921 ↑ 50
1073741821 - (((1073741821 - now7 + 500) / 25) | 0) * 25 - 25; // 53027921 ↑ 50
1073741821 - (((1073741821 - now8 + 500) / 25) | 0) * 25 - 25; // 53027946 ↑ 75

// HIGH 任务 以DEV模式为例
1073741821 - (((1073741821 - now1 + 50) / 10) | 0) * 10 - 10; // 53028321
1073741821 - (((1073741821 - now2 + 50) / 10) | 0) * 10 - 10; // 53028331 ↑ 10
1073741821 - (((1073741821 - now3 + 50) / 10) | 0) * 10 - 10; // 53028331 ↑ 10
1073741821 - (((1073741821 - now4 + 50) / 10) | 0) * 10 - 10; // 53028351 ↑ 30
1073741821 - (((1073741821 - now5 + 50) / 10) | 0) * 10 - 10; // 53028361 ↑ 40
1073741821 - (((1073741821 - now6 + 50) / 10) | 0) * 10 - 10; // 53028371 ↑ 50
1073741821 - (((1073741821 - now7 + 50) / 10) | 0) * 10 - 10; // 53028391 ↑ 70
1073741821 - (((1073741821 - now8 + 50) / 10) | 0) * 10 - 10; // 53028411 ↑ 90
复制代码

通过规律,可以看到LOW优先级任务时,区间<25的,得到的都是同一个值,而HIGH高优先级任务的区间为10,单位为毫秒,这个有什么用呢?

如果触发了多次事件,每次难道都要丢enqueueUpdate里立即调度,那未免性能太差了。

React让两个相近(25ms内)的update得到相同的expirationTime,它可以将多次事件分批打包丢入 enqueueUpdate里,假如在24ms内触发了两个事件,那么React会将他们丢入同一批车,目的就是让这两个update自动合并成一个Update,并且只会触发一次更新,从而达到批量更新的目的。

从之前的代码看 computeInteractiveExpiration传入的是150、100,computeAsyncExpiration传入的是5000、250,前者的优先级更高,而过期执行时间为交互事件为 100/UNIT_SIZE = 10,异步事件则为 250/UNIT_SIZE = 25, 佐证了事实。

PureComponent

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;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
复制代码

Component 类相同属性的 PureComponent 类有所不同,首先创建了一个空函数 ComponentDummy,并将通过共享原型继承的方式将实例原型指向了 Component 的原型,其构造函数指定为PureComponent。其实就是在外面套了一层 pureComponentPrototypeComponent

createRef

import {createRef} from './ReactCreateRef';
复制代码
export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  if (__DEV__) {
    Object.seal(refObject);
  }
  return refObject;
}
复制代码

返回一个refObject,其current的属性在组件挂载时进行关联,与react-dom强相关,后面再了解。现在只需要知道它很简单。

Children

import {forEach, map, count, toArray, only} from './ReactChildren';
复制代码

React 将 children 的 API 暴露出来,这里最常使用的应该是 React.Children.mapReact.Children.forEach

Children.map

适用于替代 this.props.children.map ,因为这种写法通常用来嵌套组件,但是如果嵌套的是一个函数就会报错。而 React.Children.map 则不会。当需要写一个 Radio 组件需要依赖其父组件 RadioGroupprops 值,那么this.props.children.map 配合 cloneElement 简直不能再完美。还可以用来过滤某些组件。

React.cloneElement(props.children, {
  name: props.name
})

render(){
  return (
    <div>
        {
            React.Children.map(children, (child, i) => {
              if ( i < 1 ) return;
              return child;
            })
        }
    </div>
  )
}
复制代码

mapChildren

function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}
复制代码

mapIntoWithKeyPrefixInternal

第一步,如果子组件为null直接不处理。正常情况下申明一个数组,进行加工。

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  let escapedPrefix = '';
  if (prefix != null) { // 顾名思义,处理key
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }

  // 取出一个对象,作为上下文,遍历children
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  // 释放对象
  releaseTraverseContext(traverseContext);
}
复制代码

getPooledTraverseContext 与 releaseTraverseContext

第二步,处理key的暂时不用管。最终通过 getPooledTraverseContext 到对象池里取一个对象,给 traverseAllChildren 进行处理,结束的时候通过 releaseTraverseContext reset所有属性放回去,做到复用,避免了一次性创建大量对象和释放对象消耗性能造成的内存抖动。的概念在计算机领域被大量引用,包括Node的Buffer的内存分配策略也是用的池的概念。

getPooledTraverseContext 用来取。 releaseTraverseContext 用来清空后放回

// 维护一个大小为 10 的对象重用池
const POOL_SIZE = 10;
const traverseContextPool = [];

function getPooledTraverseContext(
  mapResult,
  keyPrefix,
  mapFunction,
  mapContext,
) {
  if (traverseContextPool.length) { // 如果当前对象池内有可用对象,就从队尾pop一个初始化后返回
    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,
    };
  }
}

function releaseTraverseContext(traverseContext) {
  traverseContext.result = null;
  traverseContext.keyPrefix = null;
  traverseContext.func = null;
  traverseContext.context = null;
  traverseContext.count = 0;
  if (traverseContextPool.length < POOL_SIZE) { // 如果对象池内没满,就放到对象池内,等待复用
    traverseContextPool.push(traverseContext);
  }
}
复制代码

traverseAllChildren/traverseAllChildrenImpl

第三步,最重要的一步,在取出一个待复用对象后,traverseAllChildren 判断为null就没必要处理了。直接 return。

function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0; // return
  }
  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
复制代码
function traverseAllChildrenImpl(
  children, // children
  nameSoFar, // 父级 key
  callback, // 如果是可渲染节点
  traverseContext, // 对象池复用对象
) {
  const type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    // 以上都被认为是null。
    children = null;
  }
  // 如果是可渲染的节点则为true,表示能调用callback
  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: // React元素 或者是 Portals
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
  }

  if (invokeCallback) { // 可渲染节点,直接调用回调
    callback( // 调用 mapSingleChildIntoContext
      traverseContext,
      children,
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

  let child;
  let nextName;
  let subtreeCount = 0; //在当前子树中找到的子级的层级数。
  const nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;

  if (Array.isArray(children)) { // 如果children是数组,则递归处理
  // 例如 React.Children.map(this.props.children, c => [[c, c]])
  // c => [[c, c]] 会被摊平为 [c, c, c, c]
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getComponentKey(child, i); // 在每一层不断用“:”分隔拼接key
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  } else { // 如果是对象的话通过 obj[Symbol.iterator] 取迭代器
    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 if (type === 'object') { // 如果迭代器是普通对象也就无法迭代
      let addendum = '';
      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;
}
复制代码
const Demo = ({ children }) => {
  console.log(React.Children.map(children, c => [[[[c, c]]]]));
  return (
    children
  );
};
const Children = ({ msg }) => (
  <span>
    { msg }
  </span>
);
复制代码

alt

上面函数的核心作用就是通过把传入的 children 数组通过遍历摊平成单个节点,其中迭代的所有callback都是 mapSingleChildIntoContext

mapSingleChildIntoContext

// bookKeeping getPooledTraverseContext 内从复用对象池取出来的 traverseContext
// child 传入的节点
// childKey 节点的 key
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;
  
  // func === React.Children.map(props.children, c => c) 的 c => c 函数
  let mappedChild = func.call(context, child, bookKeeping.count++);
  // 如果func返回值设定为数组 React.Children.map(this.props.children, c => [c, c])
  // 表示每个元素将被返回两次。假如children为 c1,c2,那么最后返回的应该是c1,c1,c2,c2
  if (Array.isArray(mappedChild)) {
    // 是数组的话,就再调用 mapIntoWithKeyPrefixInternal
    // 和 mapChildren 调用它的流程一样。递归将其铺平
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    // 如果是不为null并且是有效的 Element
    if (isValidElement(mappedChild)) {
      // 克隆 Element && 替换掉key 推入result
      mappedChild = cloneAndReplaceKey(
        mappedChild,
        keyPrefix + (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/' : '') + childKey,
      );
    }
    result.push(mappedChild);
  }
}
复制代码

最终的逻辑又回到了 mapIntoWithKeyPrefixInternal ,通过递归调用使返回的数组结果展开铺平。

整体流程

大概整体流程

`mapChildren` ===================> `mapIntoWithKeyPrefixInternal` ==================> `getPooledTraverseContext`(复用对象池)
                                                  /\                                                ||
                                                 /||\                                               ||
                                                  ||                                                ||
                                                  ||                                               \||/
                                                  ||                                                \/
                                                  ||Yes                                 `traverseAllChildren`(遍历children树)
                                                  ||                                                ||
                                                  ||                                                ||
                                                  ||                                               \||/
                                                  ||                                                \/
                                     No           ||                                        (children是数组又会重新递归执行)
`releaseTraverseContext`(释放对象池)<=====`mapSingleChildIntoContext`(铺平result)<=============`traverseAllChildrenImpl`
复制代码

Children.forEach

相比 mapforEachChildren 则简单的多,因为不用去返回一个新的结果,只需要对children做遍历,

function forEachChildren(children, forEachFunc, forEachContext) {
  if (children == null) {
    return children;
  }
  const traverseContext = getPooledTraverseContext(
    null, // 不需要返回数组,所以result为null
    null, // key也不需要
    forEachFunc,
    forEachContext,
  );
  // 第二个参数 forEachSingleChild 简单调用了 forEachFunc
  traverseAllChildren(children, forEachSingleChild, traverseContext);
  releaseTraverseContext(traverseContext);
}

function forEachSingleChild(bookKeeping, child, name) {
  const {func, context} = bookKeeping;
  func.call(context, child, bookKeeping.count++);
}

复制代码

Children.count

function countChildren(children) {
  return traverseAllChildren(children, () => null, null);
}
复制代码

用来计算children的个数,平时用的较少。和前面2个方法的行为差不多,预置的callback也不会进行任何处理。最终返回当前children的子元素,并不会向下递归查找。

Children.toArray

function toArray(children) {
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, child => child);
  return result;
}
复制代码

用来将children转化成普通的数组,原理和 mapChildren 一样,可以用来将传入的children重新进行排序。

 class Sort extends React.Component {
   render () {
     const children = React.Children.toArray(this.props.children);
     return <p>{children.sort((a,b)=>a-b).join('-')}</p>
   }
 }

 <Sort>
  {2}{5}{8}{4}{9}
 </Sort>

//  view
2-4-5-8-9
复制代码

Children.only

function onlyChild(children) {
  invariant(
    isValidElement(children),
    'React.Children.only expected to receive a single React element child.',
  );
  return children;
}
复制代码

校验children是否为ReactElement,是则返回,否则报错。可以用来制做一个只接受一个 children<Single> 组件。

class Single extends Component{
  render(){
    return React.Children.only(this.props.children)
  }
}

function App(){
  return (
      <Single>
        <div>first</div>
        <div>second</div> {/* error */}
      </Single>
  )
}
复制代码

ReactElement

import {
  createElement,
  createFactory,
  cloneElement,
  isValidElement,
  jsx,
} from './ReactElement';
const React = {
  // ...
  createElement: __DEV__ ? createElementWithValidation : createElement,
  cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
  createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
  isValidElement: isValidElement
}

复制代码

开发环境下会自动validator,切到packages/react/src/ReactElement.js,大纲如下

hasValidRef ------------------------------- 判断是否有合理的ref
hasValidKey ------------------------------- 判断是否有合理的key
defineRefPropWarningGetter ---------------- 锁定props.ref
defineKeyPropWarningGetter ---------------- 锁定props.key
ReactElement ------------------------------ 转化ReactElement
jsx --------------------------------------- 使用jsx方式创建Element
jsxDEV ------------------------------------ 使用jsx方式创建Element(DEV)
createElement ----------------------------- 创建并返回指定类型的ReactElement
createFactory ----------------------------- 工厂模式createElement构造器
cloneAndReplaceKey ------------------------ 替换新key
cloneElement ------------------------------ 克隆Element
isValidElement ---------------------------- 判定是否ReactElement
复制代码

createElement

这个方法大家耳熟能详。React用的最多的方法,没有之一。它负责React内所有元素节点的创建及初始化。

该方法接受三个参数 type, config, children ,type 是标签或者组件的名称,div/span/ul,对应的自定义Component首字母一定是大写。config 则包含所有的属性配置,children 代表子节点。

export function createElement(type, config, children) {
  let propName;

  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    if (hasValidRef(config)) {// 查找config内是否存在合理的ref
      ref = config.ref;
    }
    if (hasValidKey(config)) {// 查找config内是否存在合理的key
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 将剩余属性添加到新的props对象中
    // 这也就是为什么<Component key={Math.random()}/>子组件中为什么找不到key/ref/__self/__source属性的原因
    for (propName in config) {
      if (
        // 忽略原型链上的属性,并且抽离key/ref/__self/__source属性
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // children参数可以不止一个,除去前两个参数,其他的都是children
  const childrenLength = arguments.length - 2;

  // 对children做格式处理。一个为对象,多个则为Array
  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;
  }

  // 解析defaultProps,定义组件的默认Props属性
  // Com.defaultProps = { msg:'default' }
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  // 在开发环境下,锁定props上 key与 ref 的getter,不予获取
  if (__DEV__) {
    if (key || ref) {
      const displayName = typeof type === 'function'
          ? type.displayName || type.name || 'Unknown'
          : type;
      if (key) {
        defineKeyPropWarningGetter(props, displayName);
      }
      if (ref) {
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }
  // 最后转化成React元素
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current, // 创建React元素的组件
    props,
  );
}
复制代码
const ReactCurrentOwner = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Fiber),
};
复制代码

createElement 仅仅起到为ReactElement加工前过滤属性的作用。

create ReactElement

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // $$typeof将其标识为一个React元素
    $$typeof: REACT_ELEMENT_TYPE,

    // ReactElement的内置属性
    type: type,
    key: key,
    ref: ref,
    props: props,

    // 创建此元素的组件。
    _owner: owner,
  };
  //为了利于测试,在开发环境下忽略这些属性(不可枚举),并且冻结props与element
  if (__DEV__) {
    element._store = {};
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    });
    // self and source are DEV only properties.
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    });
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
};
复制代码

ReactElement 将其属性做二次处理,等待被渲染成DOM,在平常开发我们通过console.log打印出自定义Component 属性与element一致。

alt

createFactory

工厂模式的 createElement,通过预置 type 参数创建指定类型的节点。

var child1 = React.createElement('li', null, 'First Text Content');
var child2 = React.createElement('li', null, 'Second Text Content');
// 等价于
var factory = React.createFactory("li");
var child1 = factory(null, 'First Text Content');
var child2 = factory(null, 'Second Text Content');


var root  = React.createElement('ul', {className: 'list'}, child1, child2);
ReactDOM.render(root,document.getElementById('root'));
复制代码
export function createFactory(type) {
  // 通过bind预置type参数返回新的函数
  const factory = createElement.bind(null, type);
  // Expose the type on the factory and the prototype so that it can be easily accessed on elements. E.g. `<Foo />.type === Foo`.
  // This should not be named `constructor` since this may not be the function that created the element, and it may not even be a constructor.
  // Legacy hook: remove it
  factory.type = type;
  return factory;
}
复制代码

cloneElement

通过传入新的element与props及children,得到clone后的一个新元素。element 为cloneElement,config 是 newProps,可以重新定义 keyrefchildren 子节点。 整体方法与createElement大致相同。

export function cloneElement(element, config, children) {
  // 判断有效的ReactElement
  invariant(
    !(element === null || element === undefined),
    'React.cloneElement(...): The argument must be a React element, but you passed %s.',
    element,
  );

  let propName;

  // copy原始 props
  const props = Object.assign({}, element.props);

  // 提取保留key & ref
  let key = element.key;
  let ref = element.ref;
  // 为了追踪与定位,继承被clone的Element这三个属性
  const self = element._self;
  const source = element._source;
  let owner = element._owner;

  if (config != null) {
    // 这里的处理和createElement差不多
    // unique ,ref 和 key 可以自定义覆盖
    if (hasValidRef(config)) {
      ref = config.ref;
      owner = ReactCurrentOwner.current;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    // 其他属性覆盖current props
    let defaultProps;
    if (element.type && element.type.defaultProps) {
      defaultProps = element.type.defaultProps;
    }
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        if (config[propName] === undefined && defaultProps !== undefined) {
          // Resolve default props
          props[propName] = defaultProps[propName];
        } else {
          props[propName] = config[propName];
        }
      }
    }
  }
  // 剩余的与 createElement 一样
  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;
  }

  return ReactElement(element.type, key, ref, self, source, owner, props);
}
复制代码

cloneAndReplaceKey

顾名思义,与 cloneElement 名字上虽然差不多,但实际返回的是通过ReactElement传入newKey重新创建的旧Element。

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

  return newElement;
}
复制代码

isValidElement

也很简单,通过判断 $$typeof 是否为内置的ReactElement类型。

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

jsx/jsxDEV

没怎么使用过这个API,猜测应该是通过 react-dom 转化后使用的创建语法。

从代码逻辑上看,与 createElement 大致形同,jsxDEVjsx 多了两个能自定义的属性,sourceself,按照代码注释,是为了防止出现 <div key="Hi" {...props} /> 情况中 keyprops 先定义,导致被覆盖的情况。将对<div {...props} key="Hi" /> 之外的所有情况统一使用 jsxDEV 来强行赋值 key 与 ref。

export function jsxDEV(type, config, maybeKey, source, self) {
  let propName;

  // Reserved names are extracted
  const props = {};

  let key = null;
  let ref = null;

  // Currently, key can be spread in as a prop. This causes a potential issue if key is also explicitly declared (ie. <div {...props} key="Hi" /> or <div key="Hi" {...props} /> ). We want to deprecate key spread,
  // but as an intermediary step, we will use jsxDEV for everything except <div {...props} key="Hi" />, because we aren't currently able to tell if key is explicitly declared to be undefined or not.
  if (maybeKey !== undefined) {
    key = '' + maybeKey;
  }

  if (hasValidKey(config)) {
    key = '' + config.key;
  }

  if (hasValidRef(config)) {
    ref = config.ref;
  }

  // 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];
    }
  }

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

  if (key || ref) {
    const displayName =
      typeof type === 'function'
        ? type.displayName || type.name || 'Unknown'
        : type;
    if (key) {
      defineKeyPropWarningGetter(props, displayName);
    }
    if (ref) {
      defineRefPropWarningGetter(props, displayName);
    }
  }

  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}
复制代码

defineRefPropWarningGetter/defineKeyPropWarningGetter

key 是用来优化React渲染速度的,而 ref 是用来获取到React渲染后的真实DOM节点。正常情况下应该将这两个属性置之世外,仿佛这两个属性都应该是React本身的API。所以这两个方法就是用来禁止获取和设置的。

let specialPropKeyWarningShown, specialPropRefWarningShown;

function defineKeyPropWarningGetter(props, displayName) {
  const warnAboutAccessingKey = function() {
    if (!specialPropKeyWarningShown) { // 只会读取一次
      specialPropKeyWarningShown = true;
      warningWithoutStack(
        false,
        '%s: `key` is not a prop. Trying to access it will result ' +
          'in `undefined` being returned. If you need to access the same ' +
          'value within the child component, you should pass it as a different ' +
          'prop. (https://fb.me/react-special-props)',
        displayName,
      );
    }
  };
  warnAboutAccessingKey.isReactWarning = true;
  Object.defineProperty(props, 'key', {
    get: warnAboutAccessingKey,
    configurable: true,
  });
}
复制代码

当在组件内尝试 console.log(props.key) 的时候,就会发现报错。

alt

两个方法逻辑一模一样,就不写粘贴两遍了。

hasValidRef/hasValidKey

这两个方法差不多,在开发模式下多了一个校验,通过 Object.prototype.hasOwnProperty 检查当前对象属性上是否存在 ref/key,并获取其访问器函数 get,如果事先被defineKeyPropWarningGetter/defineRefPropWarningGetter 锁定则 getter.isReactWarning 就必然为 true(注意锁定方法调用的时机)。

function hasValidRef(config) {
  if (__DEV__) {
    if (hasOwnProperty.call(config, 'ref')) {
      const getter = Object.getOwnPropertyDescriptor(config, 'ref').get;
      if (getter && getter.isReactWarning) {
        return false;
      }
    }
  }
  return config.ref !== undefined;
}
复制代码
关注下面的标签,发现更多相似文章
评论