Zong
新的挑战即将开始

React 源码阅读计划

截止 11 月底,学习版 React 编写基本完成,很高兴终于基本把 16.8.6 版本进度完成,从 5 月至今已时过半年之久,陆陆续续算是把她完成了。(喝彩)

来记录一下最近学习过程中遇到的问题,那么先来看到一段代码:

class ExampleApplication extends React.Component {
  constructor(props) {
    super(props)
  }
  render() {
    return <p>Example Application</p>
  }
}

ReactDOM.render(
  <ExampleApplication />,
  document.getElementById('container')
)

我们知道,上面代码中,我们定义了一个 ClassComponent 类组件,但是在进入 beginWork (ReactFiberBeginWork.js) 函数时,她却跳进了 IndeterminateComponent 判断分支,而没有进入她应该进入的 ClassComponent 分支,这使得我很困惑,为什么会这样呢(已忽略无关代码)?

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  switch (workInProgress.tag) {
    case IndeterminateComponent: {    // <== 出乎意料的进入此分支
      const elementType = workInProgress.elementType;
      return mountIndeterminateComponent(
        current,
        workInProgress,
        elementType,
        renderExpirationTime,
      );
    }
    case ClassComponent: {    // <== 为什么没有进入应该进入的分支?
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime,
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderExpirationTime);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderExpirationTime);
  }
}

我开始查找原因,直到我看到 createFiberFromTypeAndProps (ReactFiber.js) 函数(已忽略无关代码):

function shouldConstruct(Component: Function) {
  const prototype = Component.prototype;
  return !!(prototype && prototype.isReactComponent);
}

export function createFiberFromTypeAndProps(
  type: any, // React$ElementType
  key: null | string,
  pendingProps: any,
  owner: null | Fiber,
  mode: TypeOfMode,
  expirationTime: ExpirationTime,
): Fiber {
  let fiber;

  // 原来 IndeterminateComponent 是初始化 Fiber 时的初始 WorkTag 
  // 所以才会造成我上面产生的困惑
  let fiberTag = IndeterminateComponent;
  // The resolved type is set if we know what the final type will be. I.e. it's not lazy.
  let resolvedType = type;

  // 这里是 ClassComponent 需要进入的分支逻辑
  if (typeof type === 'function') {

    // 问题就出在我没有进入到这个分支, shouldConstruct 函数执行返回 false
    // 导致完全跳过这里的逻辑判断,直接进入 createFiber 函数
    // 上面准备了 shouldConstruct ,我相信看到源码的你,很容易就能发现问题所在
    if (shouldConstruct(type)) {
      fiberTag = ClassComponent;
    }
  }

  fiber = createFiber(fiberTag, pendingProps, key, mode);
  fiber.elementType = type;
  fiber.type = resolvedType;
  fiber.expirationTime = expirationTime;

  return fiber;
}

到这里,我发现了是因为校验 isReactComponent 失败才出现上述问题的,那么着手修复即可。我们在 ReactBaseClasses.js 中加入 Component.prototype.isReactComponent = {}; 即可。

在后续又遇到了一个很诡异的 state 丢失问题,我知道,我也很清楚肯定是我哪里代码漏写才导致的,我又开始了无尽的调试,考虑从类的 constructor 函数开始调试,其实可以直接看到 constructClassInstance (ReactFiberClassComponent.js) 函数(已忽略无关代码):

function constructClassInstance(
  workInProgress: Fiber,
  ctor: any,
  props: any,
  renderExpirationTime: ExpirationTime,
): any {
  let context = null;

  const instance = new ctor(props, context);

  // 因为在学习时,我是直接跳过 __DEV__ 情况下的分支的
  // 所以不慎将下面这一行代码删除了
  // 这行代码中除了声明 state ,还有一步赋值操作
  // 就是对 workInProgress.memoizedState 值的赋值操作
  const state = (workInProgress.memoizedState =
    instance.state !== null && instance.state !== undefined
      ? instance.state
      : null);
  adoptClassInstance(workInProgress, instance);

  return instance;
}

function mountClassInstance(
  workInProgress: Fiber,
  ctor: any,
  newProps: any,
  renderExpirationTime: ExpirationTime,
): void {
  const instance = workInProgress.stateNode;
  instance.props = newProps;

  // 由于上面那一行代码的丢失,导致这一步赋值操作永远为 null
  instance.state = workInProgress.memoizedState;
  instance.refs = emptyRefsObject;
}

到此为止, state 丢失问题也得以解决,进入 mountClassInstance 函数其实就是在进入 ClassComponent 分支之后执行的代码,所以真的是环环相扣啊。

遇到的问题也分享完了,那么接下来安排一下《React 源码学习》系列文章的后续章节内容(暂定):

  1. React 源码学习(九):16.8.6 的概况与变化
  2. React 源码学习(十):Fiber 数据结构
  3. React 源码学习(十一):调度机制 Scheduler
  4. React 源码学习(十二):调和机制 Reconciler
  5. React 源码学习(未知): React Hook(因为没有读)

建立自己的前端知识体系

  • 计划在新的一年逐步建立自己的前端知识体系,并进行不断完善和补全,从而全方面提升自己。

Ant Design Hank 小记

迭代过程中发现了一个 UI 样式的局限性,关于 Tabel 组件,官方提供了 scroll 属性可用于控制列宽,但是当我在列中配置 fixed 属性后,并且列的总宽度计算不及屏幕宽度时,就会出现 Table 撕裂现象,当然此情况是出现在 3.15.2 版本(我司目前固定为此版本),借此我能做的就是再此基础上打补丁,毕竟 fork 下来改不合适,要改的地方连锁反应太大, Tabel 组件 基于 rc-table ,当然我是去看了源码的(已忽略无关代码):

// https://github.com/react-component/table/blob/6.4.0/src/ColGroup.js#L25

  cols = cols.concat(
    leafColumns.map(c => {
      return <col key={c.key || c.dataIndex} style={{ width: c.width, minWidth: c.width }} />;
    }),
  );

看到这里,我留下了眼泪,他只接受唯一一个样式参数 width ,我哭了,所以经过考虑,如果出现上述场景,我们考虑自动调整尾列宽度来修复此问题,借助 ref 来获取当前组件实际在屏幕中的宽度后进行补全计算,代码我就不贴了,因为很简单。

日语学习计划

截止 11 月底,近 2 周没学习日语,决定在后期进行计划,固定时间学习日语。