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的使用,提供运行时检查能够有效提醒开发者去处理,例如:

React 16.3新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 DOMStrict ModeReact v16.3.0: New lifecycles and context API

更多相关文章

  1. 函数式编程中如何处理副作用?
  2. SpringMVC源码分析:一个request请求的完整流程和各组件介绍
  3. 几款代码高亮组件的体验,说不定你以后会用到
  4. 帆软报表自定义函数-取json数据
  5. 函数和递归
  6. java的getClass()函数
  7. 函数的学习
  8. java多线程(3)Thread构造函数解析
  9. JavaScript 测试教程–part 3:测试 props,挂载函数和快照测试[每日

随机推荐

  1. 使用Jquery Ajax更改按钮的颜色(从外部PHP
  2. HTML5音频播放,歌词同步,及视频播放功能(JPl
  3. JS获取文件名的方法
  4. Google Maps API v3:如何设置缩放级别和地
  5. 深入理解JavaScript 中为什么没有重载?
  6. 输入类型=日期的日期显示为dd-mm-yyyy格
  7. classList介绍和原生JavaScript实现addCl
  8. js中常见的操作
  9. Python和Visual Studio需要安装Node.js模
  10. 在javascript中使用onclick在使用onclick