React总结

210 阅读52分钟

文章是对react官方文档的总结。

1 代码分割

1.1 import()

你的应用中引入代码分割的最佳方式是通过动态import()语法。使用之前

    import { add } from './math';
    console.log(add(16, 26));

使用之后

    import("./math").then(math => {
        console.log(math.add(16, 26));
    });

当 Webpack 解析到该语法时,会自动进行代码分割。如果你使用 Create React App,该功能已开箱即用,你可以立刻使用该特性。

2.1

React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。使用之前

    import OtherComponent from './OtherComponent';

使用之后

    const OtherComponent = React.lazy(() => import('./OtherComponent'));

React.lazy接受一个函数,这个函数需要动态调用import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 defalut exportReact 组件。`

然后应在Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。

    import React, { Suspense } from 'react';
    const OtherComponent = React.lazy(() => import('./OtherComponent'));
    function MyComponent() {
      return (
        <div>
          <Suspense fallback={<div>Loading...</div>}>
            <OtherComponent />
          </Suspense>
        </div>
      );
    }

2.2 命名导出

React.lazy 目前只支持默认导出(default exports)。如果你想被引入的模块使用命名导出(named exports),你可以创建一个中间模块,来重新导出为默认模块。这能保证 tree shaking 不会出错,并且不必引入不需要的组件。

    // ManyComponents.js
    export const MyComponent = /* ... */;
    export const MyUnusedComponent = /* ... */
    // MyComponent.js
    export { MyComponent as default } from "./ManyComponents.js"  
    // MyApp.js
    import React, { lazy } from 'react';
    const MyComponent = lazy(() => import("./MyComponent.js"));

3 Context

使用 context, 我们可以避免通过中间元素传递 props

    const MyContext = React.createContext(defaultValue);
    // Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
    // 创建一个 context(“light”为默认值)。
    const ThemeContext = React.createContext('light');
    class App extends React.Component {
      render() {
        // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
        // 无论多深,任何组件都能读取这个值。
        return (
          <ThemeContext.Provider value="dark">
            <Toolbar />
          </ThemeContext.Provider>
        );
      }
    }
    // 中间的组件再也不必指明往下传递 theme 了。
    function Toolbar() {
      return (
        <div>
          <ThemedButton />
        </div>
      );
    }
    class ThemedButton extends React.Component {
      // 指定 contextType 读取当前的 theme context。
      // React 会往上找到最近的 theme Provider,然后使用它的值。
      // 在这个例子中,当前的 theme 值为 “dark”。
      static contextType = ThemeContext;
      render() {
        return <Button theme={this.context} />;
      }
    }

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Providervalue 时,消费组件的 defaultValue 不会生效。

Providervalue 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。

    <MyContext.Consumer>
      {value => /* 基于 context 值进行渲染*/}
    </MyContext.Consumer>

这里,React 组件也可以订阅到 context 变更。这能让你在函数式组件中完成订阅 context

这需要函数作为子元素(function as a child)这种做法。这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext()defaultValue

4 错误边界

部分 UIJavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。

错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

:错误边界无法捕获以下场景中产生的错误:

  • 事件处理
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
  • 服务端渲染
  • 它自身抛出来的错误(并非它的子组件)

如果一个 class 组件中定义了 static getDerivedStateFromError()componentDidCatch() 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }

      static getDerivedStateFromError(error) {
        // 更新 state 使下一次渲染能够显示降级后的 UI
        return { hasError: true };
      }

      componentDidCatch(error, errorInfo) {
        // 你同样可以将错误日志上报给服务器
        logErrorToMyService(error, errorInfo);
      }

      render() {
        if (this.state.hasError) {
          // 你可以自定义降级后的 UI 并渲染
          return <h1>Something went wrong.</h1>;
        }

        return this.props.children; 
      }
    }

React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。

5 Refs 转发

    const FancyButton = React.forwardRef((props, ref) => (
      <button ref={ref} className="FancyButton">
        {props.children}
      </button>
    ));

    // 你可以直接获取 DOM button 的 ref:
    const ref = React.createRef();
    <FancyButton ref={ref}>Click</FancyButton>;

以下是对上述示例发生情况的逐步解释:

  • 调用 React.createRef创建了一个 React ref 并将其赋值给 ref 变量。
  • 指定 refJSX 属性,将其向下传递给 <FancyButton ref={ref}>
  • React 传递 refforwardRef 内函数 (props, ref) => ...,作为其第二个参数。
  • 我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
  • ref 挂载完成,ref.current 将指向 <button> DOM 节点。

:第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收ref 参数,且 props 中也不存在 ref

Ref 转发不仅限于 DOM 组件,你也可以转发 refsclass 组件实例中。

6 高阶组件

高阶组件是参数为组件,返回值为新组件的函数。

    function logProps(WrappedComponent) {
      return class extends React.Component {
        componentDidUpdate(prevProps) {
          console.log('Current props: ', this.props);
          console.log('Previous props: ', prevProps);
        }
        render() {
          // 将 input 组件包装在容器中,而不对其进行修改。Good!
          return <WrappedComponent {...this.props} />;
        }
      }
    }

Reactdiff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。

通常,你不需要考虑这点。但对 HOC 来说这一点很重要,因为这代表着你不应在组件的 render 方法中对一个组件应用 HOC

    render() {
      // 每次调用 render 函数都会创建一个新的 EnhancedComponent
      // EnhancedComponent1 !== EnhancedComponent2
      const EnhancedComponent = enhance(MyComponent);
      // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
      return <EnhancedComponent />;
    }

7 深入JSX

你不能将通用表达式作为 React 元素类型。如果你想通过通用表达式来(动态)决定元素类型,你需要首先将它赋值给大写字母开头的变量。这通常用于根据 prop 来渲染不同组件的情况下:

    import React from 'react';
    import { PhotoStory, VideoStory } from './stories';

    const components = {
      photo: PhotoStory,
      video: VideoStory
    };

    function Story(props) {
      // 错误!JSX 类型不能是一个表达式。
      return <components[props.storyType] story={props.story} />;
    }

要解决这个问题, 需要首先将类型赋值给一个大写字母开头的变量:

    import React from 'react';
    import { PhotoStory, VideoStory } from './stories';

    const components = {
      photo: PhotoStory,
      video: VideoStory
    };

    function Story(props) {
      // 正确!JSX 类型可以是大写字母开头的变量。
      const SpecificStory = components[props.storyType];
      return <SpecificStory story={props.story} />;
    }

当你将字符串字面量赋值给 prop 时,它的值是未转义的。所以,以下两个 JSX 表达式是等价的:

    <MyComponent message="&lt;3" />
    <MyComponent message={'<3'} />
通常,`JSX` 中的 `js` 表达式将会被计算为字符串、`React` 元素或者是列表。不过,`props.children` 和其他 `prop` 一样,它可以传递任意类型的数据,而不仅仅是 `React` 已知的可渲染类型。例如,如果你有一个自定义组件,你可以把回调函数作为 `props.children` 进行传递:
    // 调用子元素回调 numTimes 次,来重复生成组件
    function Repeat(props) {
      let items = [];
      for (let i = 0; i < props.numTimes; i++) {
        items.push(props.children(i));
      }
      return <div>{items}</div>;
    }

    function ListOfTenThings() {
      return (
        <Repeat numTimes={10}>
          {(index) => <div key={index}>This is item {index} in the list</div>}
        </Repeat>
      );
    }

你可以将任何东西作为子元素传递给自定义组件,只要确保在该组件渲染之前能够被转换成 React 理解的对象。这种用法并不常见,但可以用于扩展 JSX

false, null, undefined, and true 是合法的子元素。但它们并不会被渲染。以下的 JSX 表达式渲染结果相同:

<div />
<div></div>
<div>{false}</div>
<div>{null}</div>
<div>{undefined}</div>
<div>{true}</div>

这有助于依据特定条件来渲染其他的 React 元素。例如,在以下 JSX 中,仅当 showHeadertrue 时,才会渲染 <Header /> 组件:

    <div>
      {showHeader && <Header />}
      <Content />
    </div>

值得注意的是有一些 falsy 值,如数字 0,仍然会被 React 渲染。例如,以下代码并不会像你预期那样工作,因为当 props.messages 是空数组时,0 仍然会被渲染:

    <div>
      {props.messages.length &&
        <MessageList messages={props.messages} />
      }
    </div>

要解决这个问题,确保 && 之前的表达式总是布尔值:

<div>
  {props.messages.length > 0 &&
    <MessageList messages={props.messages} />
  }
</div>

8 组合

有些组件无法提前知晓它们子组件的具体内容。在 Sidebar(侧边栏)和 Dialog(对话框)等展现通用容器的组件中特别容易遇到这种情况。

我们建议这些组件使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中:

    function FancyBorder(props) {
      return (
        <div className={'FancyBorder FancyBorder-' + props.color}>
          {props.children}
        </div>
      );
    }

这使得别的组件可以通过 JSX 嵌套,将任意组件作为子组件传递给它们。

    function WelcomeDialog() {
      return (
        <FancyBorder color="blue">
          <h1 className="Dialog-title">
            Welcome
          </h1>
          <p className="Dialog-message">
            Thank you for visiting our spacecraft!
          </p>
        </FancyBorder>
      );
    }

<FancyBorder> JSX 标签中的所有内容都会作为一个 children prop 传递给 FancyBorder 组件。因为 FancyBorder{props.children} 渲染在一个 <div> 中,被传递的这些子组件最终都会出现在输出结果中。

少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用 children,而是自行约定:将所需内容传入 props,并使用相应的 prop

    function SplitPane(props) {
      return (
        <div className="SplitPane">
          <div className="SplitPane-left">
            {props.left}
          </div>
          <div className="SplitPane-right">
            {props.right}
          </div>
        </div>
      );
    }

    function App() {
      return (
        <SplitPane left={<Contacts /> right={<Chat />} />
      );
    }

9 性能优化

  • 使用 Chrome Performance 标签分析组件,在 Chrome 中进行如下操作:

    • 临时禁用所有的 Chrome 扩展,尤其是 React 开发者工具。他们会严重干扰度量结果!
    • 确保你是在 React 的开发模式下运行应用。
    • 打开 Chrome 开发者工具的 Performance 标签并按下 Record
    • 对你想分析的行为进行复现。尽量在 20 秒内完成以避免 Chrome 卡住。
    • 停止记录。
    • User Timing 标签下会显示 React 归类好的事件。

你可以查阅 Ben Schwarz的文章 以获取更详尽的指导。

  • shouldComponentUpdate

当一个组件的 propsstate 变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM。当它们不相同时,React 会更新该 DOM

即使 React 只更新改变了的 DOM 节点,重新渲染仍然花费了一些时间。在大部分情况下它并不是问题,不过如果它已经慢到让人注意了,你可以通过覆盖生命周期方法 shouldComponentUpdate 来进行提速。

在大部分情况下,你可以继承 React.PureComponent 以代替手写 shouldComponentUpdate()。它用当前与之前 propsstate 的浅比较覆写了 shouldComponentUpdate() 的实现。

10 Portals

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

ReactDOM.createPortal(child, container)

第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:

    render() {
      // React 挂载了一个新的 div,并且把子元素渲染其中
      return (
        <div>
          {this.props.children}
        </div>
      );
    }

然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:

    render() {
      // React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
      // `domNode` 是一个可以在任何位置的有效 DOM 节点。
      return ReactDOM.createPortal(
        this.props.children,
        domNode
      );
    }

一个 portal 的典型用例是当父组件有 overflow: hiddenz-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:

:对于模态对话框,通过遵循 WAI-ARIA 模态开发实践,来确保每个人都能够运用它。尽管 portal 可以被放置在 DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。由于 portal 仍存在于 React 树, 且与 DOM 树 中的位置无关,那么无论其子节点是否是 portal,像 context 这样的功能特性都是不变的。 这包含事件冒泡。一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先。

11 Profiler

Profiler 测量渲染一个 React 应用多久渲染一次以及渲染一次的代价。 它的目的是识别出应用中渲染较慢的部分。Profiler 能添加在 React 树中的任何地方来测量树中这部分渲染所带来的开销。 它需要两个 prop :一个是 id(string),一个是当组件树中的组件“提交”更新的时候被React调用的回调函数onRender(function)

例如,为了分析 Navigation 组件和它的子代:

    <App>
        <Profiler id="Panel" onRender={callback}>
          <Panel {...props}>
            <Profiler id="Content" onRender={callback}>
              <Content {...props} />
            </Profiler>
            <Profiler id="PreviewPane" onRender={callback}>
              <PreviewPane {...props} />
            </Profiler>
          </Panel>
        </Profiler>
    </App>

:尽管 Profiler 是一个轻量级组件,我们依然应该在需要时才去使用它。对一个应用来说,每添加一些都会给 CPU 和内存带来一些负担。

Profiler 需要一个 onRender 函数作为参数。 React 会在 profile 包含的组件树中任何组件 “提交” 一个更新的时候调用这个函数。 它的参数描述了渲染了什么和花费了多久。

    function onRenderCallback(
      id,
      phase, 
      actualDuration,
      baseDuration,
      startTime,
      commitTime,
      interactions
    ) {
      // 合计或记录渲染时间。。。
    }
  • id: string - 发生提交的 Profiler 树的 id。 如果有多个 profiler,它能用来分辨树的哪一部分发生了“提交”。

  • phase: mount | update - 判断是组件树的第一次装载引起的重渲染,还是由 propsstate 或是 hooks 改变引起的重渲染。

  • actualDuration: number - 本次更新在渲染 Profiler 和它的子代上花费的时间。 这个数值表明使用 memoization 之后能表现得多好。(例如 React.memouseMemoshouldComponentUpdate)。 理想情况下,由于子代只会因特定的 prop 改变而重渲染,因此这个值应该在第一次装载之后显著下降。

  • baseDuration: number - 在 Profiler 树中最近一次每一个组件 render 的持续时间。 这个值估计了最差的渲染时间。(例如当它是第一次加载或者组件树没有使用 memoization)。

  • startTime: number - 本次更新中 React 开始渲染的时间戳。

  • commitTime: number - 本次更新中 React commit 阶段结束的时间戳。 在一次 commit 中这个值在所有的 profiler 之间是共享的,可以将它们按需分组。

  • interactions: Set - 当更新被制定时,“interactions” 的集合会被追踪。(例如当 render 或者 setState 被调用时)。

12 createReactClass()

12.1 声明属性

无论是函数组件还是 class 组件,都拥有 defaultProps 属性:

    class Greeting extends React.Component {
      // ...
    }

    Greeting.defaultProps = {
      name: 'Mary'
    };

如果使用 createReactClass() 方法创建组件,那就需要在组件中定义 getDefaultProps() 函数:

    var Greeting = createReactClass({
      getDefaultProps: function() {
        return {
          name: 'Mary'
        };
      },

      // ...

    });

12.2 声明状态

如果使用 ES6class 关键字创建组件,你可以通过给 this.state 赋值的方式来定义组件的初始 state

    class Counter extends React.Component {
      constructor(props) {
        super(props);
        this.state = {count: props.initialCount};
      }
      // ...
    }

如果使用 createReactClass() 方法创建组件,你需要提供一个单独的 getInitialState 方法,让其返回初始state

    var Counter = createReactClass({
      getInitialState: function() {
        return {count: this.props.initialCount};
      },
      // ...
    });

12.3 绑定this

对于使用 ES6class 关键字创建的 React 组件,组件中的方法遵循与常规 ES6 class 相同的语法规则。这意味着这些方法不会自动绑定 this 到这个组件实例。 你需要在 constructor 中显式地调用 .bind(this)

    class SayHello extends React.Component {
      constructor(props) {
        super(props);
        this.state = {message: 'Hello!'};
        // 这一行很重要!
        this.handleClick = this.handleClick.bind(this);
      }

      handleClick() {
        alert(this.state.message);
      }

      render() {
        // 由于 `this.handleClick` 已经绑定至实例,因此我们才可以用它来处理点击事件
        return (
          <button onClick={this.handleClick}>
            Say hello
          </button>
        );
      }
    }

如果使用 createReactClass() 方法创建组件,组件中的方法会自动绑定至实例,所以不需要像上面那样做:

    var SayHello = createReactClass({
      getInitialState: function() {
        return {message: 'Hello!'};
      },

      handleClick: function() {
        alert(this.state.message);
      },

      render: function() {
        return (
          <button onClick={this.handleClick}>
            Say hello
          </button>
        );
      }
    });

12.4 绑定mixins

    var SetIntervalMixin = {
      componentWillMount: function() {
        this.intervals = [];
      },
      setInterval: function() {
        this.intervals.push(setInterval.apply(null, arguments));
      },
      componentWillUnmount: function() {
        this.intervals.forEach(clearInterval);
      }
    };

    var createReactClass = require('create-react-class');

    var TickTock = createReactClass({
      mixins: [SetIntervalMixin], // 使用 mixin
      getInitialState: function() {
        return {seconds: 0};
      },
      componentDidMount: function() {
        this.setInterval(this.tick, 1000); // 调用 mixin 上的方法
      },
      tick: function() {
        this.setState({seconds: this.state.seconds + 1});
      },
      render: function() {
        return (
          <p>
            React has been running for {this.state.seconds} seconds.
          </p>
        );
      }
    });

    ReactDOM.render(
      <TickTock />,
      document.getElementById('example')
    );

如果组件拥有多个 mixins,且这些 mixins 中定义了相同的生命周期方法(例如,当组件被销毁时,几个 mixins 都想要进行一些清理工作),那么这些生命周期方法都会被调用的。使用mixins 时,mixins 会先按照定义时的顺序执行,最后调用组件上对应的方法。

13 React.createElement()

每个 JSX 元素只是调用 React.createElement(component, props, ...children) 的语法糖。因此,使用 JSX 可以完成的任何事情都可以通过纯 JavaScript 完成。

render() {
    <div>Hello {this.props.toWhat}</div>;
}

相当于

render() {
    React.createElement('div', null, `Hello ${this.props.toWhat}`)
}

14 协调

在某一时间节点调用 Reactrender() 方法,会创建一棵由 React 元素组成的树。在下一次 stateprops 更新时,相同的 render() 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前 UI与最新的树保持同步。

这个算法问题有一些通用的解决方案,即生成将一棵树转换成另一棵树的最小操作数。 然而,即使在最前沿的算法中,该算法的复杂程度为 O(n3 ),其中 n 是树中元素的数量。

如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。于是 React 在以下两个假设的基础之上提出了一套 O(n) 的启发式算法:

  1. 两个不同类型的元素会产生出不同的树;
  2. 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定;

在实践中,我们发现以上假设在几乎所有实用的场景下都成立。

举个例子,当一个元素从 <a> 变成 <img>或从 <Button> 变成 <div> 都会触发一个完整的重建流程。

当拆卸一棵树时,对应的 DOM 节点也会被销毁。组件实例将执行 componentWillUnmount() 方法。当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中。组件实例将执行 componentWillMount() 方法,紧接着 componentDidMount() 方法。所有跟之前的树所关联的 state 也会被销毁。

14.1 比对同一类型的元素

当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。比如:

    <div className="before" title="stuff" />
    <div className="after" title="stuff" /> 

通过比对,React 知道只需要修改 DOM 元素上的 className 属性。

当更新 style 属性时,React 仅更新有所更变的属性。比如:

    <div style={{color: 'red', fontWeight: 'bold'}} />
    <div style={{color: 'green', fontWeight: 'bold'}} />

通过比对,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight。(注:待验证,感觉每次render()时,style对应都是一个新对象,因此按理来说,两个属性都会修改。

在处理完当前节点后,React 继续对子节点进行递归。

14.2 比对同类型的组件元素

当一个组件更新时,组件实例保持不变,这样 state 在跨越不同的渲染时保持一致。React 将更新该组件实例的 props 以跟最新的元素保持一致,并且调用该实例的 componentWillReceiveProps()componentWillUpdate() 方法。

下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归。

14.3 对子节点进行递归

在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation

在子元素列表末尾新增元素时,更变开销比较小。比如:

    <ul>
      <li>first</li>
      <li>second</li>
    </ul>

    <ul>
      <li>first</li>
      <li>second</li>
      <li>third</li>
    </ul>

React 会先匹配两个 <li>first</li> 对应的树,然后匹配第二个元素 <li>second</li> 对应的树,最后插入第三个元素的 <li>third</li> 树。

如果简单实现的话,那么在列表头部插入会很影响性能,那么更变开销会比较大。比如:

    <ul>
      <li>Duke</li>
      <li>Villanova</li>
    </ul>
    <ul>
      <li>Connecticut</li>
      <li>Duke</li>
      <li>Villanova</li>
    </ul>

React 会针对每个子元素 mutate 而不是保持相同的 <li>Duke</li><li>Villanova</li> 子树完成。这种情况下的低效可能会带来性能问题。

14.4 Keys

为了解决以上问题,React 支持 key 属性。当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。以下例子在新增 key 之后使得之前的低效转换变得高效:

    <ul>
      <li key="2015">Duke</li>
      <li key="2016">Villanova</li>
    </ul>
    <ul>
      <li key="2014">Connecticut</li>
      <li key="2015">Duke</li>
      <li key="2016">Villanova</li>
    </ul>

现在 React 知道只有带着 '2014' key 的元素是新元素,带着 '2015' 以及 '2016' key 的元素仅仅移动了。这个 key 不需要全局唯一,但在列表中需要保持唯一。

当基于下标的组件进行重新排序时,组件 state 可能会遇到一些问题。由于组件实例是基于它们的 key 来决定是否更新以及复用,如果 key 是一个下标,那么修改顺序时会修改当前的 key,导致非受控组件的 state(比如输入框)可能相互篡改导致无法预期的变动。

Codepen 有两个例子,分别为 展示使用下标作为 key 时导致的问题,以及不使用下标作为 key 的例子的版本,修复了重新排列,排序,以及在列表头插入的问题 。

15 refs

ref 的值根据节点的类型而有所不同:

  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
  • ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例。(:可以在函数组件中国使用useRef)

15.1 createRef()

    class CustomTextInput extends React.Component {
      constructor(props) {
        super(props);
        // 创建一个 ref 来存储 textInput 的 DOM 元素
        this.textInput = React.createRef();
        this.focusTextInput = this.focusTextInput.bind(this);
      }

      focusTextInput() {
        // 直接用原生 API 使 text 输入框获得焦点
        // 我们通过 "current" 来访问 DOM 节点
        this.textInput.current.focus();
      }

      render() {
        // 告诉 React 我们想把 <input> ref 关联到
        // 构造器里创建的 `textInput` 上
        return (
          <div>
            <input
              type="text"
              ref={this.textInput} />
            <input
              type="button"
              value="Focus the text input"
              onClick={this.focusTextInput}
            />
          </div>
        );
      }
    }

React 会在组件挂载时给 current 属性传入 DOM 元素,并在组件卸载时传入 null 值。ref 会在 componentDidMountcomponentDidUpdate 生命周期钩子触发前更新。

当我们在CustomTextInput组件上使用ref时,<CustomTextInput ref={this.textInput} />CustomTextInput仅在声明为class组件时,ref才有效。 默认情况下,你不能在函数组件上使用 ref 属性,因为它们没有实例:

    function MyFunctionComponent() {
      return <input />;
    }

    class Parent extends React.Component {
      constructor(props) {
        super(props);
        this.textInput = React.createRef();
      }
      render() {
        // This will *not* work!
        return (
          <MyFunctionComponent ref={this.textInput} />
        );
      }
    }

如果要在函数组件中使用 ref,你可以使用 forwardRef(可与 useImperativeHandle 结合使用),或者可以将该组件转化为 class 组件。

15.2 useRef()

不管怎样,你可以在函数组件内部使用 ref 属性,只要它指向一个 DOM 元素或 class 组件:

    function CustomTextInput(props) {
      // 这里必须声明 textInput,这样 ref 才可以引用它
      const textInput = useRef(null);

      function handleClick() {
        textInput.current.focus();
      }

      return (
        <div>
          <input
            type="text"
            ref={textInput} />
          <input
            type="button"
            value="Focus the text input"
            onClick={handleClick}
          />
        </div>
      );
    }

15.3 回调 Refs

不同于传递 createRef() 创建的 ref 属性,你会传递一个函数。这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。

    class CustomTextInput extends React.Component {
      constructor(props) {
        super(props);

        this.textInput = null;

        this.setTextInputRef = element => {
          this.textInput = element;
        };

        this.focusTextInput = () => {
          // 使用原生 DOM API 使 text 输入框获得焦点
          if (this.textInput) this.textInput.focus();
        };
      }

      componentDidMount() 
        this.focusTextInput();
      }

      render() {
        // 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
        // 实例上(比如 this.textInput)
        return (
          <div>
            <input
              type="text"
              ref={this.setTextInputRef}
            />
            <input
              type="button"
              value="Focus the text input"
              onClick={this.focusTextInput}
            />
          </div>
        );
      }
    }

React 将在组件挂载时,会调用 ref 回调函数并传入DOM 元素,当卸载时调用它并传入 null。在 componentDidMountcomponentDidUpdate 触发前,React 会保证 refs 一定是最新的。

如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。

16 render prop

render prop 是一个用于告知组件需要渲染什么内容的函数 prop。这项技术使我们共享行为非常容易。例如:

    class Cat extends React.Component {
      render() {
        const mouse = this.props.mouse;
        return (
          <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
        );
      }
    }

    class Mouse extends React.Component {
      constructor(props) {
        super(props);
        this.handleMouseMove = this.handleMouseMove.bind(this);
        this.state = { x: 0, y: 0 };
      }

      handleMouseMove(event) {
        this.setState({
          x: event.clientX,
          y: event.clientY
        });
      }

      render() {
        return (
          <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
            // render prop 使用的地方
            {this.props.render(this.state)}
          </div>
        );
      }
    }

    class MouseTracker extends React.Component {
      render() {
        return (
          <div>
            <h1>移动鼠标!</h1>
            <Mouse render={mouse => (
              <Cat mouse={mouse} />
            )}/>
          </div>
        );
      }
    }

我们可以提供一个带有函数 prop<Mouse> 组件,它能够动态决定什么需要渲染的,而不是将 <Cat> 硬编码到 <Mouse> 组件里,并有效地改变它的渲染结果。

我们提供了一个 render 方法 让 <Mouse> 能够动态决定什么需要渲染,而不是克隆 <Mouse> 组件然后硬编码来解决特定的用例。

关于 render prop 一个有趣的事情是你可以使用带有 render prop 的常规组件来实现大多数高阶组件 (HOC)。 例如,如果你更喜欢使用 withMouse HOC而不是 <Mouse> 组件,你可以使用带有 render prop 的常规 <Mouse> 轻松创建一个:

    // 如果你出于某种原因真的想要 HOC,那么你可以轻松实现
    // 使用具有 render prop 的普通组件创建一个!
    function withMouse(Component) {
      return class extends React.Component {
        render() {
          return (
            <Mouse render={mouse => (
              <Component {...this.props} mouse={mouse} />
            )}/>
          );
        }
      }
    }

render prop 是因为模式才被称为 render prop ,你不一定要用名为 render 的 prop 来使用这种模式。

Render PropsReact.PureComponent 一起使用时要小心。

如果你在 render 方法里创建函数,那么使用 render prop 会抵消使用 React.PureComponent 带来的优势。因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render prop 将会生成一个新的值(即render对应的函数每次都是新创建的,所以每次render时,都会创建Cat组件)。

以下是解决办法

    class MouseTracker extends React.Component {
      // 定义为实例方法,`this.renderTheCat`始终
      // 当我们在渲染中使用它时,它指的是相同的函数
      renderTheCat(mouse) {
        return <Cat mouse={mouse} />;
      }

      render() {
        return (
          <div>
            <h1>Move the mouse around!</h1>
            <Mouse render={this.renderTheCat} />
          </div>
        );
      }
    }

17 严格模式

StrictMode 是一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。

: 严格模式检查仅在开发模式下运行;它们不会影响生产构建。

StrictMode 目前有助于:

  • 识别不安全的生命周期
  • 关于使用过时字符串 ref API 的警告
  • 关于使用废弃的 findDOMNode 方法的警告
  • 检测意外的副作用
  • 检测过时的 context API

17.1 识别不安全的生命周期

某些过时的生命周期方法在异步 React 应用程序中使用是不安全的。但是,如果你的应用程序使用了第三方库,很难确保它们不使用这些生命周期方法。幸运的是,严格模式可以帮助解决这个问题!

17.2 关于使用过时字符串 ref API 的警告

React 提供了两种方法管理 refs 的方式:已过时的字符串 ref API 的形式及回调函数 API 的形式。尽管字符串 ref API 在两者中使用更方便,但是它有一些缺点,因此官方推荐采用回调的方式。

React 16.3 新增了第三种选择,它提供了使用字符串 ref 的便利性,并且不存在任何缺点(即:createRef()

由于对象 ref 主要是为了替换字符串 ref 而添加的,因此严格模式现在会警告使用字符串 ref

17.3 关于使用废弃的 findDOMNode 方法的警告

React 支持用 findDOMNode 来在给定 class 实例的情况下在树中搜索 DOM节点。通常你不需要这样做,因为你可以将 ref 直接绑定到 DOM 节点。

findDOMNode 也可用于 class 组件,但它违反了抽象原则,它使得父组件需要单独渲染子组件。它会产生重构危险,你不能更改组件的实现细节,因为父组件可能正在访问它的 DOM 节点。findDOMNode 只返回第一个子节点,但是使用 Fragments,组件可以渲染多个 DOM 节点。findDOMNode 是一个只读一次的 API。调用该方法只会返回第一次查询的结果。如果子组件渲染了不同的节点,则无法跟踪此更改。因此,findDOMNode 仅在组件返回单个且不可变的 DOM 节点时才有效。

你可以通过将 ref 传递给自定义组件并使用 ref 转发来将其传递给 DOM 节点。

17.4 检测意外的副作用

从概念上讲,React 分两个阶段工作:

  • 渲染 阶段会确定需要进行哪些更改,比如 DOM。在此阶段,React 调用 render,然后将结果与上次渲染的结果进行比较。
  • 提交 阶段发生在当 React 应用变化时。(对于 React DOM 来说,会发生在 React 插入,更新及删除 DOM 节点的时候。)在此阶段,React 还会调用 componentDidMountcomponentDidUpdate 之类的生命周期方法。

提交阶段通常会很快,但渲染过程可能很慢。因此,即将推出的 concurrent 模式 (默认情况下未启用) 将渲染工作分解为多个部分,对任务进行暂停和恢复操作以避免阻塞浏览器。这意味着 React 可以在提交之前多次调用渲染阶段生命周期的方法,或者在不提交的情况下调用它们(由于出现错误或更高优先级的任务使其中断)。

渲染阶段的生命周期包括以下 class 组件方法:

  1. constructor
  2. componentWillMount (or UNSAFE_componentWillMount)
  3. componentWillReceiveProps (or UNSAFE_componentWillReceiveProps)
  4. componentWillUpdate (or UNSAFE_componentWillUpdate)
  5. getDerivedStateFromProps
  6. shouldComponentUpdate
  7. render
  8. setState 更新函数(第一个参数)

因为上述方法可能会被多次调用,所以不要在它们内部编写副作用相关的代码,这点非常重要。忽略此规则可能会导致各种问题的产生,包括内存泄漏和或出现无效的应用程序状态。不幸的是,这些问题很难被发现,因为它们通常具有非确定性。

严格模式不能自动检测到你的副作用,但它可以帮助你发现它们,使它们更具确定性。通过故意重复调用以下函数来实现的该操作:

  • class 组件的 constructorrender 以及 shouldComponentUpdate 方法
  • class 组件的生命周期方法 getDerivedStateFromProps
  • 函数组件体
  • 状态更新函数 (即 setState 的第一个参数)
  • 函数组件通过使用 useStateuseMemo 或者 useReducer

:这仅适用于开发模式。生产模式下生命周期不会被调用两次。

17.5 检测过时的 context API

过时的 context API 容易出错,将在未来的主要版本中删除。在所有 16.x 版本中它仍然有效,但在严格模式下,将显示以下警告:

image.png

18 PropTypes

PropTypes 提供一系列验证器,可用于确保组件接收到的数据类型是有效的。出于性能方面的考虑,propTypes 仅在开发模式下进行检查。

    import PropTypes from 'prop-types';

    MyComponent.propTypes = {
      // 你可以将属性声明为 JS 原生类型,默认情况下
      optionalArray: PropTypes.array,
      optionalBool: PropTypes.bool,
      optionalFunc: PropTypes.func,
      optionalNumber: PropTypes.number,
      optionalObject: PropTypes.object,
      optionalString: PropTypes.string,
      optionalSymbol: PropTypes.symbol,

      // 任何可被渲染的元素(包括数字、字符串、元素或数组)
      // (或 Fragment) 也包含这些类型。
      optionalNode: PropTypes.node,

      // 一个 React 元素。
      optionalElement: PropTypes.element,

      // 一个 React 元素类型(即,MyComponent)。
      optionalElementType: PropTypes.elementType,

      // 你也可以声明 prop 为类的实例,这里使用
      // JS 的 instanceof 操作符。
      optionalMessage: PropTypes.instanceOf(Message),

      // 你可以让你的 prop 只能是特定的值,指定它为
      // 枚举类型。
      optionalEnum: PropTypes.oneOf(['News', 'Photos']),

      // 一个对象可以是几种类型中的任意一个类型
      optionalUnion: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.number,
        PropTypes.instanceOf(Message)
      ]),

      // 可以指定一个数组由某一类型的元素组成
      optionalArrayOf: PropTypes.arrayOf(PropTypes.number),

      // 可以指定一个对象由某一类型的值组成
      optionalObjectOf: PropTypes.objectOf(PropTypes.number),

      // 可以指定一个对象由特定的类型值组成
      optionalObjectWithShape: PropTypes.shape({
        color: PropTypes.string,
        fontSize: PropTypes.number
      }),

      // An object with warnings on extra properties
      optionalObjectWithStrictShape: PropTypes.exact({
        name: PropTypes.string,
        quantity: PropTypes.number
      }),   

      // 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保
      // 这个 prop 没有被提供时,会打印警告信息。
      requiredFunc: PropTypes.func.isRequired,

      // 任意类型的数据
      requiredAny: PropTypes.any.isRequired,

      // 你可以指定一个自定义验证器。它在验证失败时应返回一个 Error 对象。
      // 请不要使用 `console.warn` 或抛出异常,因为这在 `onOfType` 中不会起作用。
      customProp: function(props, propName, componentName) {
        if (!/matchme/.test(props[propName])) {
          return new Error(
            'Invalid prop `' + propName + '` supplied to' +
            ' `' + componentName + '`. Validation failed.'
          );
        }
      },

      // 你也可以提供一个自定义的 `arrayOf` 或 `objectOf` 验证器。
      // 在验证失败时返回一个 Error 对象。
      // 验证器将验证数组或对象中的每个值。验证器的前两个参数
      // 第一个是数组或对象本身
      // 第二个是他们当前的键。
      customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
        if (!/matchme/.test(propValue[key])) {
          return new Error(
            'Invalid prop `' + propFullName + '` supplied to' +
            ' `' + componentName + '`. Validation failed.'
          );
        }
      })
    };

18.1 默认Prop

您可以通过配置特定的 defaultProps 属性来定义 props 的默认值:

    class Greeting extends React.Component {
      render() {
        return (
          <h1>Hello, {this.props.name}</h1>
        );
      }
    }

    // 指定 props 的默认值:
    Greeting.defaultProps = {
      name: 'Stranger'
    };

如果你正在使用像 transform-class-propertiesBabel 转换工具,你也可以在 React 组件类中声明 defaultProps 作为静态属性。此语法提案还没有最终确定,需要进行编译后才能在浏览器中运行。

    class Greeting extends React.Component {
      static defaultProps = {
        name: 'stranger'
      }

      render() {
        return (
          <div>Hello, {this.props.name}</div>
        )
      }
    }

defaultProps 用于确保 this.props.name 在父组件没有指定其值时,有一个默认值。propTypes 类型检查发生在 defaultProps 赋值后,所以类型检查也适用于 defaultProps

19 Web Components

ReactWeb Components 为了解决不同的问题而生。Web Components 为可复用组件提供了强大的封装,而 React 则提供了声明式的解决方案,使 DOM 与数据保持同步。两者旨在互补。作为开发人员,可以自由选择在 Web Components 中使用 React,或者在 React 中使用 Web Components,或者两者共存。

React 中使用 Web Components

    class HelloMessage extends React.Component {
      render() {
        return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
      }
    }
    

常见的误区是在 Web Components 中使用的是 class 而非 className

    function BrickFlipbox() {
      return (
        <brick-flipbox class="demo">
          123
        </brick-flipbox>
      );
    }

Web Components 中使用 React

    class XSearch extends HTMLElement {
      connectedCallback() {
        const mountPoint = document.createElement('span');
        this.attachShadow({ mode: 'open' }).appendChild(mountPoint);

        const name = this.getAttribute('name');
        const url = 'https://www.google.com/search?q=' + encodeURIComponent(name);
        ReactDOM.render(<a href={url}>{name}</a>, mountPoint);
      }
    }
    customElements.define('x-search', XSearch);

:如果使用 Babel 来转换 class,此代码将不会起作用。请查阅该 issue 了解相关讨论。 在加载 Web Components 前请引入 custom-elements-es5-adapter 来解决该 issue

20 reactApi

20.1 React.PureComponent

React.PureComponentReact.Component 很相似。两者的区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了该函数。

如果赋予 React 组件相同的 propsstaterender() 函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent 可提高性能。

React.PureComponent 中的 shouldComponentUpdate() 仅作对象的浅层比较。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。仅在你的 propsstate 较为简单时,才使用 React.PureComponent,或者在深层数据结构发生变化时调用 forceUpdate() 来确保组件被正确地更新。你也可以考虑使用 immutable 对象加速嵌套数据的比较。 此外,React.PureComponent 中的 shouldComponentUpdate() 将跳过所有子组件树的 prop 更新。因此,请确保所有子组件也都是“纯”的组件。

20.2 React.memo

React.memo 为高阶组件。它与 React.PureComponent 非常相似,但只适用于函数组件,而不适用 class 组件。

如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseContextHook,当 context 发生变化时,它仍会重新渲染。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

    function MyComponent(props) {
      /* 使用 props 渲染 */
    }
    function areEqual(prevProps, nextProps) {
      /*
      如果把 nextProps 传入 render 方法的返回结果与
      将 prevProps 传入 render 方法的返回结果一致则返回 true,
      否则返回 false
      */
    }
    export default React.memo(MyComponent, areEqual);

此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug

20.3 React.createElement

    React.createElement(
      type,
      [props],
      [...children]
    )

创建并返回指定类型的新 React 元素。其中的类型参数type既可以是标签名字符串(如 'div''span'),也可以是 React 组件 类型 (class 组件或函数组件),或是 React fragment 类型。

20.4 React.cloneElement

    React.cloneElement(
      element,
      [props],
      [...children]
    )

element 元素为样板克隆并返回新的 React 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有的子元素,而来自原始元素的 keyref 将被保留。

React.cloneElement() 几乎等同于:

    <element.type {...element.props} {...props}>{children}</element.type>

但是,这也保留了组件的 ref。这意味着当通过 ref 获取子节点时,你将不会意外地从你祖先节点上窃取它。相同的 ref 将添加到克隆后的新元素中。

20.5 React.isValidElement

    React.isValidElement(object)

验证对象是否为 React 元素,返回值为 truefalse

20.6 React.Children

React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。

  • React.Children.map

        React.Children.map(children, function[(thisArg)])
    

children 里的每个直接子节点上调用一个函数,并将 this 设置为 thisArg。如果 children 是一个数组,它将被遍历并为数组中的每个子节点调用该函数。如果子节点为 null 或是 undefined,则此方法将返回 null 或是 undefined,而不会返回数组。

:如果 children 是一个 Fragment 对象,它将被视为单一子节点的情况处理,而不会被遍历。

  • React.Children.forEach
    React.Children.forEach(children, function[(thisArg)])

React.Children.map() 类似,但它不会返回一个数组。

  • React.Children.count
    React.Children.count(children)

返回 children 中的组件总数量,等同于通过 mapforEach 调用回调函数的次数。

  • React.Children.only
    React.Children.only(children)

验证 children 是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。

React.Children.only() 不接受 React.Children.map() 的返回值,因为它是一个数组而并不是 React 元素。

  • React.Children.toArray
    React.Children.toArray(children)

children 这个复杂的数据结构以数组的方式扁平展开并返回,并为每个子节点分配一个 key。当你想要在渲染函数中操作子节点的集合时,它会非常实用,特别是当你想要在向下传递 this.props.children 之前对内容重新排序或获取子集时。

React.Children.toArray() 在拉平展开子节点列表时,更改 key值以保留嵌套数组的语义。也就是说,toArray 会为返回数组中的每个 key 添加前缀,以使得每个元素 key 的范围都限定在此函数入参数组的对象内。

20.7 React.Fragment

React.Fragment 组件能够在不额外创建 DOM 元素的情况下,让 render() 方法中返回多个元素。

    render() {
      return (
        <React.Fragment>
          Some text.
          <h2>A heading</h2>
        </React.Fragment>
      );
    }

Fragment标签可以传递属性,如果不需要传递属性可以使用简写形式<></>

20.8 React.createRef

参考前面

20.9 React.forwardRef

参考前面

20.10 React.lazy

参考前面

20.11 React.Suspense

参考前面

React.Suspense 可以指定加载指示器(loading indicator),以防其组件树中的某些子组件尚未具备渲染条件。目前,懒加载组件是 <React.Suspense> 支持的唯一用例。

21 React.Componentapi

class 组件目前提供了更多的功能,这些功能将在此章节中详细介绍。如需定义 class 组件,需要继承 React.Component, 并且有个必须定义的 render() 函数

21.1 生命周期

  • 挂载,当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

    • constructor()
    • static getDerivedStateFromProps()
    • render()
    • componentDidMount() : 下述生命周期方法即将过时,在新代码中应该避免使用它们:UNSAFE_componentWillMount()
  • 更新,当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

    • static getDerivedStateFromProps()
    • shouldComponentUpdate()
    • render()
    • getSnapshotBeforeUpdate()
    • componentDidUpdate()

: 下述生命周期方法即将过时,在新代码中应该避免使用它们:UNSAFE_componentWillUpdate(), UNSAFE_componentWillReceiveProps()

  • 卸载,当组件从 DOM 中移除时会调用如下方法:

    • componentWillUnmount()
  • 错误处理, 当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

    • static getDerivedStateFromError()
    • componentDidCatch()
  • 实例属性

    • props
    • state
  • class属性

    • defaultProps
    • displayName
  • 其他 APIs, 组件还提供了一些额外的 API

    • setState()
    • forceUpdate()

21.2 render()

render() 方法是 class 组件中唯一必须实现的方法。

render 被调用时,它会检查 this.propsthis.state 的变化并返回以下类型之一:

  • React 元素。通常通过 JSX 创建。例如,<div /> 会被 React 渲染为 DOM 节点,<MyComponent /> 会被 React 渲染为自定义组件,无论是 <div /> 还是 <MyComponent /> 均为 React 元素。

  • 数组或 fragments。 使得 render 方法可以返回多个元素。欲了解更多详细信息,请参阅 fragments 文档。

  • Portals。可以渲染子节点到不同的 DOM 子树中。欲了解更多详细信息,请参阅有关 portals 的文档

  • 字符串或数值类型。它们在 DOM 中会被渲染为文本节点

  • 布尔类型或 null。什么都不渲染。(主要用于支持返回 test && <Child />的模式,其中 test 为布尔类型。)

render() 函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。

21.3 constructor()

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

React 组件挂载之前,会调用它的构造函数。在为 React.Component 子类实现构造函数时,应在其他语句之前前调用 super(props)。否则,this.props 在构造函数中可能会出现未定义的 bug

只能在构造函数中直接为 this.state 赋值。如需在其他方法中赋值,你应使用 this.setState() 替代。

要避免在构造函数中引入任何副作用或订阅。如遇到此场景,请将对应的操作放置在 componentDidMount 中。

22.4 componentDidMount()

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。

这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅。

你可以在 componentDidMount() 里直接调用 setState()。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 render() 两次调用的情况下,用户也不会看到中间状态。请谨慎使用该模式,因为它会导致性能问题。

22.5 componentDidUpdate()

    componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行此方法。

当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。

你也可以在 componentDidUpdate() 中直接调用 setState(),但请注意它必须被包裹在一个条件语句里,正如上述的例子那样进行处理,否则会导致死循环。它还会导致额外的重新渲染,虽然用户不可见,但会影响组件性能。

如果组件实现了 getSnapshotBeforeUpdate() 生命周期(不常用),则它的返回值将作为 componentDidUpdate() 的第三个参数 “snapshot” 参数传递。否则此参数将为 undefined

: 如果 shouldComponentUpdate() 返回值为 false,则不会调用 componentDidUpdate()

22.6 componentWillUnmount()

componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。

componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

22.7 shouldComponentUpdate()

    shouldComponentUpdate(nextProps, nextState)

根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 stateprops 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染。大部分情况下,你应该遵循默认行为。

propsstate 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。首次渲染或使用 forceUpdate() 时不会调用该方法。

此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。你应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()PureComponent 会对 propsstate 进行浅层比较,并减少了跳过必要更新的可能性。

如果你一定要手动编写此函数,可以将 this.propsnextProps 以及 this.statenextState 进行比较,并返回 false 以告知 React 可以跳过更新。请注意,返回 false 并不会阻止子组件在 state 更改时重新渲染。

我们不建议在 shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会损害性能。

目前,如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()render()componentDidUpdate()。后续版本,React 可能会将 shouldComponentUpdate 视为提示而不是严格的指令,并且,当返回 false 时,仍可能导致组件重新渲染。

22.8 static getDerivedStateFromProps()

    static getDerivedStateFromProps(props, state)

getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

此方法无权访问组件实例。如果你需要,可以通过提取组件 props 的纯函数及 class 之外的状态,在getDerivedStateFromProps()和其他 class 方法之间重用代码。

请注意,不管原因是什么,都会在每次渲染前触发此方法。这与 UNSAFE_componentWillReceiveProps形成对比,后者仅在父组件重新渲染时触发,而不是在内部调用 setState 时。

22.9 getSnapshotBeforeUpdate()

    getSnapshotBeforeUpdate(prevProps, prevState)

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()

此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。

应返回 snapshot 的值(或 null)。

    class ScrollingList extends React.Component {
      constructor(props) {
        super(props);
        this.listRef = React.createRef();
      }

      getSnapshotBeforeUpdate(prevProps, prevState) {
        // 我们是否在 list 中添加新的 items ?
        // 捕获滚动位置以便我们稍后调整滚动位置。
        if (prevProps.list.length < this.props.list.length) {
          const list = this.listRef.current;
          return list.scrollHeight - list.scrollTop;
        }
        return null;
      }

      componentDidUpdate(prevProps, prevState, snapshot) {
        // 如果 snapshot 有值,说明我们刚刚添加了新的 items,
        // 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
        //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
        if (snapshot !== null) {
          const list = this.listRef.current;
          list.scrollTop = list.scrollHeight - snapshot;
        }
      }

      render() {
        return (
          <div ref={this.listRef}>{/* ...contents... */}</div>
        );
      }
    }

在上述示例中,重点是从 getSnapshotBeforeUpdate 读取 scrollHeight 属性,因为 “render” 阶段生命周期(如 render)和 “commit” 阶段生命周期(如 getSnapshotBeforeUpdatecomponentDidUpdate)之间可能存在延迟。

22.10 static getDerivedStateFromError()

    static getDerivedStateFromError(error)

此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state

    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }

      static getDerivedStateFromError(error) {
        // 更新 state 使下一次渲染可以显降级 UI
        return { hasError: true };
      }

      render() {
        if (this.state.hasError) {
          // 你可以渲染任何自定义的降级  UI
          return <h1>Something went wrong.</h1>;
        }

        return this.props.children; 
      }
    }

: getDerivedStateFromError() 会在渲染阶段调用,因此不允许出现副作用。 如遇此类情况,请改用 componentDidCatch()

22.11 componentDidCatch()

    componentDidCatch(error, info)

此生命周期在后代组件抛出错误后被调用。 它接收两个参数:

  • error —— 抛出的错误。
  • info —— 带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息。 componentDidCatch() 会在“提交”阶段被调用,因此允许执行副作用。 它应该用于记录错误之类的情况:

22.12 setState()

    setState(updater, [callback])

setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

setState() 并不总是立即更新组件。它会批量推迟更新。这使得在调用 setState() 后立即读取 this.state 成为了隐患。为了消除隐患,请使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。如需基于之前的 state 来设置当前的 state,请阅读下述关于参数 updater 的内容。

setState() 的第一个参数除了接受对象类型,还可以接受函数:

    this.setState((state, props) => {
      return {counter: state.counter + props.step};
    });

函数中接收的 stateprops 都保证为最新。updater 的返回值会与 state 进行浅合并。

22.13 forceUpdate()

    component.forceUpdate(callback)

默认情况下,当组件的 stateprops 发生变化时,组件将重新渲染。如果 render() 方法依赖于其他数据,则可以调用 forceUpdate() 强制让组件重新渲染。

调用 forceUpdate() 将致使组件调用 render() 方法,此操作会跳过该组件的 shouldComponentUpdate()。但其子组件会触发正常的生命周期方法,包括 shouldComponentUpdate() 方法。如果标记发生变化,React 仍将只更新 DOM

通常你应该避免使用 forceUpdate(),尽量在 render() 中使用 this.propsthis.state

22.14 defaultProps

了解更多

22.14 displayName

displayName 字符串多用于调试消息。通常,你不需要设置它,因为它可以根据函数组件或 class 组件的名称推断出来。如果调试时需要显示不同的名称或创建高阶组件, 则需要手动设置。

23 react-dom

23.1 render()

    ReactDOM.render(element, container[, callback])

在提供的 container 里渲染一个 React 元素,并返回对该组件的引用(或者针对无状态组件返回 null)。

如果 React 元素之前已经在 container 里渲染过,这将会对其执行更新操作,并仅会在必要时改变 DOM 以映射最新的 React 元素。

如果提供了可选的回调函数,该回调将在组件被渲染或更新之后被执行。

ReactDOM.render()不会修改容器节点(只会修改容器的子节点)。可以在不覆盖现有子节点的情况下,将组件插入已有的 DOM 节点中。

使用 ReactDOM.render() 对服务端渲染容器进行 hydrate 操作的方式已经被废弃,并且会在 React 17 被移除。作为替代,请使用 hydrate()

23.2 hydrate()

    ReactDOM.hydrate(element, container[, callback])

render() 相同,但它用于在 ReactDOMServer 渲染的容器中对 HTML 的内容进行 hydrate 操作。React 会尝试在已有标记上绑定事件监听器。

render() 相同,但它用于在 ReactDOMServer 渲染的容器中对 HTML 的内容进行 hydrate 操作。React 会尝试在已有标记上绑定事件监听器。

React 希望服务端与客户端渲染的内容完全一致。React 可以弥补文本内容的差异,但是你需要将不匹配的地方作为bug 进行修复。在开发者模式下,React 会对 hydration 操作过程中的不匹配进行警告。但并不能保证在不匹配的情况下,修补属性的差异。由于性能的关系,这一点非常重要,因为大多是应用中不匹配的情况很少见,并且验证所有标记的成本非常昂贵。

如果单个元素的属性或者文本内容,在服务端和客户端之间有无法避免差异(比如:时间戳),则可以为元素添加 suppressHydrationWarning={true} 来消除警告。这种方式只在一级深度上有效,应只作为一种应急方案(escape hatch)。请不要过度使用!除非它是文本内容,否则 React 仍不会尝试修补差异,因此在未来的更新之前,仍会保持不一致。

如果你执意要在服务端与客户端渲染不同内容,你可以采用双重(two-pass)渲染。在客户端渲染不同内容的组件可以读取类似于 this.state.isClientstate 变量,你可以在 componentDidMount() 里将它设置为 true。这种方式在初始渲染过程中会与服务端渲染相同的内容,从而避免不匹配的情况出现,但在 hydration 操作之后,会同步进行额外的渲染操作。注意,因为进行了两次渲染,这种方式会使得组件渲染变慢,请小心使用。

记得保证弱网环境下的用户体验。JavaScript 代码的加载要比最初的 HTML 渲染晚的多。因此如果你只在客户端渲染不同的内容,其转换可能会不稳定。但是,如果执行顺利,那么在服务端负责渲染的 shell 会对渲染提供帮助,并且只显示客户端上额外的小组件。欲了解如何在不出现标记不匹配的情况下执行此操作,请参考上一段的解释。

23.3 unmountComponentAtNode()

    ReactDOM.unmountComponentAtNode(container)

DOM 中卸载组件,会将其事件处理器(event handlers)和 state 一并清除。如果指定容器上没有对应已挂载的组件,这个函数什么也不会做。如果组件被移除将会返回 true,如果没有组件可被移除将会返回 false

23.4 findDOMNode()

在大多数情况下,不推荐使用该方法,因为它会破坏组件的抽象结构。严格模式下该方法已弃用。

    ReactDOM.findDOMNode(component)

如果组件已经被挂载到 DOM 上,此方法会返回浏览器中相应的原生 DOM 元素。

当组件渲染的内容为 nullfalse 时,findDOMNode 也会返回 null。当组件渲染的是字符串时,findDOMNode 返回的是字符串对应的 DOM 节点。从 React 16 开始,组件可能会返回有多个子节点的 fragment,在这种情况下,findDOMNode 会返回第一个非空子节点对应的 DOM 节点。

findDOMNode 只在已挂载的组件上可用(即,已经放置在 DOM 中的组件)。如果你尝试调用未挂载的组件(例如在一个还未创建的组件上调用 render() 中的 findDOMNode())将会引发异常。findDOMNode 不能用于函数组件。

23.5 createPortal()

    ReactDOM.createPortal(child, container)

创建 portalPortal 将提供一种将子节点渲染到 DOM 节点中的方式,该节点存在于 DOM 组件的层次结构之外。了解更多

24 ReactDOMServer

ReactDOMServer 对象允许你将组件渲染成静态标记。通常,它被使用在 Node 服务端上:

    // ES modules
    import ReactDOMServer from 'react-dom/server';
    // CommonJS
    var ReactDOMServer = require('react-dom/server');

24.1 renderToString()

    ReactDOMServer.renderToString(element)

React 元素渲染为初始 HTMLReact 将返回一个 HTML 字符串。你可以使用此方法在服务端生成 HTML,并在首次请求时将标记下发,以加快页面加载速度,并允许搜索引擎爬取你的页面以达到 SEO 优化的目的。

如果你在已有服务端渲染标记的节点上调用 ReactDOM.hydrate() 方法,React 将会保留该节点且只进行事件处理绑定,从而让你有一个非常高性能的首次加载体验。

24.2 renderToStaticMarkup()

    ReactDOMServer.renderToStaticMarkup(element)

此方法与 renderToString 相似,但此方法不会在 React 内部创建的额外 DOM 属性,例如 data-reactroot。如果你希望把 React 当作静态页面生成器来使用,此方法会非常有用,因为去除额外的属性可以节省一些字节。

如果你计划在前端使用 React 以使得标记可交互,请不要使用此方法。你可以在服务端上使用 renderToString 或在前端上使用 ReactDOM.hydrate() 来代替此方法。

24.3 renderToNodeStream()

    ReactDOMServer.renderToNodeStream(element)

将一个 React 元素渲染成其初始 HTML。返回一个可输出 HTML 字符串的可读流。通过可读流输出的 HTML 完全等同于 ReactDOMServer.renderToString 返回的 HTML。你可以使用本方法在服务器上生成 HTML,并在初始请求时将标记下发,以加快页面加载速度,并允许搜索引擎抓取你的页面以达到 SEO 优化的目的。

如果你在已有服务端渲染标记的节点上调用 ReactDOM.hydrate() 方法,React 将会保留该节点且只进行事件处理绑定,从而让你有一个非常高性能的首次加载体验。

:这个 API 仅允许在服务端使用。不允许在浏览器使用。 通过本方法返回的流会返回一个由 utf-8 编码的字节流。如果你需要另一种编码的流,请查看像 iconv-lite 这样的项目,它为转换文本提供了转换流。

24.4 renderToStaticNodeStream()

    ReactDOMServer.renderToStaticNodeStream(element)

此方法与 renderToNodeStream 相似,但此方法不会在 React 内部创建的额外 DOM 属性,例如 data-reactroot。如果你希望把 React 当作静态页面生成器来使用,此方法会非常有用,因为去除额外的属性可以节省一些字节。

通过可读流输出的 HTML,完全等同于 ReactDOMServer.renderToStaticMarkup 返回的 HTML

如果你计划在前端使用 React 以使得标记可交互,请不要使用此方法。你可以在服务端上使用 renderToNodeStream 或在前端上使用 ReactDOM.hydrate() 来代替此方法。

:这个 API 仅允许在服务端使用。不允许在浏览器使用。 通过本方法返回的流会返回一个由 utf-8 编码的字节流。如果你需要另一种编码的流,请查看像 iconv-lite 这样的项目,它为转换文本提供了转换流。

25 Dom元素

React 中,所有的 DOM 特性和属性(包括事件处理)都应该是小驼峰命名的方式。例外的情况是 aria-* 以及 data-* 属性,一律使用小写字母命名。比如, 你依然可以用 aria-label 作为 aria-label

25.1 checked

<input> 组件的 type 类型为 checkboxradio 时,组件支持 checked 属性。

25.2 className

className 属性用于指定 CSSclass,此特性适用于所有常规 DOM 节点和 SVG 元素。如果你在 React 中使用 Web Components(这是一种不常见的使用方式),请使用 class 属性代替。

25.3dangerouslySetInnerHTML

dangerouslySetInnerHTMLReact 为浏览器 DOM 提供 innerHTML 的替换方案。

    function markup() {
      return {__html: 'First &middot; Second'};
    }

    function MyComponent() {
      return <div dangerouslySetInnerHTML={markup()} />;
    }

25.4 htmlFor

由于 forJavaScript 中是保留字,所以 React 元素中使用了 htmlFor 来代替。

25.5 onChange

onChange 事件与预期行为一致:每当表单字段变化时,该事件都会被触发。我们故意没有使用浏览器已有的默认行为,是因为 onChange 在浏览器中的行为和名称不对应,并且 React 依靠了该事件实时处理用户输入。

25.6 style

通常不推荐将 style 属性作为设置样式的主要方式。在多数情况下,应使用 className 来引用外部 CSS 样式表定义的 class

style 接受一个采用小驼峰命名属性的 JavaScript 对象,而不是 CSS 字符串。样式不会自动补齐前缀。如需支持旧版浏览器,请手动补充对应的样式属性。

    const divStyle = {
      WebkitTransition: 'all', // note the capital 'W' here
      msTransition: 'all' // 'ms' is the only lowercase vendor prefix
    };

25.7 suppressContentEditableWarning

通常,当拥有子节点的元素被标记为 contentEditable 时,React 会发出一个警告,因为这不会生效。该属性将禁止此警告。尽量不要使用该属性,除非你要构建一个类似 Draft.js 的手动管理 contentEditable 属性的库。

25.8 suppressHydrationWarning

如果你使用 React 服务端渲染,通常会在当服务端与客户端渲染不同的内容时发出警告。但是,在一些极少数的情况下,很难甚至于不可能保证内容的一致性。例如,在服务端和客户端上,时间戳通常是不同的。

如果设置 suppressHydrationWarningtrueReact 将不会警告你属性与元素内容不一致。它只会对元素一级深度有效,并且打算作为应急方案。因此不要过度使用它。你可以在 ReactDOM.hydrate() 文档 中了解更多关于 hydration 的信息。

26 合成事件SyntheticEvent

SyntheticEvent 实例将被传递给你的事件处理函数,它是浏览器的原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation()preventDefault()

如果因为某些原因,当你需要使用浏览器的底层事件时,只需要使用 nativeEvent 属性来获取即可。每个 SyntheticEvent 对象都包含以下属性:

    boolean bubbles
    boolean cancelable
    DOMEventTarget currentTarget
    boolean defaultPrevented
    number eventPhase
    boolean isTrusted
    DOMEvent nativeEvent
    void preventDefault()
    boolean isDefaultPrevented()
    void stopPropagation()
    boolean isPropagationStopped()
    void persist()
    DOMEventTarget target
    number timeStamp
    string type

以下的事件处理函数在冒泡阶段被触发。如需注册捕获阶段的事件处理函数,则应为事件名添加 Capture。例如,处理捕获阶段的点击事件请使用 onClickCapture,而不是 onClick

Clipboard Events, Composition Events, Keyboard Events, Focus Events, Form Events, Generic Events, Mouse Events, Pointer Events, Selection Events, Touch Events, UI Events, Wheel Events, Media Events, Image Events, Animation Events, Transition Events, Other Events

27 JavaScript 环境要求

React 16 依赖集合类型 MapSet 。如果你要支持无法原生提供这些能力(例如 IE < 11)或实现不规范(例如 IE 11)的旧浏览器与设备,考虑在你的应用库中包含一个全局的 polyfill ,例如 core-js

: 由于字数限制,react hooks会在另外一篇文章更新,敬请期待。