React学习笔记

401 阅读16分钟

核心概念

高级指引

1️⃣ 无障碍

JSX 支持所有 aria-* HTML 属性。虽然大多数 React 的 DOM 变量和属性命名都使用驼峰命名(camelCased),但 aria-* 应该像其在 HTML 中一样使用带连字符的命名法

<input
  type="text"
  aria-label={labelText}
  aria-required="true"
  onChange={onchangeHandler}
  value={inputValue}
  name="name"
/>

2️⃣ 代码分割

打包

使用webpack、Rollup等构建工具,把多文件互相引入合并到一个单文件bundle,整个应用可以一次性加载

import

最佳引入代码的方式

React.lazy

在组件首次渲染时,再导入相关的包

import React, { Suspense, lazy } from 'react';
const OtherComponent = lazy(() => import('./OtherComponent'));

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

fallback 属性接受任何在组件加载过程中你想展示的 React 元素

<div>
  <Suspense fallback={<div>Loading...</div>}>
    <section>
      <OtherComponent />
      <AnotherComponent />
    </section>
  </Suspense>
</div>

在外层包裹一层异常捕获边界,可增强用户体验

3️⃣ Context

组件间跨层级的数据传递方式

何时使用?

共享数据,避免中间元素传递

使用context的顾虑

context会使组件的可复用性变差,有时候比context更好的操作:

  • 组件组合
  • 把组件作为参数传递

API

React.createContext
const MyContext = React.createContext(defaultValue);
Context.Provider
<MyContext.Provider value={/* 某个值 */}>
  • 可以嵌套,但是消费者只能取到距离自身最近的value
  • Provider 及其内部 consumer 组件都不受制于shouldComponentUpdate 函数
  • ❗️不要在value中使用对象,最好存放于state中,提高性能
Class.contextType
// 也可写成以下形式
static contextType = MyContext;
  • 订阅最近的value
Context.Consumer
<MyContext.Consumer>
  {context => {/* 基于 context 值进行渲染*/}}
</MyContext.Consumer>
  • 可嵌套使用
Context.displayName

DevTools 中将显示为 MyDisplayName

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

4️⃣ 错误边界

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

❗️无法捕获:

  • 事件:可使用JavaScript中的try-catch
  • 异步代码
  • 服务器渲染
  • 自身组件抛出的错误(仅捕获子组件错误)

如果一个 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; 
  }
}

5️⃣ Refs转发

对于可重用的组件库高阶组件非常有用。

Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。

// 使用 React.forwardRef 来获取传递给它的 ref,然后转发到它渲染的 DOM button
const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 创建一个React ref并向下传递
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
// button挂载完成后,ref.current 将指向 <button> DOM 节点

这样,使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样

在HOC组件中使用

❗️如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。我们可以使用 React.forwardRef API 明确地将 refs 转发到内部的 FancyButton 组件

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

6️⃣ Fragment

将子列表分组,无需添加额外DOM元素。

<React.Fragment>
  <td>Hello</td>
  <td>World</td>
</React.Fragment>

短语法

<>
  <td>Hello</td>
  <td>World</td>
</>

7️⃣ 高阶组件HOC

——参数为组件,返回值为新组件的函数

  • 组件:将 props 转换为 UI
  • 高阶组件:将组件转换为另一个组件

使用HOC解决横切关注点问题

我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。我们可以编写一个创建组件函数。该函数将接受一个子组件作为它的其中一个参数,该子组件将订阅数据作为 prop

const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

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

约定:将不相关的props传递给被包裹的组件

render() {
  // 过滤掉非此 HOC 额外的 props,且不要进行透传
  const { extraProp, ...passThroughProps } = this.props;

  // 将 props 注入到被包装的组件中。
  // 通常为 state 的值或者实例方法。
  const injectedProp = someStateOrInstanceMethod;

  // 将 props 传递给被包装组件
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

约定:最大化可组合性

HOC 通常可以接收多个参数

const ConnectedComment = connect(commentSelector, commentActions)(CommentList);

// 如果你把它分开,就会更容易看出发生了什么

// connect 是一个函数,它的返回值为另外一个函数。
const enhance = connect(commentListSelector, commentListActions);
// 返回值为 HOC,它会返回已经连接 Redux store 的组件
const ConnectedComment = enhance(CommentList);

约定:包装显示名称以便轻松调试

‼️ 注意

不要在render中使用HOC

diff算法中需要判断组件是否相同,决定是否递归更新子树

HOC每次会产生新组件,导致不断卸载-加载新子树

原组件上的静态方法不会跟随到新组件上
// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);

// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true

需要把静态方法拷贝到组件上

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 必须准确知道应该拷贝哪些方法 :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}

或者把静态方法导出,按需导入

// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...单独导出该方法...
export { someFunction };

// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from './MyComponent.js';
refs不会被传递

跟props中的key一样

8️⃣ 与第三方库协同

介绍了jQuery、Backbone

9️⃣ 深入JSX

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

React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)

所以每个使用了jsx的文件,必须引入React(即使没有看到直接调用的地方)。如果你不使用 JavaScript 打包工具而是直接通过 <script> 标签加载 React,则必须将 React 挂载到全局变量中。

参数1:type

  • react组件必须使用大写字母开头的标签,用户自定义的任何类型组件都必须大写字母开头

  • 可以使用点语法来引用一个 React 组件

    <MyComponents.DatePicker color="blue" />
    
  • 动态决定元素类型:类型可以是变量,不可以是表达式

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

参数2:props

  • 可以使用表达式(if、for不是表达式)、字面量

    <MyComponent foo={1 + 2 + 3 + 4} />
    <MyComponent message="hello world" />
    
  • 如果你没给 prop 赋值,它的默认值是 true,通常,我们不建议这样使用,因为它可能与 ES6 对象简写混淆

  • 属性展开

    const props = {firstName: 'Ben', lastName: 'Hector'};
    const { kind, ...other } = props;
    <Greeting {...props} />
    

参数3:childrens

类型
  • 字面量
<div>Hello World</div>
  • jsx元素
<MyContainer>
  <MyFirstComponent />
  <MySecondComponent />
</MyContainer>
  • 表达式
<li>{props.message}</li>
  • 函数❗️
<Repeat numTimes={10}>
  {(index) => <div key={index}>item{index}</div>}
</Repeat>

❗️布尔类型、Null 以及 Undefined 将会忽略

🔟 性能优化

使用生产版本的react

配合打包工具,或者在单文件中引入js库

使用 Chrome Performance 标签分析组件

开发模式下,你可以通过支持的浏览器可视化地了解组件是如何 挂载、更新以及卸载的

使用开发者工具中的分析器对组件进行分析

在Chrome中安装扩展

虚拟化长列表

使用虚拟滚动库,在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建 DOM 节点的数量

避免调停

使用虚拟dom与diff算法,判断是否真的需要更新。

可以通过覆盖生命周期方法 shouldComponentUpdate 来进行提速

shouldComponentUpdate 的作用

返回false,则组件被调停(子组件也不进行判断),默认为true。

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

‼️浅比较针对复杂数据结构时会报错

不可变数据的力量

function updateColorMap(colormap) {
  return Object.assign({}, colormap, {right: 'blue'});
}

updateColorMap 返回了一个新的对象,而不是修改老对象。

❗️Object.assign 是 ES6 的方法,需要 polyfill。

⭕️ 参考 Immerimmutability-helper,帮助你编写高可读性的代码,且不会失去 immutability (不可变性)带来的好处。

1️⃣1️⃣ Portals

——将子节点渲染到父组件以外的DOM节点上,常用于对话框、悬浮卡、提示框

ReactDOM.createPortal(child, container)

❗️注意:挂载、卸载、渲染

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
  }
  componentDidMount() {
    // 挂载-appendChild
    modalRoot.appendChild(this.el);
  }
  componentWillUnmount() {
    // 卸载-removeChild
    modalRoot.removeChild(this.el);
  }
  render() {
    // 渲染-ReactDOM.createPortal
    return ReactDOM.createPortal(
      this.props.children,
      this.el,
    );
  }
}

事件冒泡❗️

portal中触发的事件会一直冒泡到react树的祖先(即使不是DOM树祖先)

1️⃣2️⃣ Profiler

  • 测量渲染一次时间代价
  • 识别渲染较慢的部分

❗️增加额外开支,生产构建时被禁用

// 支持多个、嵌套
render(
  <App>
    <Profiler id="Navigation" onRender={callback}>
      <Navigation {...props} />
    </Profiler>
    <Main {...props} />
  </App>
);

onRender 回调

function onRenderCallback(
  id, // 发生提交的 Profiler 树的 “id”
  phase, // "mount" (如果组件树刚加载) 或者 "update" (如果它重渲染了)之一
  actualDuration, // 本次更新 committed 花费的渲染时间
  baseDuration, // 估计不使用 memoization 的情况下渲染整颗子树需要的时间
  startTime, // 本次更新中 React 开始渲染的时间
  commitTime, // 本次更新中 React committed 的时间
  interactions // 属于本次更新的 interactions 的集合
) {
  // 合计或记录渲染时间。。。
}

1️⃣3️⃣ 不使用ES6

不使用ES6的class关键字,需要引入create-react-class模块,使用createReactClass方法创建类

var createReactClass = require('create-react-class');
var Greeting = createReactClass({
  render: function() {
    return <h1>Hello, {this.props.name}</h1>;
  }
});

❗️与class组件的不同

声明默认属性 - getDefaultProps
Greeting.defaultProps = {
  name: 'Mary'
};
//----------------------
var Greeting = createReactClass({
  getDefaultProps: function() {
    return {
      name: 'Mary'
    };
  },
});
初始化State - getInitialState
var Counter = createReactClass({
  getInitialState: function() {
    return {count: this.props.initialCount};
  },
});
事件自动绑定到this
// 不需要
this.handleClick = this.handleClick.bind(this);

❗️mixins

ES6本身不支持mixin,所以使用ES6 class时不支持mixins。不建议在新代码中使用。

如果完全不同的组件有相似的功能,这就会产生“横切关注点(cross-cutting concerns)“问题。针对这个问题,在使用 createReactClass 创建 React 组件的时候,引入 mixins 功能会是一个很好的解决方案。

var SetIntervalMixin = {
  componentWillMount: function() {
    this.intervals = [];
  },
  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  },
  componentWillUnmount: function() {
    this.intervals.forEach(clearInterval);
  }
};
var TickTock = createReactClass({
  mixins: [SetIntervalMixin], // 使用 mixin
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // 调用 mixin 上的方法
  },
  render: function() {
    return <p>React </p>
  }
});

如果组件拥有多个 mixins,且这些 mixins 中定义了相同的生命周期方法,那么这些生命周期方法都会被调用的。使用 mixins 时,mixins 会先按照定义时的顺序执行最后调用组件上对应的方法。

1️⃣4️⃣ 不使用JSX

当你不想在构建环境中配置有关 JSX 编译时,不在 React 中使用 JSX 会更加方便。可以创建快捷方式,使键入更方便:

const e = React.createElement;

ReactDOM.render(
  e('div', null, 'Hello World'),
  document.getElementById('root')
);

1️⃣5️⃣ 协调‼️‼️‼️

设计动力

更新UI的方法:

  • render时,创建一颗新树
  • update时,创建一颗新树
  • 对比两颗树的差异,同步UI(问题:对比算法代价太高)

react基于以下两个假设,把算法复杂度由O(n^3 )降为O(n):

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

Diffing算法

元素类型不同:拆掉原有的树(包括所有子组件),建立新的树

执行:componentWillUnmount()componentWillMount()componentDidMount()

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>
元素类型相同:保留DOM节点,对比更新属性

执行: componentWillReceiveProps()componentWillUpdate()

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

<div className="after" title="stuff" />
子节点递归

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

  • 在子元素列表末尾新增元素时,变更开销比较小;

  • 在列表头部插入会很影响性能,那么更变开销会比较大

keys

——可以解决以上问题

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

这个 key 不需要全局唯一,但在列表中需要保持唯一。

❗️不推荐用index作为key,会使diff性能变差,也会遇到一些问题

权衡

请谨记协调算法是一个实现细节:React 可以在每个 action 之后对整个应用进行重新渲染,得到的最终结果也会是一样的。在此情境下,重新渲染表示在所有组件内调用 render 方法,这不代表 React 会卸载或装载它们。React 只会基于以上提到的规则来决定如何进行差异的合并。

以下假设没有得到满足,性能会有所损耗:

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

1️⃣6️⃣ Refs & DOM

refs可以访问DOM节点、react元素

何时使用?

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

使用refs

❗️ 创建并附加在react元素上

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

❗️ 访问

const node = this.myRef.current;
// 当ref属性用于HTML元素时,current为底层DOM元素
// 当ref属性用于自定义class组件时,current为组件的挂载实例

‼️你不能在函数组件上使用 ref 属性,因为他们没有实例。你可以在函数组件内部使用 ref 属性,只要它指向一个 DOM 元素或 class 组件。

‼️如果想把refs暴露给父组件,推荐使用Ref转发。Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref

1️⃣7️⃣ Render Props

接受一个函数,返回调用此函数的值

<DataProvider render={mouse => (
  <Cat mouse={mouse} />
)}/>

DataProvider中

render() {
  return (
    <div onMouseMove={this.handleMouseMove}>
      {this.props.render(this.state)}
    </div>
  );
}

解决横切关注点

**render prop 是一个用于告知组件需要渲染什么内容的函数 prop。**你可以使用带有 render prop 的常规组件来实现大多数高阶组件 (HOC)

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

1️⃣8️⃣ 静态类型检查

建议在大型代码库中使用flow或TS来代替PropTypes

Flow

现已不维护,跳过

TypeScript

  • 安装typeScript

    npm install --save-dev typescript
    
  • 配置typescript编译器,tsconfig.json

    npx tsc --init
    
  • 文件拓展名:.ts.tsx

1️⃣9️⃣ 严格模式

Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。

function ExampleApplication() {
  return (
    <div>
      <React.StrictMode>
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>
    </div>
  );
}

目前有助于:

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

2️⃣0️⃣ 使用 PropTypes 进行类型检查

❗️请使用 prop-types库

为了通过类型检查捕获大量错误,大型项目可以使用flow或者ts。不使用这些扩展,可以使用react提供的类型检查功能。

import PropTypes from 'prop-types';

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

Greeting.propTypes = {
  name: PropTypes.string,
  children: PropTypes.element.isRequired //限制:只能有1个元素
};

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

2️⃣1️⃣ 非受控组件

因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。

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

  handleSubmit=(event) =>{
    alert('name' + 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 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value

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

handleSubmit=(event)=>{
  event.preventDefault();
  alert(
    `Selected ${this.fileInput.current.files[0].name}`
  );
}

2️⃣2️⃣ Web Components

在 React 中使用 Web Components

class HelloMessage extends React.Component {
  render() {
    return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
  }
}
  • Web Components 为可复用组件提供了强大的封装;
  • React 则提供了声明式的解决方案,使 DOM 与数据保持同步。

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

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