React

React 官网笔记 更新于:2020-12-15

2020-12-15  本文已影响0人  HeroMeikong

React 官网学习笔记

  1. 所有 React 组件都必须像纯函数一样保护它们的 props 不被更改

  2. 在 React 中另一个不同点是你不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault

    function ActionLink() {
      function handleClick(e) {
        e.preventDefault();
        console.log('The link was clicked.');
      }
    
      return (
        <a href="#" onClick={handleClick}>
          Click me
        </a>
      );
    }
    
  3. 当你的函数需要传旨的时候,你应该绑定 this

    class Toggle extends React.Component {
      constructor(props) {
        super(props);
        this.state = {isToggleOn: true};
    
        // 为了在回调中使用 `this`,这个绑定是必不可少的
        this.handleClick = this.handleClick.bind(this);
      }
    
      handleClick() {
        this.setState(state => ({
          isToggleOn: !state.isToggleOn
        }));
      }
    
      render() {
        return (
          <button onClick={this.handleClick}>
            {this.state.isToggleOn ? 'ON' : 'OFF'}
          </button>
        );
      }
    }
    

    你必须谨慎对待 JSX 回调函数中的 this,在 JavaScript 中,class 的方法默认不会绑定 this。如果你忘记绑定 this.handleClick 并把它传入了 onClick,当你调用这个函数的时候 this 的值为 undefined。通常情况下,如果你没有在方法后面添加 (),例如 onClick={this.handleClick},你应该为这个方法绑定 this

    为什么绑定很必要的?

    obj.method();
    
    var method = obj.method;
    method();
    //两个不一样
    
    例子:
    const module = {
      x: 42,
      getX: function() {
        return this.x;
      }
    };
    
    const unboundGetX = module.getX;
    console.log(unboundGetX());// undefined
    
    const boundGetX = unboundGetX.bind(module);
    console.log(boundGetX()); // 42let
    // this 的指定不对
    bind方法确保了第二种写法与第一种写法相同
    

    不使用 bind 方法一:public class fields 语法

    class LoggingButton extends React.Component {
      // 此语法确保 `handleClick` 内的 `this` 已被绑定。
      // 注意: 这是 *实验性* 语法。
      handleClick = () => {
        console.log('this is:', this);
      }
    
      render() {
        return (
          <button onClick={this.handleClick}>
            Click me
          </button>
        );
      }
    }
    

    不使用 bind 方法二:箭头函数

    class LoggingButton extends React.Component {
      handleClick() {
        console.log('this is:', this);
      }
    
      render() {
        // 此语法确保 `handleClick` 内的 `this` 已被绑定。
        return (
          <button onClick={() => this.handleClick()}>
            Click me
          </button>
        );
      }
    }
    
  4. 向事件处理程序传递参数

    # 箭头函数,e 在指定位置传值
    <button onClick={(e) => this.deleteRow('1', e, '2')}>Delete Row</button>
    # Function.prototype.bind,注意第一个值this|null 这里是不会传递的,e 在末尾传值
    <button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
    
  5. 简单条件渲染
    注意:使用函数直接传值,使用组件则通过对象(props)传值

    function Greeting(props) {
      const isLoggedIn = props.isLoggedIn;
      if (isLoggedIn) {
        return <UserGreeting />;
      }
      return <GuestGreeting />;
    }
    
    function Greeting1(isLoggedIn) {
      if (isLoggedIn) {
        return <UserGreeting />;
      }
      return <GuestGreeting />;
    }
    
    ReactDOM.render(
      // this.Greeting1(false),
      <Greeting isLoggedIn={false} />,
      document.getElementById('root')
    );
    
  6. 当使用 function() {} + bind() 结合时,推荐写法:

    class LoginControl extends React.Component {
      constructor(props) {
        super(props);
          this.handleLoginClick = this.handleLoginClick.bind(this);
      }
    
      handleLoginClick() {
        this.setState({isLoggedIn: true});
      }
    
      render() {
        // 不推荐写在这里 this.handleLoginClick = this.handleLoginClick.bind(this);
        // 注意:在 render 方法中使用 Function.prototype.bind 会在每次组件渲染时创建一个新的函数,可能会影响性能
        return (
          <div>
            <LogoutButton onClick={this.handleLogoutClick} />
          </div>
        );
      }
    }
    
  7. 阻止组件渲染可以这样:

    function WarningBanner(props) {
      if (!props.warn) {
        return null;
      }
    
      return (
        <div className="warning">
          Warning!
        </div>
      );
    }
    
  8. 如果列表项目的顺序可能会变化,我们不建议使用索引来用作 key 值,因为这样做会导致性能变差,还可能引起组件状态的问题。如果你选择不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值。

  9. map() 方法中的元素需要设置 key 属性:

    const listItems = numbers.map((number) =>
      // 正确!key 应该在数组的上下文中被指定
      <ListItem key={number.toString()} value={number} />
    );
    
  10. key 会传递信息给 React ,但不会传递给你的组件。组件可以读出 props.id,但是不能读出 props.key

  11. 表单元素:<input><textarea><select>

  12. 受控组件:表单数据是由 React 组件来管理的。使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。对于受控组件,输入的值始终由 React 的 state 驱动。

  13. 非受控组件:表单数据将交由 DOM 节点来处理。

  14. 在 React 中,<input><textarea><select> 的值都由 value 控制,如果 <select> 想要选中多个,则传入数组即可

  15. 处理多个输入:当需要处理多个 input 元素时,我们可以给每个元素添加 name 属性,并让处理函数根据 event.target.name 的值选择要执行的操作。

    class Reservation extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          isGoing: true,
          numberOfGuests: 2
        };
        ...
      }
    
      handleInputChange(event) {
        ...
        const name = target.name;
        this.setState({
          [name]: value
        });
      }
    
      render() {
        return (
          <form>
            <input name="isGoing" ... />
            <input name="numberOfGuests" ... />
          </form>
        );
      }
    }
    
  16. 受控输入空值:在受控组件上指定 value 的 prop 会阻止用户更改输入。如果你指定了 value,但输入仍可编辑,则可能是你意外地将value 设置为 undefinednull

  17. 状态提升:将多个组件中需要共享的 state 向上移动到它们的最近共同父组件中,便可实现共享 state。

  18. 在 React 中推荐使用组合而非继承来实现代码重用:props.children

    function FancyBorder(props) {
      return (
        <div>
          {props.children}
        </div>
      );
    }
    
    function WelcomeDialog() {
      return (
        <FancyBorder color="blue">
          <h1 className="Dialog-title">
            Welcome
          </h1>
          <p className="Dialog-message">
            Thank you for visiting our spacecraft!
          </p>
        </FancyBorder>
      );
    }
    
  19. 如果需要多个“洞“|“槽”(slot),不建议使用 props.children 而是自行约定,例如:

    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 /> }
        />
      );
    }
    

    注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。

  20. WAI-ARIA(网络无障碍倡议 - 无障碍互联网应用(Web Accessibility Initiative - Accessible Rich Internet Applications))
    注意:JSX 支持所有 aria-* HTML 属性。虽然大多数 React 的 DOM 变量和属性命名都使用驼峰命名(camelCased),但 aria-* 应该像其在 HTML 中一样使用带连字符的命名法(也叫诸如 hyphen-cased,kebab-case,lisp-case)。

    <input
      type="text"
      aria-label={labelText}
      aria-required="true"
      onChange={onchangeHandler}
      value={inputValue}
      name="name"
    />
    
  21. React Fragments:为了不破坏HTML语义化

    import React, { Fragment } from 'react';
    // fragment 普通用法
    function ListItem({ item }) {
      return (
        <Fragment>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </Fragment>
      );
    }
    // fragment 带 key
    function Glossary(props) {
      return (
        <dl>
          {props.items.map(item => (
            // Fragments should also have a `key` prop when mapping collections
            <Fragment key={item.id}>
              <dt>{item.term}</dt>
              <dd>{item.description}</dd>
            </Fragment>
          ))}
        </dl>
      );
    }
    // fragment 短语法
    function ListItem({ item }) {
      return (
        <>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </>
      );
    }
    
  22. 标记:所有的 HTML 表单控制,例如 <input><textarea> ,都需要被标注来实现无障碍辅助功能。我们需要提供屏幕朗读器以解释性标注。

    // 请注意 for 在 JSX 中应该被写作 htmlFor:
    <label htmlFor="namedInput">Name:</label>
    <input id="namedInput" type="text" name="name"/>
    
  23. setState 写法:

    this.setState(currentState => ({
      isOpen: !currentState.isOpen
    }));
    
    this.setState({
      isOpen: !this.state.isOpen
    });
    
  24. 当需要关闭类似弹窗的时候,可给指定元素添加 ref ,通过 refs 来判断是否点击的非指定元素:

    class OuterClickExample extends React.Component {
      constructor(props) {
        super(props);
        this.toggleContainer = React.createRef();
      }
    
      componentDidMount() {
        window.addEventListener('click', this.onClickOutsideHandler);
      }
    
      componentWillUnmount() {
        window.removeEventListener('click', this.onClickOutsideHandler);
      }
    
      onClickOutsideHandler(event) {
        if (!this.toggleContainer.current.contains(event.target)) {
          this.setState({ isOpen: false });
        }
      }
    
      render() {
        return (
          <div ref={this.toggleContainer}>
            <button>Select an option</button>
          </div>
        );
      }
    }
    
  25. 无障碍使用浮窗:使用 onBluronFocus 替代 click

    class BlurExample extends React.Component {
      constructor(props) {
        super(props);
    
        this.state = { isOpen: false };
        this.timeOutId = null;
    
        this.onClickHandler = this.onClickHandler.bind(this);
        this.onBlurHandler = this.onBlurHandler.bind(this);
        this.onFocusHandler = this.onFocusHandler.bind(this);
      }
    
      onClickHandler() {
        this.setState(currentState => ({
          isOpen: !currentState.isOpen
        }));
      }
    
      // 我们在下一个时间点使用 setTimeout 关闭弹窗。
      // 这是必要的,因为失去焦点事件会在新的焦点事件前被触发,
      // 我们需要通过这个步骤确认这个元素的一个子节点
      // 是否得到了焦点。
      onBlurHandler() {
        this.timeOutId = setTimeout(() => {
          this.setState({
            isOpen: false
          });
        });
      }
    
      // 如果一个子节点获得了焦点,不要关闭弹窗。
      onFocusHandler() {
        clearTimeout(this.timeOutId);
      }
    
      render() {
        // React 通过把失去焦点和获得焦点事件传输给父节点
        // 来帮助我们。
        return (
          <div onBlur={this.onBlurHandler}
               onFocus={this.onFocusHandler}>
            <button onClick={this.onClickHandler}
                    aria-haspopup="true"
                    aria-expanded={this.state.isOpen}>
              Select an option
            </button>
            {this.state.isOpen && (
              <ul>
                <li>Option 1</li>
                <li>Option 2</li>
                <li>Option 3</li>
              </ul>
            )}
          </div>
        );
      }
    }
    
  26. React.lazy & Suspense:

    import React, { Suspense } from 'react';
    
    const OtherComponent = React.lazy(() => import('./OtherComponent'));
    
    function MyComponent() {
      return (
        <div>
          <Suspense fallback={<div>Loading...</div>}>
            <OtherComponent />
          </Suspense>
        </div>
      );
    }
    
  27. Context:提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
    通用的场景:管理当前的 locale,theme,或者一些缓存数据。

    注意:如果你只是想避免层层传递一些属性,可以通过组件组合(component composition)组件传递下去**
    也可使用 Render Props

    <DataProvider render={data => (
      <h1>Hello {data.target}</h1>
    )}/>
    
  28. React.createContext:创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

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

    const MyContext = React.createContext(defaultValue);
    
  29. Context.Provider:每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。

    ​Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个使用该值组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

    ​当 Provider 的 value 值发生变化时,它内部的所有使用该值组件都会重新渲染。Provider 及其内部 consumer(顾客) 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer(顾客) 组件在其祖先组件退出更新的情况下也能更新。

    <MyContext.Provider value={/* 某个值 */}>
    
  30. Class.contextType:挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
    注意:

    1. 你只通过该 API 订阅单一 context。

    2. 如果你正在使用实验性的 public class fields 语法,你可以使用 static 这个类属性来初始化你的 contextType

    class MyClass extends React.Component {
      static contextType = MyContext; // public class fields need
      componentDidMount() {
        let value = this.context;
        /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
      }
      componentDidUpdate() {
        let value = this.context;
        /* ... */
      }
      componentWillUnmount() {
        let value = this.context;
        /* ... */
      }
      render() {
        let value = this.context;
        /* 基于 MyContext 组件的值进行渲染 */
      }
    }
    MyClass.contextType = MyContext;
    
    
  31. Context.Consumer:一个 React 组件可以订阅 context 的变更,这让你在函数式组件中可以订阅 context。

    ​这个函数接收当前的 context 值,并返回一个 React 节点。传递给函数的 value 值等价于组件树上方离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext()defaultValue

    <MyContext.Consumer>
      {value => /* 基于 context 值进行渲染*/}
    </MyContext.Consumer>
    
  32. Context.displayName:context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。

    const MyContext = React.createContext(/* some value */);
    MyContext.displayName = 'MyDisplayName';
    
    <MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
    <MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
    
  33. ES5 中,方法写在 constructor 里面和外面的区别:

    /* A类,方法写在constructor里面 */
    class A{
      constructor(){
        this.show = function (){
          console.log( 'A show' )
        }
      }
    }
    
    /* B类,方法写在constructor外面 */
    class B{
      show(){
        console.log( 'B show' )
      }
    }
    
    const a1 = new A();
    const a2 = new A();
    console.log(a1.show === a2.show); // false
    const b1 = new B();
    const b2 = new B();
    console.log(b1.show === b2.show); // true
    
    说明:B的方法是在 prototype 上的
    A每实例一个类就创建一个独有的方法,每个都需要单独修改
    B则是公用原型上的方法,改一次
    即 A 的 a1.__proto__ 无 show 方法,是不会影响已存在的对象 a1,a2 的 show 方法的
    而 B 的 a1.__proto__ 有 show 方法,则会同时修改 b1,b2
    
  34. 在 React 中,获取父类传递的值可通过以下方式:

    class ThemedButton extends React.Component {
      render() {
        let props = this.props;
        return (
          <button
            {...props}
            style={{backgroundColor: 'yellow'}}
          />
        );
      }
    }
    
    class A extends React.Component {
      render() {
        return (
          <ThemedButton
            aria-label="hahaha"
            aria-required="true"
            onClick={this.changeTheme}>
            what
          </ThemedButton>
        );
      }
    }
    
    // 输出
    <button style="background-color: 'yellow'" aria-label="hahaha" aria-required="true">
      what
    </button>
    
    
  35. 如何使用 Context.Consumer

    const themes = {
      light: {
        foreground: '#000000',
        background: '#eeeeee',
      },
      dark: {
        foreground: '#ffffff',
        background: '#222222',
      },
    };
    
    const ThemeContext = React.createContext({
      theme: themes.dark,
      toggleTheme: () => {},
    });
    
    function ThemeTogglerButton() {
      // Theme Toggler 按钮不仅仅只获取 theme 值,它也从 context 中获取到一个 toggleTheme 函数
      return (
        <ThemeContext.Consumer>
          {({theme, toggleTheme}) => (
            <button
              onClick={toggleTheme}
              style={{backgroundColor: theme.background}}>
              Toggle Theme
            </button>
          )}
        </ThemeContext.Consumer>
      );
    }
    
    class App extends React.Component {
      constructor(props) {
        super(props);
    
        // 方式一
        this.toggleTheme = () => {
          this.setState(state => ({
            theme:
              state.theme === themes.dark
                ? themes.light
                : themes.dark,
          }));
        };
    
        // State 也包含了更新函数,因此它会被传递进 context provider。
        this.state = {
          theme: themes.light,
          toggleTheme: this.toggleTheme,
        };
      }
    
      // 方式二
      // toggleTheme = () => {
      //   this.setState(state => ({
      //     theme:
      //     state.theme === themes.dark
      //     ? themes.light
      //     : themes.dark,
      //   }));
      // };
    
      render() {
        // 整个 state 都被传递进 provider
        return (
          <ThemeContext.Provider value={this.state}>
            <Content />
          </ThemeContext.Provider>
        );
      }
    }
    
    function Content() {
      return (
        <div>
          <ThemeTogglerButton />
        </div>
      );
    }
    
    ReactDOM.render(
      <App />,
      document.getElementById('root')
    );
    
    
  36. 使用多个 Context:为了确保 context 快速进行重渲染,React 需要使每一个 consumers 组件的 context 在组件树中成为一个单独的节点。

    // Theme context,默认的 theme 是 “light” 值
    const ThemeContext = React.createContext('light');
    
    // 用户登录 context
    const UserContext = React.createContext({
      name: 'Guest',
    });
    
    class App extends React.Component {
      render() {
        const {signedInUser, theme} = this.props;
    
        // 提供初始 context 值的 App 组件
        return (
          <ThemeContext.Provider value={theme}>
            <UserContext.Provider value={signedInUser}>
              <Layout />
            </UserContext.Provider>
          </ThemeContext.Provider>
        );
      }
    }
    
    function Layout() {
      return (
        <div>
          <Sidebar />
          <Content />
        </div>
      );
    }
    
    // 一个组件可能会消费多个 context
    function Content() {
      return (
        <ThemeContext.Consumer>
          {theme => (
            <UserContext.Consumer>
              {user => (
                <ProfilePage user={user} theme={theme} />
              )}
            </UserContext.Consumer>
          )}
        </ThemeContext.Consumer>
      );
    }
    
  37. 使用 context 的注意事项:因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:

    // the wrong way
    class App extends React.Component {
      render() {
        return (
          <MyContext.Provider value={{something: 'something'}}>
            <Toolbar />
          </MyContext.Provider>
        );
      }
    }
    
    // the right way
    class App extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          value: {something: 'something'},
        };
      }
    
      render() {
        return (
          <Provider value={this.state.value}>
            <Toolbar />
          </Provider>
        );
      }
    }
    
  38. 错误边界:是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

    ​只有 class 组件才可以成为错误边界组件。大多数情况下, 你只需要声明一次错误边界组件, 并在整个应用中使用它。

    注意错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。如果一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界,这也类似于 JavaScript 中 catch {} 的工作机制。

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

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

    ​如果一个 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;
      }
    }
    
    // 使用方式
    <ErrorBoundary>
      <MyWidget />
    </ErrorBoundary>
    
    
  39. 未捕获错误(Uncaught Errors)的新行为:任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。

  40. 为什么不用 try / catch 捕获异常?

    // try / catch 很棒但它仅能用于命令式代码(imperative code)
    try {
      showButton();
    } catch (error) {
      // ...
    }
    
    // React 组件是声明式的并且具体指出什么需要被渲染
    <Button />
    

    错误边界无法捕获事件处理器内部的错误。如果你需要在事件处理器内部捕获错误,使用普通的 JavaScript try / catch 语句:

    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = { error: null };
        this.handleClick = this.handleClick.bind(this);
      }
    
      handleClick() {
        try {
          // 执行操作,如有错误则会抛出
        } catch (error) {
          this.setState({ error });
        }
      }
    
      render() {
        if (this.state.error) {
          return <h1>Caught an error.</h1>
        }
        return <button onClick={this.handleClick}>Click Me</button>
      }
    }
    
  41. 编程范式:命令式编程(Imperative)、声明式编程(Declarative)和函数式编程(Functional)

    • 命令式编程:关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。
    • 声明式编程:以数据结构的形式来表达程序执行的逻辑。告诉计算机应该做什么,但不指定具体要怎么做。
    • 函数式编程:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。
  42. Refs 转发:Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于可重用的组件库是很有用的。Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。

    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 me!</FancyButton>;
    

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

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

  43. 在高阶组件中转发 refs:

    function logProps(WrappedComponent) {
      class LogProps extends React.Component {
        componentDidUpdate(prevProps) {
          console.log('old props:', prevProps);
          console.log('new props:', this.props);
        }
    
        render() {
          return <WrappedComponent {...this.props} />;
        }
      }
    
      return LogProps;
    }
    
    class FancyButton extends React.Component {
      focus() {
        // ...
      }
    
      // ...
    }
    
    // 我们导出 LogProps,而不是 FancyButton。
    // 虽然它也会渲染一个 FancyButton。
    export default logProps(FancyButton);
    

    注意:refs 将不会透传下去。这是因为 ref 不是 prop 属性。就像 key 一样,其被 React 进行了特殊处理。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。

    import FancyButton from './FancyButton';
    
    const ref = React.createRef();
    
    // 我们导入的 FancyButton 组件是高阶组件(HOC)LogProps。
    // 尽管渲染结果将是一样的,
    // 但我们的 ref 将指向 LogProps 而不是内部的 FancyButton 组件!
    // 这意味着我们不能调用例如 ref.current.focus() 这样的方法
    <FancyButton
      label="Click Me"
      handleClick={handleClick}
      ref={ref}
    />;
    

    但是,我们可以使用 React.forwardRef API 明确地将 refs 转发到内部的 FancyButton 组件。React.forwardRef 接受一个渲染函数,其接收 propsref 参数并返回一个 React 节点:

    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;
    
          // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
          return <Component ref={forwardedRef} {...rest} />;
        }
      }
    
      // 注意 React.forwardRef 回调的第二个参数 “ref”。
      // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
      // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
      return React.forwardRef((props, ref) => {
        return <LogProps {...props} forwardedRef={ref} />;
      });
    }
    
  44. 高阶组件:是参数为组件,返回值为新组件的函数。
    HOC 在 React 的第三方库中很常见,例如 Redux 的 connect 和 Relay 的 createFragmentContainer

    高阶组件使用场景:我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。

    // 此函数接收一个组件...
    function withSubscription(WrappedComponent, selectData) {
      // ...并返回另一个组件...
      return class extends React.Component {
        constructor(props) {
          super(props);
          this.handleChange = this.handleChange.bind(this);
          this.state = {
            data: selectData(DataSource, props)
          };
        }
    
        componentDidMount() {
          // ...负责订阅相关的操作...
          DataSource.addChangeListener(this.handleChange);
        }
    
        componentWillUnmount() {
          DataSource.removeChangeListener(this.handleChange);
        }
    
        handleChange() {
          this.setState({
            data: selectData(DataSource, this.props)
          });
        }
    
        render() {
          // ... 并使用新数据渲染被包装的组件!
          // 请注意,我们可能还会传递其他属性
          return <WrappedComponent data={this.state.data} {...this.props} />;
        }
      };
    }
    

    注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件 包装 在容器组件中来 组成 新组件。HOC 是纯函数,没有副作用。

    • 不要改变原始组件。使用组合。

    • 约定:将不相关的 props 传递给被包裹的组件 --- 保证 HOC 的灵活性以及可复用性。

      render() {
        // 过滤掉非此 HOC 额外的 props,且不要进行透传
        const { extraProp, ...passThroughProps } = this.props;
      
        // 将 props 注入到被包装的组件中。
        // 通常为 state 的值或者实例方法。
        const injectedProp = someStateOrInstanceMethod;
      
        // 将 props 传递给被包装组件
        return (
          <WrappedComponent
            injectedProp={injectedProp}
            {...passThroughProps}
          />
        );
      }
      
    • 约定:最大化可组合性

      // React Redux 的 `connect` 函数
      const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
      
      // connect 是一个函数,它的返回值为另外一个函数。
      const enhance = connect(commentListSelector, commentListActions);
      // 返回值为 HOC,它会返回已经连接 Redux store 的组件
      const ConnectedComment = enhance(CommentList);
      
      --------------------------------------------------------
      
      // 而不是这样...
      const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
      
      // ... 你可以编写组合工具函数
      // compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
      const enhance = compose(
        // 这些都是单参数的 HOC
        withRouter,
        connect(commentSelector)
      )
      const EnhancedComponent = enhance(WrappedComponent)
      
    • 约定:包装显示名称以便轻松调试

      function withSubscription(WrappedComponent) {
        class WithSubscription extends React.Component {/* ... */}
        WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
        return WithSubscription;
      }
      
      function getDisplayName(WrappedComponent) {
        return WrappedComponent.displayName || WrappedComponent.name || 'Component';
      }
      

      不要在 render 方法中使用 HOC

      务必复制静态方法

      function enhance(WrappedComponent) {
        class Enhance extends React.Component {/*...*/}
        // 必须准确知道应该拷贝哪些方法 :(
        Enhance.staticMethod = WrappedComponent.staticMethod;
        return Enhance;
      }
      
      // or
      import hoistNonReactStatic from 'hoist-non-react-statics';
      function enhance(WrappedComponent) {
        class Enhance extends React.Component {/*...*/}
        hoistNonReactStatic(Enhance, WrappedComponent);
        return Enhance;
      }
      
      // or
      // 使用这种方式代替...
      MyComponent.someFunction = someFunction;
      export default MyComponent;
      
      // ...单独导出该方法...
      export { someFunction };
      
      // ...并在要使用的组件中,import 它们
      import MyComponent, { someFunction } from './MyComponent.js';
      

    Refs 不会被传递,可通过 React.forwardRef 传递

  45. JSX:仅仅只是 React.createElement(component, props, ...children) 函数的语法糖

    <MyButton color="blue" shadowSize={2}>
      Click Me
    </MyButton>
    
    =========
    React.createElement(
      MyButton,
      {color: 'blue', shadowSize: 2},
      'Click Me'
    )
    
    --------------------------------------------------------
    <div className="sidebar" />
    =========
    React.createElement(
      'div',
      {className: 'sidebar'}
    )
    

    JSX 点语法

    import React from 'react';
    
    const MyComponents = {
      DatePicker: function DatePicker(props) {
        return <div>Imagine a {props.color} datepicker here.</div>;
      }
    }
    
    function BlueDatePicker() {
      return <MyComponents.DatePicker color="blue" />;
    }
    
  46. 在运行时选择类型(动态组件)

    import React from 'react';
    import { PhotoStory, VideoStory } from './stories';
    
    const components = {
      photo: PhotoStory,
      video: VideoStory
    };
    
    // wrong
    
    function Story(props) {
      // 错误!JSX 类型不能是一个表达式。
      return <components[props.storyType] story={props.story} />;
    }
    // right
    
    function Story(props) {
      // 正确!JSX 类型可以是大写字母开头的变量。
      const SpecificStory = components[props.storyType];
      return <SpecificStory story={props.story} />;
    }
    
  47. 字符串字面量:

    <MyComponent message="hello world" />
    // 等价于
    <MyComponent message={'hello world'} />
    
    <MyComponent message="&lt;3" />
    // 等价于
    <MyComponent message={'<3'} />
    
  48. Props 默认值为 “True”:如果你没给 prop 赋值,它的默认值是 true

    <MyTextBox autocomplete /> // 不推荐
    // 等价于
    <MyTextBox autocomplete={true} /> // 推荐
    
  49. 函数作为子元素:

    // 调用子元素回调 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>
      );
    }
    
  50. 布尔类型、Null 以及 Undefined 将会忽略:

    // 以下的 JSX 表达式渲染结果相同:
    <div />
    
    <div></div>
    
    <div>{false}</div>
    
    <div>{null}</div>
    
    <div>{undefined}</div>
    
    <div>{true}</div>
    

    值得注意的是有一些 “falsy” 值,如数字 0,仍然会被 React 渲染。

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

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

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

    // index.html
    <html>
      <body>
        <div id="app-root"></div>
        <div id="modal-root"></div>
      </body>
    </html>
    
    // index.jsx
    // 在 DOM 中有两个容器是兄弟级 (siblings)
    const appRoot = document.getElementById('app-root');
    const modalRoot = document.getElementById('modal-root');
    
    class Modal extends React.Component {
      constructor(props) {
        super(props);
        this.el = document.createElement('div');
      }
    
      componentDidMount() {
        // 在 Modal 的所有子元素被挂载后,
        // 这个 portal 元素会被嵌入到 DOM 树中,
        // 这意味着子元素将被挂载到一个分离的 DOM 节点中。
        // 如果要求子组件在挂载时可以立刻接入 DOM 树,
        // 例如衡量一个 DOM 节点,
        // 或者在后代节点中使用 ‘autoFocus’,
        // 则需添加 state 到 Modal 中,
        // 仅当 Modal 被插入 DOM 树中才能渲染子元素。
        modalRoot.appendChild(this.el);
      }
    
      componentWillUnmount() {
        modalRoot.removeChild(this.el);
      }
    
      render() {
        return ReactDOM.createPortal(
          this.props.children,
          this.el
        );
      }
    }
    
    class Parent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {clicks: 0};
        this.handleClick = this.handleClick.bind(this);
      }
    
      handleClick() {
        // 当子元素里的按钮被点击时,
        // 这个将会被触发更新父元素的 state,
        // 即使这个按钮在 DOM 中不是直接关联的后代
        this.setState(state => ({
          clicks: state.clicks + 1
        }));
      }
    
      render() {
        return (
          <div onClick={this.handleClick}>
            <p>Number of clicks: {this.state.clicks}</p>
            <p>
              Open up the browser DevTools
              to observe that the button
              is not a child of the div
              with the onClick handler.
            </p>
            <Modal>
              <Child />
            </Modal>
          </div>
        );
      }
    }
    
    function Child() {
      // 这个按钮的点击事件会冒泡到父元素
      // 因为这里没有定义 'onClick' 属性
      return (
        <div className="modal">
          <button>Click</button>
        </div>
      );
    }
    
    ReactDOM.render(<Parent />, appRoot);
    
  52. Profiler API:测量渲染一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。

    注意:Profiling 增加了额外的开支,所以它在生产构建中会被禁用

    用法:Profiler 能添加在 React 树中的任何地方来测量树中这部分渲染所带来的开销。 它需要两个 prop :一个是 id(string),一个是当组件树中的组件“提交”更新的时候被React调用的回调函数 onRender(function)。

    // 分析 Navigation 组件和它的子代
    render(
    <App>
        <Profiler id="Navigation" onRender={callback}>
      <Navigation {...props} />
        </Profiler>
        <Main {...props} />
      </App>
    );
    
    // 多个 Profiler 组件能测量应用中的不同部分
    render(
      <App>
        <Profiler id="Navigation" onRender={callback}>
          <Navigation {...props} />
        </Profiler>
        <Profiler id="Main" onRender={callback}>
          <Main {...props} />
        </Profiler>
      </App>
    );
    
    // 嵌套使用 Profiler 组件来测量相同一个子树下的不同组件
    render(
      <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>
    );
    
  53. 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 会先按照定义时的顺序执行,最后调用组件上对应的方法

  54. 不使用 JSX:

    // React.createElement(component, props, ...children)
    // 使用 JSX
    class Hello extends React.Component {
      render() {
        return <div>Hello {this.props.toWhat}</div>;
      }
    }
    
    ReactDOM.render(
      <Hello toWhat="World" />,
      document.getElementById('root')
    );
    
    // 不使用 JSX
    class Hello extends React.Component {
      render() {
        return React.createElement('div', null, `Hello ${this.props.toWhat}`);
      }
    }
    
    ReactDOM.render(
      React.createElement(Hello, {toWhat: 'World'}, null),
      document.getElementById('root')
    );
    
  55. 设计动力:在某一时间节点调用 React 的 render() 方法,会创建一棵由 React 元素组成的树。在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步。

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

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

    都成立

  56. Diffing 算法:当对比两颗树时,React 首先比较两棵树的根节点。不同类型的根节点元素会有不同的形态。

    • 比对不同类型的元素

      当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。

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

      // React 会销毁 Counter 组件并且重新装载一个新的组件
      <div>
        <Counter />
      </div>
      
      <span>
        <Counter />
      </span>
      
    • 对比同一类型的元素

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

      // 只需要修改 DOM 元素上的 className 属性
      <div className="before" title="stuff" />
      
      <div className="after" title="stuff" />
      // 仅更新有所更变的属性
      <div style={{color: 'red', fontWeight: 'bold'}} />
      
      <div style={{color: 'green', fontWeight: 'bold'}} />
      // 只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight。
      <div style={{color: 'red', fontWeight: 'bold'}} />
      
      <div style={{color: 'green', fontWeight: 'bold'}} />
      
    • 对比同类型的组件元素

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

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

    • 对子节点进行递归

      ​在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。
      ​在子元素列表末尾新增元素时,更新开销比较小。(只更新最后一个)

      ​简单的将新增元素插入到表头,更新开销比较大。(更新全部)

    • Keys(解决以上问题)

      ​当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。

      // 这个 key 不需要全局唯一,但在列表中需要保持唯一
      <li key={item.id}>{item.name}</li>
      // 也可以使用元素在数组中的下标作为 key。这个策略在元素不进行重新排序时比较合适,如果有顺序修改,diff 就会变得慢。
      

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

    注意:

    1. 该算法不会尝试匹配不同组件类型的子树。如果你发现你在两种不同类型的组件中切换,但输出非常相似的内容,建议把它们改成同一类型。在实践中,我们没有遇到这类问题。
    2. Key 应该具有稳定,可预测,以及列表内唯一的特质。不稳定的 key(比如通过 Math.random() 生成的)会导致许多组件实例和 DOM 节点被不必要地重新创建,这可能导致性能下降和子组件中的状态丢失。
  57. 何时使用 Refs:

    • 管理焦点,文本选择或媒体播放。
    • 触发强制动画。
    • 集成第三方 DOM 库。

    注意:避免使用 refs 来做任何可以通过声明式实现来完成的事情。

  58. 创建 Refs:Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。

    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.myRef = React.createRef();
      }
      render() {
        return <div ref={this.myRef} />;
      }
    }
    
  59. 访问 Refs:当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

    const node = this.myRef.current;
    

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

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

    例子:

    1. 为 DOM 元素添加 ref

      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 生命周期钩子触发前更新。

    2. 为 class 组件添加 Ref:仅在 CustomTextInput 声明为 class 时才有效!

      class AutoFocusTextInput extends React.Component {
        constructor(props) {
          super(props);
          this.textInput = React.createRef();
        }
      
        componentDidMount() {
          this.textInput.current.focusTextInput();
        }
      
        render() {
          return (
            <CustomTextInput ref={this.textInput} />
          );
        }
      }
      
    3. Refs 与函数组件:

      默认情况下,你不能在函数组件上使用 ref 属性,因为它们没有实例。

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

      你可以在函数组件内部使用 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>
        );
      }
      
    4. 将 DOM Refs 暴露给父组件:Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。见前文- Ref 转发

  60. 回调 Refs:

    ​React 也支持另一种设置 refs 的方式,称为“回调 refs”。它能助你更精细地控制何时 refs 被设置和解除。

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

    范例:使用 ref 回调函数,在实例的属性中存储对 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 一定是最新的。

    function CustomTextInput(props) {
      return (
        <div>
          <input ref={props.inputRef} />
        </div>
      );
    }
    
    class Parent extends React.Component {
      render() {
        return (
          <CustomTextInput
            inputRef={el => this.inputElement = el}
          />
        );
      }
    }
    
    //Parent 把它的 refs 回调函数当作 inputRef props 传递给了 CustomTextInput,而且 CustomTextInput 把相同的函数作为特殊的 ref 属性传递给了 <input>。结果是,在 Parent 中的 this.inputElement 会被设置为与 CustomTextInput 中的 input 元素相对应的 DOM 节点。
    
  61. this.refs:过时的API
    注意:如果你目前还在使用 this.refs.textInput 这种方式访问 refs ,我们建议用回调函数createRef API 的方式代替。

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

  63. Render Props:指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

    ​具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑。

    <DataProvider render={data => (
      <h1>Hello {data.target}</h1>
    )}/>
    

    使用 render prop 的库有 React RouterDownshift 以及 Formik

  64. 使用 Render Props 来解决横切关注点(Cross-Cutting Concerns):

    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}>
    
            {/*
              Instead of providing a static representation of what <Mouse> renders,
              use the `render` prop to dynamically determine what to render.
            */}
            {this.props.render(this.state)}
          </div>
        );
      }
    }
    
    class MouseTracker extends React.Component {
      render() {
        return (
          <div>
            <h1>移动鼠标!</h1>
            <Mouse render={mouse => (
              <Cat mouse={mouse} />
            )}/>
          </div>
        );
      }
    }
    

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

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

    任何被用于告知组件需要渲染什么内容的函数 prop 在技术上都可以被称为 “render prop”

    尽管之前的例子使用了 render,我们也可以简单地使用 children prop!

    <Mouse children={mouse => (
      <p>鼠标的位置是 {mouse.x},{mouse.y}</p>
    )}/>
    
    // 记住,children prop 并不真正需要添加到 JSX 元素的 “attributes” 列表中。相反,你可以直接放置到元素的内部!
    <Mouse>
      {mouse => (
        <p>鼠标的位置是 {mouse.x},{mouse.y}</p>
      )}
    </Mouse>
    
    // 由于这一技术的特殊性,当你在设计一个类似的 API 时,你或许会要直接地在你的 propTypes 里声明 children 的类型应为一个函数。
    Mouse.propTypes = {
      children: PropTypes.func.isRequired
    };
    
  65. 将 Render Props 与 React.PureComponent 一起使用时要小心:

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

    class Mouse extends React.PureComponent {
      // 与上面相同的代码......
    }
    
    class MouseTracker extends React.Component {
      render() {
        return (
          <div>
            <h1>Move the mouse around!</h1>
    
            {/*
              这是不好的!
              每个渲染的 `render` prop的值将会是不同的。
            */}
            <Mouse render={mouse => (
              <Cat mouse={mouse} />
            )}/>
          </div>
        );
      }
    }
    
    

    为了绕过这一问题,有时你可以定义一个 prop 作为实例方法,类似这样:

    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>
        );
      }
    }
    

    如果你无法静态定义 prop(例如,因为你需要关闭组件的 props 和/或 state),则 <Mouse> 应该扩展 React.Component

  66. 严格模式:StrictMode 是一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。
    注意:严格模式检查仅在开发模式下运行;它们不会影响生产构建。**
    你可以为应用程序的任何部分启用严格模式:

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

    在上述的示例中,会对 HeaderFooter 组件运行严格模式检查。但是,ComponentOneComponentTwo 以及它们的所有后代元素都将进行检查。

    StrictMode 目前有助于:

    • 识别不安全的生命周期

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

      ​this.refs => createRef | 回调 ref

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

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

      ​你也可以在组件中创建一个 DOM 节点的 wrapper,并将 ref 直接绑定到它。

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

      注意:在 CSS 中,如果你不希望节点成为布局的一部分,则可以使用 display: contents 属性。

      display: contents:不将自身渲染到页面(可访问树)上,只保留子元素

    • 检测意外的副作用

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

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

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

      • constructor

      • componentWillMount (or UNSAFE_componentWillMount)

      • componentWillReceiveProps (or UNSAFE_componentWillReceiveProps)

      • componentWillUpdate (or UNSAFE_componentWillUpdate)

      • getDerivedStateFromProps

      • shouldComponentUpdate

      • render

      • setState 更新函数(第一个参数)

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

    • 检测过时的 context API

  67. 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.'
          );
        }
      })
    };
    
  68. 默认 Prop 值:
    您可以通过配置特定的 defaultProps 属性来定义 props 的默认值:

    class Greeting extends React.Component {
      render() {
        return (
          <h1>Hello, {this.props.name}</h1>
        );
      }
    }
    
    // 指定 props 的默认值:
    Greeting.defaultProps = {
      name: 'Stranger'
    };
    
    // 渲染出 "Hello, Stranger":
    ReactDOM.render(
      <Greeting />,
      document.getElementById('example')
    );
    

    如果你正在使用像 transform-class-properties 的 Babel 转换工具,你也可以在 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

  69. 非受控组件:

    class NameForm extends React.Component {
      constructor(props) {
        super(props);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.input = React.createRef();
      }
    
      handleSubmit(event) {
        alert('A name was submitted: ' + this.input.current.value);
        event.preventDefault();
      }
    
      render() {
        return (
          <form onSubmit={this.handleSubmit}>
            <label>
              Name:
              <input type="text" ref={this.input} />
            </label>
            <input type="submit" value="Submit" />
          </form>
        );
      }
    }
    

    默认值:在 React 渲染生命周期时,表单元素上的 value 将会覆盖 DOM 节点中的值,在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value

    render() {
      return (
        <form onSubmit={this.handleSubmit}>
          <label>
            Name:
            <input
              defaultValue="Bob"
              type="text"
              ref={this.input} />
          </label>
          <input type="submit" value="Submit" />
        </form>
      );
    }
    

    同样,<input type="checkbox"><input type="radio"> 支持 defaultChecked<select><textarea> 支持 defaultValue

    文件输入:在 React 中,<input type="file" /> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。

  70. React 顶层 API:

    • React.Component

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

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

      ​注意:React.PureComponent 中的 shouldComponentUpdate() 仅作对象的浅层比较。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。仅在你的 props 和 state 较为简单时,才使用 React.PureComponent,或者在深层数据结构发生变化时调用 forceUpdate() 来确保组件被正确地更新。你也可以考虑使用 immutable 对象加速嵌套数据的比较。

      ​此外,React.PureComponent 中的 shouldComponentUpdate() 将跳过所有子组件树的 prop 更新。因此,请确保所有子组件也都是“纯”的组件。

    • React.memo:性能优化

      const MyComponent = React.memo(function MyComponent(props) {
        /* 使用 props 渲染 */
      });
      

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

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

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

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

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

      注意:与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。

    • createElement()

    • cloneElement()

      React.cloneElement(
        element,
        [props],
        [...children]
      )
      
      // 等同于
      <element.type {...element.props} {...props}>{children}</element.type>
      

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

    • isValidElement():验证对象是否为 React 元素,返回值为 truefalse

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

      • React.Children.map(children, function[(thisArg)]):在 children 里的每个直接子节点上调用一个函数,并将 this 设置为 thisArg。如果 children 是一个数组,它将被遍历并为数组中的每个子节点调用该函数。如果子节点为 null 或是 undefined,则此方法将返回 null 或是 undefined,而不会返回数组。

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

      • React.Children.forEach(children, function[(thisArg)]):与 React.Children.map() 类似,但它不会返回一个数组。

      • React.Children.count(children):返回 children 中的组件总数量,等同于通过 mapforEach 调用回调函数的次数。

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

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

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

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

    • React.Fragment

    • React.createRef

    • React.forwardRefReact.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用:

      React.forwardRef 接受渲染函数作为参数。React 将使用 propsref 作为参数来调用此函数。此函数应返回 React 节点。

      const FancyButton = React.forwardRef((props, ref) => (
        <button ref={ref} className="FancyButton">
          {props.children}
        </button>
      ));
      
      // You can now get a ref directly to the DOM button:
      const ref = React.createRef();
      <FancyButton ref={ref}>Click me!</FancyButton>;
      
    • React.lazy:允许你定义一个动态加载的组件。这有助于缩减 bundle 的体积,并延迟加载在初次渲染时未用到的组件。

      // 这个组件是动态加载的
      const SomeComponent = React.lazy(() => import('./SomeComponent'));
      

      注意:渲染 lazy 组件依赖该组件渲染树上层的 <React.Suspense> 组件。这是指定加载指示器(loading indicator)的方式。

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

      // 该组件是动态加载的
      const OtherComponent = React.lazy(() => import('./OtherComponent'));
      
      function MyComponent() {
        return (
          // 显示 <Spinner> 组件直至 OtherComponent 加载完成
          <React.Suspense fallback={<Spinner />}>
            <div>
              <OtherComponent />
            </div>
          </React.Suspense>
        );
      }
      
  71. React.Component 生命周期

    • 挂载

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

    • 更新

      当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

    • 卸载

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

    • 错误处理

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

    • 详细:

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

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

        ​如需与浏览器进行交互,请在 componentDidMount() 或其他生命周期方法中执行你的操作。保持 render() 为纯函数,可以使组件更容易思考。
        注意:如果 shouldComponentUpdate() 返回 false,则不会调用 render()

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

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

        通常,在 React 中,构造函数仅用于以下两种情况:

        • 通过给 this.state 赋值对象来初始化内部 state。
        • 为事件处理函数绑定实例

        在 constructor() 函数中不要调用 setState() 方法。如果你的组件需要使用内部 state,请直接在构造函数中为 this.state 赋值初始 state:

        constructor(props) {
          super(props);
          // 不要在这里调用 this.setState()
          this.state = { counter: 0 };
          this.handleClick = this.handleClick.bind(this);
        }
        
      • s

上一篇下一篇

猜你喜欢

热点阅读