createContext 之前也有context,相当于自动向下传递的props,子树中的任意组件都可以从context中按需取值(配合contextTypes声明)

像props一样,context的作用也是自上而下传递数据,通常用于多语言配置、主题和数据缓存等场景,这些场景有几个特点:

同一份数据需要被多个组件访问

这些组件处于不同的嵌套层级

从数据传递的角度看,props是一级数据共享,context是子树共享。如果没有context特性的话,就需要从数据源组件到数据消费者组件逐层显式传递数据(props),一来麻烦,二来中间组件没必要知道这份数据,逐层传递造成了中间组件与数据消费者组件的紧耦合。而context特性能够相对优雅地解决这两个问题,就像是props机制的补丁

P.S.实际上,要解耦中间组件与数据消费者组件的话,还有另一种方法:把填好数据的组件通过props传递下去,而不直接传递数据。这样中间组件就不需要知道数据消费者组件的内部细节(如依赖的数据)了,只知道这个位置将被插入某个组件(也就是组件组合,类似于Vue的slot特性),这种思路有点IoC的意思,具体见Before You Use Context

createContext API算是对context特性的重新实现(可替代之前的context):


const {Provider, Consumer} = React.createContext(defaultValue);
<Provider value={/* some value */}>
<Consumer>
  {value => /* render something based on the context value */}
</Consumer>

P.S.旧的context API在v16.x仍然可用,但之后会被移除掉

只维护value(没有key),创建时给定默认值,通过Provider组件写,通过Consumer组件来读

一个Provider可以对应多个Consumer,内层Provider能够重写外层Provider的值(实际上Consumer会从组件树中与之匹配的最近Provider那里拿到值),Provider的value prop发生变化时会通知所有后代Consumer重新渲染(直接通知,不走shouldComponentUpdate)

P.S.默认值比较有意思,如果Consumer没有与之匹配的Provider,就走defaultValue。作用是在单测等场景,Consumer可以不需要Provider自己跑

P.S.比较新旧value,确定是否发生了变化,走的是Object.is()浅对比逻辑(引用类型只比较引用)

内部实现 context类型定义如下:


export type ReactContext<T> = {
  $$typeof: Symbol | number,
  Consumer: ReactContext<T>,
  Provider: ReactProviderType<T>,
  unstable_read: () => T,

  _calculateChangedBits: ((a: T, b: T) => number) | null,

  _currentValue: T,
  _currentValue2: T,
  _changedBits: number,
  _changedBits2: number,

  // DEV only
  _currentRenderer?: Object | null,
  _currentRenderer2?: Object | null,
};

export type ReactProviderType<T> = {
  $$typeof: Symbol | number,
  _context: ReactContext<T>,
};

看起来比较奇怪,带两份_currentValue等值属性是为了支持多renderer并发工作(使之互不影响):


As a workaround to support multiple concurrent renderers, we categorize some renderers as primary and others as secondary. We only expect there to be two concurrent renderers at most: React Native (primary) and Fabric (secondary); React DOM (primary) and React ART (secondary). Secondary renderers store their context values on separate fields.

Consumer和Provider两个属性很有意思,存在循环引用:


context = {
  Consumer: context,
  Provider: {
    _context: context
  }
}

用来校验Consumer和Provider组件是否匹配:


// Check if the context matches.
dependency.context === context && (dependency.observedBits & changedBits) !== 0

createContext实现如下:

export function createContext<T>(
  defaultValue: T,
  calculateChangedBits: ?(a: T, b: T) => number,
): ReactContext<T> {
  if (calculateChangedBits === undefined) {
    calculateChangedBits = null;
  }

  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    _calculateChangedBits: calculateChangedBits,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    _changedBits: 0,
    _changedBits2: 0,
    // These are circular
    Provider: (null: any),
    Consumer: (null: any),
    unstable_read: (null: any),
  };

  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
  context.Consumer = context;
  context.unstable_read = readContext.bind(null, context);

  return context;
}

在渲染阶段把Provider组件身上的value prop转移到context对象上:


export function pushProvider(providerFiber: Fiber, changedBits: number): void {
  const context: ReactContext<any> = providerFiber.type._context;
  context._currentValue = providerFiber.pendingProps.value;
  context._changedBits = changedBits;
}

Consumer读取value时建立依赖关系:


export function readContext<T>(
  context: ReactContext<T>,
  observedBits: void | number | boolean,
): T {
  let contextItem = {
    context: ((context: any): ReactContext<mixed>),
    observedBits: resolvedObservedBits,
    next: null,
  };

  if (lastContextDependency === null) {
    // This is the first dependency in the list
    currentlyRenderingFiber.firstContextDependency = lastContextDependency = contextItem;
  } else {
    // Append a new context item.
    lastContextDependency = lastContextDependency.next = contextItem;
  }
  return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}

fiber节点上带有依赖链表firstContextDependency,Provider的value发生变化时通知所有依赖项,大致如下:


export function propagateContextChange(
  workInProgress: Fiber,
  context: ReactContext<mixed>,
  changedBits: number,
  renderExpirationTime: ExpirationTime,
): void {
    // 遍历fiber子树,找出第一个与context匹配的Consumer或Provider
    while (fiber !== null) {
      // 遍历fiber节点的所有context依赖
      do {
        // 检查是否匹配
        // 匹配的话,标记该fiber需要更新,等待调度
      } while (dependency !== null);
    }
}

P.S.具体实现细节见react/packages/react-reconciler/src/ReactFiberNewContext.js

此外还有两种组件,Provider与Consumer:

export type ReactProvider<T> = {
  $$typeof: Symbol | number,
  type: ReactProviderType<T>,
  key: null | string,
  ref: null,
  props: {
    value: T,
    children?: ReactNodeList,
  },
};

export type ReactConsumer<T> = {
  $$typeof: Symbol | number,
  type: ReactContext<T>,
  key: null | string,
  ref: null,
  props: {
    children: (value: T) => ReactNodeList,
    unstable_observedBits?: number,
  },
};

Consumer看起来比较特殊,其props.children是个value => ReactNodeList的函数

createRef 之前版本中,ref有2种形式:

字符串形式

函数形式

示例:


<div ref="mask"></div>
<div ref={(node) => this.maskNode = node}></div>

前者方便易用,后者更安全(unmount时候会给null掉,游离节点引发的内存风险降低不少)

此外,字符串ref还有很多缺陷:

要兼容Closure Compiler高级模式的话,必须把this.refs['myname']标识为字符串(具体见Types in the Closure Type System)

不允许单一实例有多个owner

动态字符串会妨碍VM优化

在异步批量渲染下存在问题,因为是同步处理的,需要始终保持一致

可以通过hook获取到兄弟ref,但破坏了组件的封装性

不支持静态类型化,在类似TypeScript的(强类型)语言中,每次用到都必须显式转换

由子组件调用的回调中无法把ref绑定到正确的owner上,例如<Child renderer={index => <div ref="test">{index}</div>} />中的ref会被挂在执行改回调的组件上,而不是当前owner

希望ref能够传递,能有多个owner,以及适应异步批处理场景……关于此话题的更多讨论,见Implement Better Refs API

第3种ref不是字符串也不是函数,而是个对象(故称之为对象ref):


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

  return refObject;
}

也就是说:


class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}

这里给div指定的ref属性,实际上是个对象(身上有个current属性),所以用法是这样:


const node = this.myRef.current;
const myComponent = this.myComponentRef.current;

就实现而言,与之前的字符串ref相比,不过是包了一层对象而已。其类型定义如下:


export type RefObject = {|
  current: any,
|};

P.S.其中|...|的Flow类型定义表示禁止扩展(Object.seal())

RefObject是仅含一个current key的对象,这样做有3个好处:

相对安全。与函数ref类似,unmount时current会被置为null,一定程度上降低了内存风险

适用于函数式组件。因为对象ref不与组件实例强关联(不要求创建实例,函数ref也具有这个优势)

可传递,也能有多个owner。这一点比函数ref和字符串ref都强大,反正只是个对象,多个组件持有也没关系,比其它两个灵活

P.S.之所以说“一定程度上”,是因为非要this.cachedNode = this.myRef.current这么干的话,肯定是null不掉的(包的这一层引用隔离,可以轻易突破)

P.S.虽然有了新的对象ref,但并没有废弃前两个,3者目前的状态是:

对象ref:因可传递等特性,建议使用

函数ref:因其灵活性而得以保留,建议使用

字符串ref:不建议使用,并且在后续版本可能被移除掉

函数形式的ref提供了更细粒度的控制(fine-grain control),包括ref绑定、解绑的时机

P.S.对象ref很大程度上是作为字符串ref的替代品推出的,所以建议用对象,废弃字符串ref

forwardRef 大多数场景用不着,但在几个典型场景很关键:

触发深层input的focus(如自动聚焦搜索框)

计算元素宽高尺寸(如JS布局方案)

重新定位DOM元素(如tooltip)

从组件角度分为两类:

DOM包装组件

高阶组件(High Order Component)

上面提到的3个场景都属于DOM包装组件,比如MyInput、MyDialog、MyTooltip,特点是对DOM节点的包装/增强。从使用角度看,与input、select等原生DOM节点地位一样,能构成视图,并且可交互。而交互的支持依赖对原生DOM节点的控制,比如无论包多少层,想要focus效果的话,最终还是要触发input节点的对应行为,这种场景下,ref传递就成了刚需


These components tend to be used throughout the application in a similar manner as a regular DOM button and input, and accessing their DOM nodes may be unavoidable for managing focus, selection, or animations.

P.S.实际应用中,甚至见到过类似this.refs.wapper.refs.node的奇技淫巧,这实际上就是对ref传递特性的强烈需求

而高阶组件一般是对组件功能的增强/扩展,因此天生就面临ref传递的问题,包了一层之后ref就不能直接访问了,但又没有太好的方式向下传递,所以一直是个问题(以不太优雅的方式维持ref链)

不使用forwardRef API的话,可以这样解决:


function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.inputElement = React.createRef();
  }
  render() {
    return (
      <CustomTextInput inputRef={this.inputElement} />
    );
  }
}

(摘自gaearon/dom_ref_forwarding_alternatives_before_16.3.md)

姑且称之为别名ref prop传递,说白了就是通过props向下传递一个ref载体(this.inputElement),到达目标节点后与之关联起来(ref={props.inputRef}),类似于:


function CustomTextInput(props) {
  return (
    <div>
      <input ref={node => props.refHost.node = node} />
    </div>
  );
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.refHost = {};
  }
  render() {
    return (
      <CustomTextInput refHost={this.refHost} />
    );
  }
}

forwardRef API提供了一种比较优雅的解决方案:


let CustomTextInput = React.forwardRef((props, ref) => (
  <div>
    <input ref={ref} />
  </div>
));

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = {};
  }
  render() {
    return (
      <CustomTextInput ref={this.inputRef} />
    );
  }
}

对比上面第一种替代方案,几乎一模一样,无非是把ref作为独立参数,从而避免用不叫ref的prop传递ref的尴尬

在高阶组件的场景,这样做:


function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps);
      console.log('new props:', this.props);
    }

    render() {
      const {forwardedRef, ...rest} = this.props;

      // Assign the custom prop "forwardedRef" as a ref
      return <Component ref={forwardedRef} {...rest} />;
    }
  }

  // Note the second param "ref" provided by React.forwardRef.
  // We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
  // And it can then be attached to the Component.
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />;
  });
}

因为React.forwardRef接受一个render函数,非常适合函数式组件,而对class形式的组件不太友好,所以上例这样的高阶函数场景,实质上是通过forwardRef + 别名ref prop传递来解决的

内部实现 与ref载体的思路几乎没什么区别,甚至其内部实现也差不多

先看API入口:


function forwardRef<Props, ElementType: React$ElementType>(
  render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {

  return {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
}

React.forwardRef接受一个(props, ref) => React$Node类型的render函数作为参数,返回值是一种新的React$Node(即合法ReactElement,用来描述视图结构的对象),相当于给这参数传入的render函数添上了类型标识

P.S.更多合法ReactElement见react/packages/shared/isValidElementType.js

内部根据该类型标识区分出来之后,做一些额外处理,包括挂载、更新和卸载3部分:


// 挂载
function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    const instance = finishedWork.stateNode;
    ref.current = instance;
  }
}

// 更新
function updateForwardRef(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
) {
  const render = workInProgress.type.render;
  const nextProps = workInProgress.pendingProps;
  const ref = workInProgress.ref;

  let nextChildren = render(nextProps, ref);

  reconcileChildren(
    current,
    workInProgress,
    nextChildren,
    renderExpirationTime,
  );
  return workInProgress.child;
}

// 卸载
function commitDetachRef(current: Fiber) {
  const currentRef = current.ref;
  if (currentRef !== null) {
    currentRef.current = null;
  }
}

(摘自react/packages/react-reconciler/src/ReactFiberBeginWork.js、react/packages/react-reconciler/src/ReactFiberCommitWork.js,清晰起见,不太重要的部分都删掉了)

挂载阶段实际上并不关心对象ref的来源(无论层层传递过来的还是自己创建的都一样),更新也没什么特殊的,用新的props和ref去render,卸载就是置null,实现其实比较简单

StrictMode


StrictMode is a tool for highlighting potential problems in an application.

React.StrictMode用来开启子树严格检查,是个内置组件:


import React from 'react';

function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>
      <Footer />
    </div>
  );
}

有几个特点:

不渲染UI,像Fragment一样

会为后代组件(即子树级)开启额外的检查和警告提示

仅在development环境有效,不影响production版本

主要有4个作用:

识别具有unsafe生命周期的组件

字符串ref警告

检测非预期的副作用

检测旧的context context API

P.S.以后还会添加更多功能

unsafe、字符串ref、旧context API检查的实际意义是保障API废弃决策可靠推进,尤其是涉及第三方依赖的场景,很难确认是否存在即将过时的API的使用,提供运行时检查能够有效提醒开发者去处理,例如:

而副作用检测对于Async Rendering特性是很有意义的,第一阶段涉及很多组件方法:


constructor componentWillMount componentWillReceiveProps componentWillUpdate getDerivedStateFromProps shouldComponentUpdate render setState updater functions (the first argument) `



也就是说,这些函数将来(开启异步渲染特性之后)可能会被调用多次,所以要求不含副作用(即idempotent,调用多次和调用一次产生的效果完全一样)。但问题是,副作用很难被检测到,StrictMode也做不到,所以做了这样一件事情:
```

By intentionally double-invoking methods like the component constructor, strict mode makes patterns like this easier to spot.
```

具体地,故意多调1次这些函数:

class组件的构造函数

render函数

setState传入的更新函数

getDerivedStateFromProps生命周期函数

算是多少有点帮助吧,既然无法帮助解决问题,那就想办法帮助暴露问题

参考资料
Refs and the DOM

Strict Mode

React v16.3.0: New lifecycles and context API