React文档漫步-高级指引

代码分割

import()

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

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

React.lazy + Suspense

// 使用前
import OtherComponent from './OtherComponent';

// 使用后
import React, { Suspense } from 'react';

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

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

基于路由的代码分割

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  </Router>
);

命名导出

  • React.lazy 目前只支持默认导出(default exports)。可以使用中间模块来重新导出默认模块。
// 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"));

Context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

  • 如果只是避免层层传递某些属性,组件组合可能会是比context更好的解决方案

    • // 原方案
      <Page user={user} avatarSize={avatarSize} />
      // ... 渲染出 ...
      <PageLayout user={user} avatarSize={avatarSize} />
      // ... 渲染出 ...
      <NavigationBar user={user} avatarSize={avatarSize} />
      // ... 渲染出 ...
      <Link href={user.permalink}>
        <Avatar user={user} size={avatarSize} />
      </Link>
      
      
      // 可以用
      function Page(props) {
        const user = props.user;
        const userLink = (
          <Link href={user.permalink}>
            <Avatar user={user} size={props.avatarSize} />
          </Link>
        );
        return <PageLayout userLink={userLink} />;
      }
      
      // 现在,我们有这样的组件:
      <Page user={user} avatarSize={avatarSize} />
      // ... 渲染出 ...
      <PageLayout userLink={...} />
      // ... 渲染出 ...
      <NavigationBar userLink={...} />
      // ... 渲染出 ...
      {props.userLink}
      
  • context值最好采用引用值

    • // 不推荐,value 属性总是被赋值为新的对象
      class App extends React.Component {
        render() {
          return (
            <MyContext.Provider value={{something: 'something'}}>
              <Toolbar />
            </MyContext.Provider>
          );
        }
      }
      
      // 推荐
      class App extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            value: {something: 'something'},
          };
        }
      
        render() {
          return (
            <MyContext.Provider value={this.state.value}>
              <Toolbar />
            </MyContext.Provider>
          );
        }
      }
      
  • 可以使用Context.displayName在DevTools 中显示自定义名称

Refs and the DOM

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

说人话,何时使用Refs?

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

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

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

Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref

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 me!</FancyButton>;
  1. 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
  2. 我们通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>
  3. React 传递 refforwardRef 内函数 (props, ref) => ...,作为其第二个参数。
  4. 我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
  5. 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。

可以使用forwardRef.displayName 在DevTools 中显示自定义名称

Fragments

短语法:<> </>

长语法:React.Fragment。只有长语法可以传递key。

高阶组件

HOC不是API,是一种设计模式。

高阶组件是参数为组件,返回值为新组件的函数。组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。例如 Redux 的 connect

  • HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。

  • HOC 是纯函数,没有副作用。

  • HOC 应该透传与自身无关的 props。大多数 HOC 都应该包含一个类似于下面的 render 方法

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

    • // React Redux 的 `connect` 函数
      const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
      
      // connect 是一个函数,它的返回值为另外一个函数。
      const enhance = connect(commentListSelector, commentListActions);
      // 返回值为 HOC,它会返回已经连接 Redux store 的组件
      const ConnectedComment = enhance(CommentList);
      
      // connect 是一个返回高阶组件的高阶函数!
      
      // 不推荐如下写法...
      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)
      
    • 许多第三方库都提供了 compose 工具函数,包括 lodash (比如 lodash.flowRight

  • 包装名称

    • function withSubscription(WrappedComponent) {
        class WithSubscription extends React.Component {/* ... */}
        WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
        return WithSubscription;
      }
      
      function getDisplayName(WrappedComponent) {
        return WrappedComponent.displayName || WrappedComponent.name || 'Component';
      }
      
  • 静态方法需要自行复制

    • // 定义静态函数
      WrappedComponent.staticMethod = function() {/*...*/}
      // 现在使用 HOC
      const EnhancedComponent = enhance(WrappedComponent);
      
      // 增强组件没有 staticMethod
      typeof EnhancedComponent.staticMethod === 'undefined' // true
      
      
      // 使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法
      import hoistNonReactStatic from 'hoist-non-react-statics';
      function enhance(WrappedComponent) {
        class Enhance extends React.Component {/*...*/}
        hoistNonReactStatic(Enhance, WrappedComponent);
        return Enhance;
      }
      
      
      // 或者额外导出这个方法
      / 使用这种方式代替...
      MyComponent.someFunction = someFunction;
      export default MyComponent;
      
      // ...单独导出该方法...
      export { someFunction };
      
      // ...并在要使用的组件中,import 它们
      import MyComponent, { someFunction } from './MyComponent.js';
      
      

错误边界

错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。

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 组件树被卸载。

  • 如需要在事件处理器内部捕获错误,使用普通的 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>
        }
      }
      

JSX深入

语法糖

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

React 必须在作用域内

import React from 'react';
import CustomButton from './CustomButton';

function WarningButton() {
  // return React.createElement(CustomButton, {color: 'red'}, null);
  return <CustomButton color="red" />;
}

在 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" />;
}

在运行时选择类型

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

JSX中的props

展开运算符

unction App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

JSX子元素

包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children 传递给外层组件

  • 函数作为子元素

    // 用法并不常见,但可以用于扩展 JSX
    // 调用子元素回调 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>
      );
    }
    
  • 布尔类型、Null、Undefined会忽略

    // 仅当showHeader为true时,才会渲染<Header />组件
    <div>
      {showHeader && <Header />}
      <Content />
    </div>
    

性能优化

React Profiler

在开发模式下,React 开发者工具会出现分析器标签。

虚拟化长列表

shouldComponentUpdate

// React比较更新时时浅比较的

// 使用shouldComponentUpdate 仅检查了 props.color 或 state.count 是否改变。如果这些值没有改变,那么这个组件不会更新。
class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

// 大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate.但是他是浅比较的
class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

// 当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了

Profiler

Profiler 测量一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。

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

function onRenderCallback(
  id, // 发生提交的 Profiler 树的 “id”
  phase, // "mount" (如果组件树刚加载) 或者 "update" (如果它重渲染了)之一
  actualDuration, // 本次更新 committed 花费的渲染时间
  baseDuration, // 估计不使用 memoization 的情况下渲染整棵子树需要的时间
  startTime, // 本次更新中 React 开始渲染的时间
  commitTime, // 本次更新中 React committed 的时间
  interactions // 属于本次更新的 interactions 的集合
) {
  // 合计或记录渲染时间。。。
}
  • phase: "mount" | "update" - 判断是组件树的第一次装载引起的重渲染,还是由 props、state 或是 hooks 改变引起的重渲染。

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

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

  • interactions: Set - 当更新被制定时,“interactions” 的集合会被追踪。(例如当 render 或者 setState 被调用时)。Interactions 能用来识别更新是由什么引起的,尽管这个追踪更新的 API 依然是实验性质的。

Portals

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

ReactDOM.createPortal(child, container)

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

render() {
  // React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
  // `domNode` 是一个可以在任何位置的有效 DOM 节点。
  return ReactDOM.createPortal(
    this.props.children,
    domNode
  );
}
  • 一个 portal 的典型用例是当父组件有 overflow: hiddenz-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框
  • Portal事件冒泡
    • 虽然portal可以被放在DOM书的任何地方,但他仍是一个普通React子节点,也就是说,portal仍在React树中,context等功能特性都不变。
    • 一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先。

协调

有关diffing算法的一些设计

首先比较两棵树的根节点

对比不同类型的元素

  • 根节点为不同类型的元素时,拆卸原有的树、建立新的树

  • 卸载树时,对应的DOM节点也会被销毁

    • componentWillUnmount()
  • 建立新树时,对应的DOM节点会被创建、插入到DOM中

    • UNSAFE_componentWillMount()

    • componentDidMount()

    <div>
      <Counter />
    </div>
    
    <span>
      <Counter />
    </span>
    
    // React会销毁Couter组件并重新装载
    

对比同一类型的元素

  • 保留DOM节点,仅比较更新改变的属性

    // React知道仅修改DOM元素上的className属性
    <div className="before" title="stuff" />
    
    <div className="after" title="stuff" />
        
    // 仅更新color
    <div style={{color: 'red', fontWeight: 'bold'}} />
    
    <div style={{color: 'green', fontWeight: 'bold'}} />
    

处理完当前节点后,对子节点进行递归

对比同类型的组件元素

  • 组件更新时,组件实例会保持不变,以保持不同的渲染时保持state一致
  • 更新组件实例的props以保证更新
    • UNSAFE_componentWillReceiveProps()
    • ``UNSAFE_componentWillUpdate()`
    • componentDidUpdate()
  • 调用render()方法,diff算法利用旧结果和新结果递归

子节点递归

  • key属性用于匹配新旧树上的子元素
  • 当基于下标的组件进行重新排序时,非受控组件的state可能会相互篡改

Render Props

指在React组件之间使用一个值为函数的prop共享代码

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

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 动态决定要渲染的内容,
          而不是给出一个 <Mouse> 渲染结果的静态表示
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

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

使用Render prop实现大多数HOC

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

React.PureComponent

在Render方法里创建函数,使用render prop会抵消使用React.PureCompont带来的优势。原因在于:浅比较总会得到false(render会生成一个新函数作为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>
    );
  }
}

静态类型检查

Flow略过,简单看下ts。

Create React App忠使用TS

npx create-react-app my-app --template typescript

// To add TypeScript to an existing Create React App project,
// first install it:
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
// next
// rename any file to be TS file
// e.g. src/index.js to src/index.tsx

非受控组件

很多时候,我们使用受控组件来处理表单数据,表单数据由React管理;另一种替代方案是,表单数据由DOM节点来处理,也就是非受控组件。

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

默认值defaultValue

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

文件输入

React中<input type="file" />始终是个非受控组件

class FileInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.fileInput = React.createRef();
  }
  handleSubmit(event) {
    event.preventDefault();
    alert(
      `Selected file - ${this.fileInput.current.files[0].name}`
    );
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Upload file:
          <input type="file" ref={this.fileInput} />
        </label>
        <br />
        <button type="submit">Submit</button>
      </form>
    );
  }
}

const root = ReactDOM.createRoot(
  document.getElementById('root')
);
root.render(<FileInput />);

其他未写模块

  • 不使用ES6
  • 不使用JSX
  • 严格模式
  • PropTypes
  • Web Components

相关待读

posted @ 2022-08-08 22:34  沧浪浊兮  阅读(48)  评论(0编辑  收藏  举报