React 16.3新API

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那里拿到值),Providervalue 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.

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

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

用来校验ConsumerProvider组件是否匹配:

// 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节点上带有依赖链表firstContextDependencyProvidervalue发生变化时通知所有依赖项,大致如下:

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

此外还有两种组件,ProviderConsumer

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类似,unmountcurrent会被置为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

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

  • 触发深层inputfocus(如自动聚焦搜索框)

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

  • 重新定位DOM元素(如tooltip

从组件角度分为两类:

  • DOM包装组件

  • 高阶组件(High Order Component)

上面提到的3个场景都属于DOM包装组件,比如MyInputMyDialogMyTooltip,特点是对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作为独立参数,从而避免用不叫refprop传递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.更多合法ReactElementreact/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.jsreact/packages/react-reconciler/src/ReactFiberCommitWork.js,清晰起见,不太重要的部分都删掉了)

挂载阶段实际上并不关心对象ref的来源(无论层层传递过来的还是自己创建的都一样),更新也没什么特殊的,用新的propsrefrender,卸载就是置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的使用,提供运行时检查能够有效提醒开发者去处理,例如:

  • strict mode unsafe lifecycles warning

而副作用检测对于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生命周期函数

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

参考资料

React 16.3新API》上有1条评论

  1. knive Danmark

    I was cսrious if you ever thougһt of changing the strսcture of your website? Its very well written; I love ѡhat youve got to say.

    But maybе you could a little more in the way of content so people c᧐uld conneϲt witgh it bettеr. Youve got an awdul lot of text foor only having one or two pictures. Maybe you coyld space it out better?

    回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code