React总结

React 是一个用于构建用户界面的 JavaScript 库 

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

State & 生命周期

React 的组件可以定义为 class 或函数的形式。如需定义 class 组件,需要继承 React.Component。在 React.Component 的子类中有个必须定义的 render() 函数

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

挂载

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

注意:

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

更新

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

 注意:

下述方法即将过时,在新代码中应该避免使用它们

卸载

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

错误处理

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

render():

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

注意

如果 shouldComponentUpdate() 返回 false,则不会调用 render()

constructor() 

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

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

 注意

避免将 props 的值复制给 state!这是一个常见的错误:

constructor(props) {
 super(props);
 // 不要这样做
 this.state = { color: props.color };
}
如此做毫无必要(你可以直接使用 this.props.color),
同时还产生了 bug(更新 prop 中的 color 时,并不会影响 state)。

只有在你刻意忽略 prop 更新的情况下使用。
此时,应将 prop 重命名为 initialColor 或 defaultColor。

 componentDidMount()

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

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

componentDidUpdate()

componentDidUpdate(prevProps, prevState, snapshot)

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

当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。

componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

你也可以在 componentDidUpdate() 中直接调用 setState(),但请注意它必须被包裹在一个条件语句里,正如上述的例子那样进行处理,否则会导致死循环。

注意

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

componentWillUnmount() 

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

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

shouldComponentUpdate()

shouldComponentUpdate(nextProps, nextState)

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

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

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

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

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

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

static getDerivedStateFromProps()

static getDerivedStateFromProps(props, state)

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

此方法适用于罕见的用例,即 state 的值在任何时候都取决于 props。例如,实现 <Transition> 组件可能很方便,该组件会比较当前组件与下一组件,以决定针对哪些组件进行转场动画。

派生状态会导致代码冗余,并使组件难以维护。 确保你已熟悉这些简单的替代方案:

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

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

什么时候使用派生 state

getDerivedStateFromProps 的存在只有一个目的:让组件在 props 变化时更新 state。上一个 blog 展示了一些示例,比如 props 的 offset 变化时,修改当前的滚动方向根据 props 变化加载外部数据

我们没有提供很多示例,因为有保守使用派生 state 这个规则。大部分使用派生 state 导致的问题,不外乎两个原因:1,直接复制 props 到 state 上;2,如果 props 和 state 不一致就更新 state。下面的示例包含了这两种情况。

  • 如果你只是为了缓存(memoize)基于当前 props 计算后的结果的话,你就没必要使用派生 state。尝试一下 memoization?
  • 如果只是用来保存 props 或者和当前 state 比较之后不一致后更新 state,那你的组件应该是太频繁的更新了 state。请继续阅读。

过时的生命周期方法

UNSAFE_componentWillMount() 在挂载之前被调用。它在 render() 之前调用,因此在此方法中同步调用 setState() 不会触发额外渲染。通常,我们建议使用 constructor() 来初始化 state。

避免在此方法中引入任何副作用或订阅。如遇此种情况,请改用 componentDidMount()

此方法是服务端渲染唯一会调用的生命周期函数。

UNSAFE_componentWillReceiveProps()

UNSAFE_componentWillReceiveProps(nextProps)

注意

此生命周期之前名为 componentWillReceiveProps。该名称将继续使用至 React 17。可以使用 rename-unsafe-lifecycles codemod 自动更新你的组件。

注意:

使用此生命周期方法通常会出现 bug 和不一致性:

对于其他使用场景,请遵循此博客文章中有关派生状态的建议

UNSAFE_componentWillReceiveProps() 会在已挂载的组件接收新的 props 之前被调用。如果你需要更新状态以响应 prop 更改(例如,重置它),你可以比较 this.props 和 nextProps 并在此方法中使用 this.setState() 执行 state 转换。

请注意,如果父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法。如果只想处理更改,请确保进行当前值与变更值的比较。

挂载过程中,React 不会针对初始 props 调用 UNSAFE_componentWillReceiveProps()。组件只会在组件的 props 更新时调用此方法。调用 this.setState() 通常不会触发 UNSAFE_componentWillReceiveProps()


UNSAFE_componentWillUpdate()

UNSAFE_componentWillUpdate(nextProps, nextState)

注意

此生命周期之前名为 componentWillUpdate。该名称将继续使用至 React 17。可以使用 rename-unsafe-lifecycles codemod 自动更新你的组件。

当组件收到新的 props 或 state 时,会在渲染之前调用 UNSAFE_componentWillUpdate()。使用此作为在更新发生之前执行准备更新的机会。初始渲染不会调用此方法。

注意,你不能此方法中调用 this.setState();在 UNSAFE_componentWillUpdate() 返回之前,你也不应该执行任何其他操作(例如,dispatch Redux 的 action)触发对 React 组件的更新

通常,此方法可以替换为 componentDidUpdate()。如果你在此方法中读取 DOM 信息(例如,为了保存滚动位置),则可以将此逻辑移至 getSnapshotBeforeUpdate() 中。

注意

如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()

forceUpdate()

component.forceUpdate(callback)

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

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

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

React 生命周期分为三种状态 1. 初始化 2.更新 3.销毁

image

  1. componentWillMount()

    组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次,此时可以修改state
    在渲染前调用,在客户端也在服务端。

  2. render()

    react最重要的步骤,创建虚拟dom,进行diff算法,更新dom树都在此进行。此时就不能更改state了。

componentDidMount()

组件渲染之后调用,只调用一次。
在第一次渲染后调用,只在客户端。之后组件已经生成了对应的DOM结构,可以通过**thi.getDOMNode()**来进行访问。

可以在这个方法中调用****setTimeout, setInterval或者发送****AJAX请求等操作(防止异步操作阻塞UI)。

更新阶段

componentWillReceiveProps(nextProps)

组件初始化时不调用,组件接受新的props时调用。
使用componentWillReceiveProps的时候,不要去向上分发,调用父组件的相关setState方法,否则会成为死循环

在组件接收到一个新的 prop (更新后)时被调用。这个方法在初始化render时不会被调用。
shouldComponentUpdate(nextProps, nextState)

react性能优化非常重要的一环。组件接受新的state或者props时调用,返回一个布尔值

可以在你确认不需要更新组件时使用,节省大量性能,尤其是在dom结构复杂的时候
componentWillUpdata(nextProps, nextState)

组件初始化时不调用,只有在组件将要更新时才调用,此时可以修改state

render()

组件重新渲染

componentDidUpdate()

组件初始化时不调用,组件更新完成后调用,此时可以获取dom节点。

卸载阶段

componentWillUnmount()
组件将要卸载时调用,一些事件监听和定时器需要在此时清除。

Mounting中为组件的挂载过程

componentWillMount组件挂载之前
render组件的渲染方法
componentDidMount组件挂载完成执行
Updation中为组件数据发生变化的过程

componentWillReceiveProps:props独有
子组件从父组件接受的props数据发生变动这时此方法才会被触发。

shouldComponentUpdata:props和states共有
是否要更新数据?需要一个返回值true继续执行下面的生命周期,false就会终止当前组件数

componentWillUpdate 组件将要更新
render组件的重新渲染
componentDidUpdata组件完成更新
Unmounting组件卸载

componentWillUnmount 组件销毁的时候触发

React 中 render() 的目的

每个React组件强制要求必须有一个 render()。它返回一个 React 元素,是原生 DOM 组件的表示。render()必须每次调用时都返回相同的结果

State & 生命周期

受控组件和非受控组件


受控组件:利用组件状态控制表单的值。在 HTML 中,表单元素(如<input>、 <textarea> 和 <select>)通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
非受控组件:利用节点操作控制表单的值。表单数据将交由 DOM 节点来处理。
受控组件    非受控组件
1. 没有维持自己的状态    1. 保持着自己的状态
2.数据由父组件控制    2.数据由 DOM 控制
3. 通过 props 获取当前值,然后通过回调通知更改    3. Refs 用于获取其当前值

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

访问 Refs

当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

const node = this.myRef.current;

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

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

以下例子说明了这些差异。

为 DOM 元素添加 ref

以下代码使用 ref 去存储 DOM 节点的引用:

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

为 class 组件添加 Ref

如果我们想包装上面的 CustomTextInput,来模拟它挂载之后立即被点击的操作,我们可以使用 ref 来获取这个自定义的 input 组件并手动调用它的 focusTextInput 方法:

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

  componentDidMount() {
    this.textInput.current.focusTextInput();  }

  render() {
    return (
      <CustomTextInput ref={this.textInput} />    );
  }
}

请注意,这仅在 CustomTextInput 声明为 class 时才有效:

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

Refs 与函数组件

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

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

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

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

不管怎样,你可以在函数组件内部使用 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>
  );
}

将 DOM Refs 暴露给父组件

在极少数情况下,你可能希望在父组件中引用子节点的 DOM 节点。通常不建议这样做,因为它会打破组件的封装,但它偶尔可用于触发焦点或测量子 DOM 节点的大小或位置。

虽然你可以向子组件添加 ref,但这不是一个理想的解决方案,因为你只能获取组件实例而不是 DOM 节点。并且,它还在函数组件上无效。

如果你使用 16.3 或更高版本的 React, 这种情况下我们推荐使用 ref 转发Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。关于怎样对父组件暴露子组件的 DOM 节点,在 ref 转发文档中有一个详细的例子。

如果你使用 16.2 或更低版本的 React,或者你需要比 ref 转发更高的灵活性,你可以使用这个替代方案将 ref 作为特殊名字的 prop 直接传递。

可能的话,我们不建议暴露 DOM 节点,但有时候它会成为救命稻草。注意这个方案需要你在子组件中增加一些代码。如果你对子组件的实现没有控制权的话,你剩下的选择是使用 findDOMNode(),但在严格模式 下已被废弃且不推荐使用。

回调 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。在 componentDidMount 或 componentDidUpdate 触发前,React 会保证 refs 一定是最新的。

你可以在组件间传递回调形式的 refs,就像你可以传递通过 React.createRef() 创建的对象 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 节点。

什么是高阶组件(HOC)
高阶组件是重用组件逻辑的高级方法,它是一个纯函数,是一种源于 React 的组件模式

高阶组件可以接受子组件提供的任何动态,但不会修改或复制其输入组件中的任何行为,是“纯(Pure)”组件。
高阶组件形式:

属性代理式
反向继承式
高阶组作用

代码重用,逻辑和引导抽象
渲染劫持
状态抽象和控制
Props 控制
38、什么是纯组件
纯(Pure) 组件是可以编写的最简单、最快的组件。它们可以替换任何只有 render() 的组件。这些组件增强了代码的简单性和应用的性能。

React 中 key 的重要性是什么?
key 是用于识别唯一的 Virtual DOM 元素及其驱动 UI 的相应数据
key 必须是唯一的数字或字符串

作用

通过回收 DOM 中当前所有的元素来帮助 React 优化渲染。
React 可以利用唯一的key来重新排序元素而不是重新渲染它们。这可以提高应用程序的性

hooks:

Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。Hook 使你在无需修改组件结构的情况下复用状态逻辑。Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)而并非强制按照生命周期划分。class 不能很好的压缩,并且会使热重载出现不稳定的情况。因此,我们想提供一个使代码更易于优化的 API。Hook 在不编写 class 的情况下使用 state 以及其他的 React 特性 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,

Hook 是向下兼容的

动机:

1.在组件之间复用状态逻辑很难

使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑【由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”】

2.复杂组件变得难以理解

组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。

3.难以理解的 class

class 不能很好的压缩,并且会使热重载出现不稳定的情况。Hook 使你在非 class 的情况下可以使用更多的 React 特性

State Hook:

import React, { useState } from 'react';

function Example() {
  // 声明一个新的叫做 “count” 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。useState 唯一的参数就是初始 state。

Hook 和函数组件

复习一下, React 的函数组件是这样的:

const Example = (props) => {
  // 你可以在这使用 Hook
  return <div />;
}

或是这样:

function Example(props) {
  // 你可以在这使用 Hook
  return <div />;
}

你之前可能把它们叫做“无状态组件”。但现在我们为它们引入了使用 React state 的能力,所以我们更喜欢叫它”函数组件”。

Hook 在 class 内部是起作用的。但你可以使用它们来取代 class 。

Hook 是什么?

Hook 本质就是 JavaScript 函数

在新示例中,首先引入 React 中 useState 的 Hook

import React, { useState } from 'react';
function Example() {
  // ...
}

Hook 是什么? Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState 是允许你在 React 函数组件中添加 state 的 Hook。稍后我们将学习其他 Hook。

什么时候我会用 Hook? 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其转化为 class。现在你可以在现有的函数组件中使用 Hook。

声明 State 变量

在 class 中,我们通过在构造函数中设置 this.state 为 { count: 0 } 来初始化 count state 为 0

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {      count: 0    };  }

在函数组件中,我们没有 this,所以我们不能分配或读取 this.state。我们直接在组件中调用 useState Hook:

import React, { useState } from 'react';

function Example() {
  // 声明一个叫 “count” 的 state 变量  const [count, setCount] = useState(0);

 调用 useState 方法的时候做了什么? 它定义一个 “state 变量”。我们的变量叫 count, 但是我们可以叫他任何名字,比如 banana。这是一种在函数调用时保存变量的方式 —— useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。

useState 需要哪些参数? useState() 方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了 0 作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用 useState() 两次即可。)

  • 不要在循环、条件或嵌套函数中调用Hook,必须始终在React函数的顶层使用Hook
  • 只能在React函数式组件自定义Hook中使用Hook
  • useState 方法的返回值是什么? 返回值为:当前 state 以及更新 state 的函数。这就是我们写 const [count, setCount] = useState() 的原因。这与 class 里面 this.state.count 和 this.setState 类似,唯一区别就是你需要成对的获取它们。
  • 既然我们知道了 useState 的作用,我们的示例应该更容易理解了:

    import React, { useState } from 'react';
    
    function Example() {
      // 声明一个叫 "count" 的 state 变量  const [count, setCount] = useState(0);

    我们声明了一个叫 count 的 state 变量,然后把它设为 0。React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用 setCount 来更新当前的 count

    读取 State

    当我们想在 class 中显示当前的 count,我们读取 this.state.count

      <p>You clicked {this.state.count} times</p>

    在函数中,我们可以直接用 count:

      <p>You clicked {count} times</p>

    更新 State

    在 class 中,我们需要调用 this.setState() 来更新 count 值:

      <button onClick={() => this.setState({ count: this.state.count + 1 })}>    Click me
      </button>

    在函数中,我们已经有了 setCount 和 count 变量,所以我们不需要 this:

      <button onClick={() => setCount(count + 1)}>    Click me
      </button>

    总结

    现在让我们来仔细回顾一下学到的知识,看下我们是否真正理解了。

     1:  import React, { useState } from 'react'; 2:
     3:  function Example() {
     4:    const [count, setCount] = useState(0); 5:
     6:    return (
     7:      <div>
     8:        <p>You clicked {count} times</p>
     9:        <button onClick={() => setCount(count + 1)}>10:         Click me
    11:        </button>
    12:      </div>
    13:    );
    14:  }
  • 第一行: 引入 React 中的 useState Hook。它让我们在函数组件中存储内部 state。
  • 第四行: 在 Example 组件内部,我们通过调用 useState Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。我们把变量命名为 count,因为它存储的是点击次数。我们通过传 0 作为 useState 唯一的参数来将其初始化为 0。第二个返回的值本身就是一个函数。它让我们可以更新 count 的值,所以我们叫它 setCount
  • 第九行: 当用户点击按钮后,我们传递一个新的值给 setCount。React 会重新渲染 Example 组件,并把最新的 count 传给它。
  • 提示:方括号有什么用?

    你可能注意到我们用方括号定义了一个 state 变量

      const [count, setCount] = useState(0);

这种 JavaScript 语法叫数组解构。它意味着我们同时创建了 fruit 和 setFruit 两个变量,fruit 的值为 useState 返回的第一个值,setFruit 是返回的第二个值。它等价于下面的代码:

提示:使用多个 state 变量

将 state 变量声明为一对 [something, setSomething] 也很方便,因为如果我们想使用多个 state 变量,它允许我们给不同的 state 变量取不同的名称:

function ExampleWithManyStates() {
  // 声明多个 state 变量
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

 你不必使用多个 state 变量。State 变量可以很好地存储对象和数组,因此,你仍然可以将相关数据分为一组。然而,不像 class 中的 this.setState,更新 state 变量总是替换它而不是合并它

使用 Effect Hook

Effect Hook 可以让你在函数组件中执行副作用操作

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:  useEffect(() => {    // Update the document title using the browser API    document.title = `You clicked ${count} times`;  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

提示

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdate 和 componentWillUnmount 这三个函数的组合。

无需清除的 effect

有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。

使用 class 的示例

在 React 的 class 组件中,render 函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作。

这就是为什么在 React class 中,我们把副作用操作放到 componentDidMount 和 componentDidUpdate 函数中。回到示例中,这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {    document.title = `You clicked ${this.state.count} times`;  }  componentDidUpdate() {    document.title = `You clicked ${this.state.count} times`;  }
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

注意,在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。

这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。

现在让我们来看看如何使用 useEffect 执行相同的操作。

使用 Hook 的示例

我们在本章节开始时已经看到了这个示例,但让我们再仔细观察它:

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {    document.title = `You clicked ${count} times`;  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect 做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

为什么在组件内部调用 useEffect 将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后每次更新之后都会执行。(我们稍后会谈到如何控制它。)你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。传递给 useEffect 的函数在每次渲染中都会有所不同,这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。我们将在本章节后续部分更清楚地了解这样做的意义。

提示

与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。

需要清除的 effect

之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。

使用 Class 的示例

在 React class 中,你通常会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。例如,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {    ChatAPI.subscribeToFriendStatus(      this.props.friend.id,      this.handleStatusChange    );  }  componentWillUnmount() {    ChatAPI.unsubscribeFromFriendStatus(      this.props.friend.id,      this.handleStatusChange    );  }  handleStatusChange(status) {    this.setState({      isOnline: status.isOnline    });  }
  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

你会注意到 componentDidMount 和 componentWillUnmount 之间相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。

注意

眼尖的读者可能已经注意到了,这个示例还需要编写 componentDidUpdate 方法才能保证完全正确。我们先暂时忽略这一点,本章节中后续部分会介绍它。

使用 Hook 的示例

如何使用 Hook 编写这个组件。

你可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect 的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {    function handleStatusChange(status) {      setIsOnline(status.isOnline);    }    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);    // Specify how to clean up after this effect:    return function cleanup() {      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    };  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 在执行当前 effect 之前对上一个 effect 进行清除。我们稍后将讨论为什么这将助于避免 bug以及如何在遇到性能问题时跳过此行为

需要清除的 effect

之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。

使用 Class 的示例

在 React class 中,你通常会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。例如,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {    ChatAPI.subscribeToFriendStatus(      this.props.friend.id,      this.handleStatusChange    );  }  componentWillUnmount() {    ChatAPI.unsubscribeFromFriendStatus(      this.props.friend.id,      this.handleStatusChange    );  }  handleStatusChange(status) {    this.setState({      isOnline: status.isOnline    });  }
  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

你会注意到 componentDidMount 和 componentWillUnmount 之间相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。

注意

眼尖的读者可能已经注意到了,这个示例还需要编写 componentDidUpdate 方法才能保证完全正确。我们先暂时忽略这一点,本章节中后续部分会介绍它。

使用 Hook 的示例

如何使用 Hook 编写这个组件。

你可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect 的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {    function handleStatusChange(status) {      setIsOnline(status.isOnline);    }    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);    // Specify how to clean up after this effect:    return function cleanup() {      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    };  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 在执行当前 effect 之前对上一个 effect 进行清除。我们稍后将讨论为什么这将助于避免 bug以及如何在遇到性能问题时跳过此行为

小结

了解了 useEffect 可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数:

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

其他的 effect 可能不必清除,所以不需要返回。

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

effect Hook 使用同一个 API 来满足这两种情况。

提示: 使用多个 Effect 实现关注点分离

使用 Hook 其中一个目的就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。下述代码是将前述示例中的计数器和好友在线状态指示器逻辑组合在一起的组件:

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...

可以发现设置 document.title 的逻辑是如何被分割到 componentDidMount 和 componentDidUpdate 中的,订阅逻辑又是如何被分割到 componentDidMount 和 componentWillUnmount 中的。而且 componentDidMount 中同时包含了两个不同功能的代码。

那么 Hook 如何解决这个问题呢?就像你可以使用多个 state 的 Hook 一样,你也可以使用多个 effect。这会将不相关逻辑分离到不同的 effect 中:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。

解释: 为什么每次更新的时候都要运行 Effect

如果你已经习惯了使用 class,那么你或许会疑惑为什么 effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。让我们看一个实际的例子,看看为什么这个设计可以帮助我们创建 bug 更少的组件。

本章节开始时,我们介绍了一个用于显示好友是否在线的 FriendStatus 组件。从 class 中 props 读取 friend.id,然后在组件挂载后订阅好友的状态,并在卸载组件的时候取消订阅:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

但是当组件已经显示在屏幕上时,friend prop 发生变化时会发生什么? 我们的组件将继续展示原来的好友状态。这是一个 bug。而且我们还会因为取消订阅时使用错误的好友 ID 导致内存泄露或崩溃的问题。

在 class 组件中,我们需要添加 componentDidUpdate 来解决这个问题:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {    // 取消订阅之前的 friend.id    ChatAPI.unsubscribeFromFriendStatus(      prevProps.friend.id,      this.handleStatusChange    );    // 订阅新的 friend.id    ChatAPI.subscribeToFriendStatus(      this.props.friend.id,      this.handleStatusChange    );  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

忘记正确地处理 componentDidUpdate 是 React 应用中常见的 bug 来源。

现在看一下使用 Hook 的版本:

function FriendStatus(props) {
  // ...
  useEffect(() => {
    // ...
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

它并不会受到此 bug 影响。(虽然我们没有对它做任何改动。)

并不需要特定的代码来处理更新逻辑,因为 useEffect 默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。为了说明这一点,下面按时间列出一个可能会产生的订阅和取消订阅操作调用序列:

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // 运行第一个 effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // 运行下一个 effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // 运行下一个 effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect

此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。

提示: 通过跳过 Effect 进行性能优化

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

这是很常见的需求,所以它被内置到了 useEffect 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

上面这个示例中,我们传入 [count] 作为第二个参数。这个参数是什么作用呢?如果 count 的值是 5,而且我们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。

当渲染时,如果 count 的值更新成了 6,React 将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。

对于有清除操作的 effect 同样适用:

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅

未来版本,可能会在构建时自动添加第二个参数。

注意:

注意:

如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。参阅文档,了解更多关于如何处理函数以及数组频繁变化时的措施内容。

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。

如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入 [] 作为第二个参数更接近大家更熟悉的 componentDidMount 和 componentWillUnmount 思维模式,但我们有更好的方式来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得额外操作很方便。

我们推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

我们看到了 Hook 如何解决简介章节中动机部分提出的问题。我们也发现 effect 的清除机制可以避免 componentDidUpdate 和 componentWillUnmount 中的重复,同时让相关的代码关联更加紧密,帮助我们避免一些 bug。我们还看到了我们如何根据 effect 的功能分隔他们,这是在 class 中无法做到的。        

 Hook 规则

只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序

如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部

  useEffect(function persistForm() {
    // 👍 将条件判断放置在 effect 中
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

目前为止,在 React 中有两种流行的方式来共享组件之间的状态逻辑: render props 和高阶组件,现在让我们来看看 Hook 是如何在让你不增加组件的情况下解决相同问题的。 

提取自定义 Hook

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。 例如,下面的 useFriendStatus 是我们第一个自定义的 Hook:

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

此处并未包含任何新的内容——逻辑是从上述组件拷贝来的。与组件中一致,请确保只在自定义 Hook 的顶层无条件地调用其他 Hook。

与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头,这样可以一眼看出其符合 Hook 的规则

使用自定义 Hook

我们一开始的目标是在 FriendStatus 和 FriendListItem 组件中去除重复的逻辑,即:这两个组件都想知道好友是否在线。

现在我们已经把这个逻辑提取到 useFriendStatus 的自定义 Hook 中,然后就可以使用它了:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);
  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

这段代码等价于原来的示例代码吗?等价,它的工作方式完全一样。如果你仔细观察,你会发现我们没有对其行为做任何的改变,我们只是将两个函数之间一些共同的代码提取到单独的函数中。自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。

自定义 Hook 必须以 “use” 开头吗?必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则

在两个组件中使用相同的 Hook 会共享 state 吗?不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

自定义 Hook 如何获取独立的 state?每次调用 Hook,它都会获取独立的 state。由于我们直接调用了 useFriendStatus,从 React 的角度来看,我们的组件只是调用了 useState 和 useEffect。 正如我们在之前章节了解到的一样,我们可以在一个组件中多次调用 useState 和 useEffect,它们是完全独立的。

提示:在多个 Hook 之间传递信息

由于 Hook 本身就是函数,因此我们可以在它们之间传递信息。

我们将使用聊天程序中的另一个组件来说明这一点。这是一个聊天消息接收者的选择器,它会显示当前选定的好友是否在线:

const friendList = [
  { id: 1, name: 'Phoebe' },
  { id: 2, name: 'Rachel' },
  { id: 3, name: 'Ross' },
];

function ChatRecipientPicker() {
  const [recipientID, setRecipientID] = useState(1);  const isRecipientOnline = useFriendStatus(recipientID);
  return (
    <>
      <Circle color={isRecipientOnline ? 'green' : 'red'} />      <select
        value={recipientID}
        onChange={e => setRecipientID(Number(e.target.value))}
      >
        {friendList.map(friend => (
          <option key={friend.id} value={friend.id}>
            {friend.name}
          </option>
        ))}
      </select>
    </>
  );
}

我们将当前选择的好友 ID 保存在 recipientID 状态变量中,并在用户从 <select> 中选择其他好友时更新这个 state。

由于 useState 为我们提供了 recipientID 状态变量的最新值,因此我们可以将它作为参数传递给自定义的 useFriendStatus Hook:

  const [recipientID, setRecipientID] = useState(1);
  const isRecipientOnline = useFriendStatus(recipientID);

如此可以让我们知道当前选中的好友是否在线。当我们选择不同的好友并更新 recipientID 状态变量时,useFriendStatus Hook 将会取消订阅之前选中的好友,并订阅新选中的好友状态。

 Hook API 索引

Hook 能否覆盖 class 的所有使用场景?

我们给 Hook 设定的目标是尽早覆盖 class 的所有使用场景。目前暂时还没有对应不常用的 getSnapshotBeforeUpdategetDerivedStateFromError 和 componentDidCatch 生命周期的 Hook 等价写法,但我们计划尽早把它们加进来。

Hook 会替代 render props 和高阶组件吗?

通常,render props 和高阶组件只渲染一个子节点。我们认为让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderItem 属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。

从 Class 迁移到 Hook

生命周期方法要如何对应到 Hook?

  • constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState
  • getDerivedStateFromProps:改为 在渲染时 安排一次更新。
  • shouldComponentUpdate:详见 下方 React.memo.
  • render:这是函数组件体本身。
  • componentDidMountcomponentDidUpdatecomponentWillUnmountuseEffect Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。
  • getSnapshotBeforeUpdatecomponentDidCatch 以及 getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会被添加。

性能优化

我可以在更新时跳过 effect 吗?

可以的。参见 条件式的发起 effect。注意,忘记处理更新常会 导致 bug,这也正是我们没有默认使用条件式 effect 的原因。

在依赖列表中省略函数是否安全?

一般来说,不安全。

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp);  }

  useEffect(() => {
    doSomething();
  }, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)}

要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要在 effect 内部 去声明它所需要的函数。 这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值:

function Example({ someProp }) {
  useEffect(() => {
    function doSomething() {
      console.log(someProp);    }

    doSomething();
  }, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)}

如果这样之后我们依然没用到组件作用域中的任何值,就可以安全地把它指定为 []

useEffect(() => {
  function doSomething() {
    console.log('hello');
  }

  doSomething();
}, []); // ✅ 在这个例子中是安全的,因为我们没有用到组件作用域中的 *任何* 值

根据你的用例,下面列举了一些其他的办法。

纯函数有什么特点

  • 不修改传入的参数
  • 固定输入有固定输出(不会随时间改变改变输出值)
  • 无状态。线程安全。不需要线程同步。
  • 纯函数相互调用组装起来的函数,还是纯函数。
  • 应用程序或者运行环境(Runtime)可以对纯函数的运算结果进行缓存,运算加快速度
  • 类组件和函数组件之间有什么区别
  • 类组件有this,有生命周期,有状态state
    函数组件没有this,没有生命周期,没有状态state
    函数组件的性能比类组件的性能要高

    因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。

    注意

    所有 React 组件都必须是纯函数,并禁止修改其自身 props
    React是单项数据流,父组件改变了属性,那么子组件视图会更新
    对于类组件
    属性 props 是外界传递过来的,状态 state 是组件本身的,状态可以在组件中任意修改
    组件的属性和状态改变都会更新视图

  • 60、React 中的 useState() 是什么
    react hooks的状态钩子

    61、state 和 props有什么区别
    state 和 props都是普通的JavaScript对象,都具有影响渲染输出的信息。

    区别

    props
    是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,
    具有可读性和不变性,
    只能通过外部组件主动传入新的 props 来重新渲染子组件,否则子组件的 props 以及展现形式不会改变。
    state
    是用于组件保存、控制以及修改自己的状态
    只能在 constructor 中初始化,
    是组件的私有属性,不可通过外部访问和修改,
    只能通过组件内部的 this.setState 来修改,修改 state 属性会导致组件的重新渲染。
    62、关于 setState()
    如果需要修改this.state中的数据 必须调用this.setstate这个方法

    书写方案1:this.setstate({},()=>{})
    书写方案2:this.setstate(()=({}),()=>{})

  • React 加入 Hooks 的意义是什么?为什么 React 要加入Hooks 这一特性?
    为了解决一些component问题:

    组件之间的逻辑状态难以复用
    大型复杂的组件很难拆分
    Class语法的使用不友好
    比如说:

    类组件可以访问生命周期,函数组件不能
    类组件可以定义维护state(状态),函数组件不可以
    类组件中可以获取到实例化后的this,并基于这个this做一些操作,而函数组件不可以
    mixins:变量作用于来源不清、属性重名、Mixins引入过多会导致顺序冲突
    HOC和Render props:组件嵌套过多,不易渲染调试、会劫持props,会有漏洞
    有了Hooks:

    Hooks 就是让你不必写class组件就可以用state和其他的React特性;


  • 也可以编写自己的hooks在不同的组件之间复用vue和react的区别是什么?

VUE和REACT的区别

  • 1、监听数据变化的实现原理不同

    Vue是响应式的通过getter/setter以及一些函数的劫持,能精确知道数据变化。

    React默认是通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染。为什么React不精确监听数据变化呢?这是因为Vue和React设计理念上的区别,Vue使用的是可变数据,而React更强

  • React一直不支持双向绑定提倡的是单向数据流,称之为onChange/setState()模式。视图改变需要通过this.setState来变化数据

  • 模板渲染方式的不同

    在表层上,模板的语法不同,React是通过JSX(JSX 语法的出现允许我们在 javascript 中书写 HTML 代码)渲染模板。而Vue是通过模板系统的HTML语法进行渲染,Vue 是把 HTML,CSS,JavaScript 组合到一起,用各自的处理方式,Vue 有单文件组件,可以把 HTML、CSS、JS 写到一个文件中,HTML 提供了模板引擎来处理。但其实这只是表面现象,毕竟React并不必须依赖JSX。

  • 渲染过程不同

  • react 的性能优化需要手动去做,而Vue的性能优化是自动的

    Vue可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树

    React在应用的状态被改变时,全部子组件都会重新渲染。通过shouldComponentUpdate这个生命周期方法可以进行控制,但Vue将此视为默认的优化

  • 7、框架本质不同

    Vue本质是MVVM框架,由MVC发展而来;

    React是前端组件化框架,由后端组件化发展而来。

  • 如上所说的 Vue 的响应式机制也有问题,当 state 特别多的时候,Watcher 会很多,会导致卡顿,所以大型应用(状态特别多的)一般用 React,更加可控。可对于易用性来说,VUE 是更容易上手的,对于项目来说新人更容易接手。

  • 总结:react使用jsx模式渲染页面,vue使用模板引擎HTML渲染页面,可以将html,script,css写在一个文件里面。。react单项数据流,试图改变需要使用this.setState改变数据。Vue是响应式设计。react使用diff算法改变视图,VUE使用数据劫持getter,setter改变数据。性能优化,react需要使用shouldComponentUpdate来优化,VUE已经默认优化了

MVC模式

MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。

  • Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO。它也可以带有逻辑,在数据变化时更新控制器。
  • View(视图) - 视图代表模型包含的数据的可视化
  • Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。

React 事件绑定原理 

React事件并没有原生的绑定在真实的DOM上,而是使用了行为委托方式实现事件机制。

行为委托的实质就是将子元素事件的处理委托给父级元素处理。React会将所有的事件都绑定在最外层(document),使用统一的事件监听,并在冒泡阶段处理事件。React并不是将click事件绑在该div的真实DOM上,而是document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装并交由真正的处理函数运行。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。
另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault

React并没有使用原生的浏览器事件,而是在基于Virtual DOM的基础上实现了合成事件(SyntheticEvent),事件处理程序接收到的是SyntheticEvent的实例

react事件绑定原理

合成事件与原生事件的区别

1. 写法不同,合成事件是驼峰写法,而原生事件是全部小写
2. 执行时机不同,合成事件全部委托到document上,而原生事件绑定到DOM元素本身
3. 合成事件中可以是任何类型,比如this.handleClick这个函数,而原生事件中只能是字符串

React在事件处理的优点

1. 几乎所有的事件代理(delegate)到 document ,达到性能优化的目的
2. 对于每种类型的事件,拥有统一的分发函数 dispatchEvent
3. 事件对象(event)是合成对象(SyntheticEvent),不是原生的,其具有跨浏览器兼容的特性
4. react内部事件系统实现可以分为两个阶段: 事件注册、事件分发,几乎所有的事件均委托到document上,而document上事件的回调函数只有一个:ReactEventListener.dispatchEvent,然后进行相关的分发
5. 对于冒泡事件,是在 document 对象的冒泡阶段触发。对于非冒泡事件,例如focus,blur,是在 document 对象的捕获阶段触发,最后在 dispatchEvent 中决定真正回调函数的执行

 我们来看个例子深入了解下,如果我们需要实现一个组件,这个组件点击按钮会显示一个二维码,点击二维码之外的区域可以隐藏二维码,但是点击二维码本身却不会关闭,代码如下:

//代码来源于《深入React技术栈》2.1.4节
class QrCode extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
    this.handleClickQr = this.handleClickQr.bind(this);
    this.state = {
      active: false,
    };
  }
  
  componentDidMount() {
    document.body.addEventListener('click', e => {
      this.setState({
        active: false,
      });
    });
  }

  componentWillUnmount() {
    document.body.removeEventListener('click');
  }
  
  handleClick() {
    this.setState({
      active: !this.state.active,
    });
  }
  
  handleClickQr(e) {
    e.stopPropagation();
  }

  render() {
    return (
      <div className="qr-wrapper">
        <button className="qr" onClick={this.handleClick}>二维码</button>
        <div
          className="code"
          style={{ display: this.state.active ? 'block' : 'none' }}
          onClick={this.handleClickQr}
        >
          <img src="qr.jpg" alt="qr" />
        </div>
      </div>
    );
  }
}

  上面代码从感官上感觉确实可以实现要求的组件,但事实上我们运行上述代码可以发现,点击二维码本身也会导致二维码的隐藏,现在就有意思了,我们来仔细分析一下。 上面代码从感官上感觉确实可以实现要求的组件,但事实上我们运行上述代码可以发现,点击二维码本身也会导致二维码的隐藏,现在就有意思了,我们来仔细分析一下。

 再回到我们刚开始的问题,现在看起来就很没有很费解了,之所以会出现上面的问题是因为我们混用了React的事件机制和DOM原生的事件机制,认为通过:

handleClickQr(e) {
    e.stopPropagation();
}

就能阻止原生的事件传播,其实在事件委托的情形下是不能实现这一点的。当然解决的办法也不复杂,不要将React事件和DOM原生事件混用。

componentDidMount() {
  document.body.addEventListener('click', e => {
    this.setState({
      active: false,
    });
  });
 
  document.querySelector('.code').addEventListener('click', e => {
    e.stopPropagation();
  })
}

componentWillUnmount() {
  document.body.removeEventListener('click');
  document.querySelector('.qr').removeEventListener('click');
}

或者通过事件原件对象中的target进行判断:

componentDidMount() {
  document.body.addEventListener('click', e => {
    if (e.target && e.target.matches('div.code')) {
      return;
    }
 
    this.setState({
      active: false,
    });
  });
}

都可以解决异常关闭的问题。

阻止事件冒泡

事实上阻止合成事件的方法有很多,但是有个原则:阻止合成事件的冒泡不会阻止原生事件的冒泡,但是阻止原生事件的冒泡会阻止合成事件的冒泡。

React合成事件的冒泡并不是真的冒泡,而是节点的遍历。

何时要使用异步组件?如和使用异步组件 

  • 加载大组件的时候
  • 路由异步加载的时候
  • 在项目中某些组件过大影响渲染速度时或者是组件需要懒加载时,可以使用异步组件加载。
  • 异步组件按需加载这些操作都是基于打包工具的特性,比如 Webpack 的 import ~

 1: 基于 React Router 4 和 React 的新特性快速实现异步组件按需加载。

通过 lazy 和 Suspense 组件我们就可以实现异步组件

import React from 'react'

const AsynchronousComp = React.lazy(() => { import('../React_JSX') })

class ReactComp extends React.Component {

  render() {

    /**
     * 1. 使用 React.Lazy 和 import() 来引入组件
     * 2. 使用<React.Suspense></React.Suspense>来做异步组件的父组件,
并使用 fallback 来实现组件未加载完成时展示信息
     * 3. fallback 可以传入html,也可以自行封装一个统一的提示组件
     */

    return (
      <div>
        <p>异步加载</p>
        <p>---------------------</p>
        <React.Suspense
          fallback={
            <div>loading...</div>
          }
        >
          <AsynchronousComp />
        </React.Suspense>
      </div>
    )
  }
}

export default ReactComp;

lazy 接受一个函数作为参数,函数内部使用 import() 方法异步加载组件,加载的结果返回。Suspense 组件的 fallback 属性是必填属性,它接受一个组件,在内部的异步组件还未加载完成时显示,所以我们通常传递一个 Loading 组件给它,如果没有传递的话,就会报错。

所以在使用 React Router 4 的时候,我们可以这样写:

import React, { lazy, Suspense } from 'react';
import { HashRouter, Route, Switch } from 'react-router-dom';

const Index = lazy(() => import('components/Index'));
const List = lazy(() => import('components/List'));

class App extends React.Component {
  render() {
    return <div>
      <HashRouter>
        <Suspense fallback={Loading}>
          <Switch>
            <Route path="/index" exact component={Index}/>
            <Route path="/list" exact component={List}/>
          </Switch>
        </Suspense>
      </HashRouter>
    </div>
  }
}

function Loading() {
  return <div>
    Loading...
  </div>
}

export default App;

这样我们的组件都会在路由匹配的时候才开始加载,Webpack 也会自动代码进行 code split,切割成很多小块,减小了首页的加载时间以及单独一个 js 文件的体积。在工作中已经实践过了,确实好用:

如果没有使用 React v16.6 以上版本,也可以自己实现,

我们可以写一个专门用于异步加载的函数

import React, { Component } from "react";

export default function asyncComponent(importComponent) {
  class AsyncComponent extends Component {
    constructor(props) {
      super(props);

      this.state = {
        component: null
      };
    }

    async componentDidMount() {
      const { default: component } = await importComponent();

      this.setState({
        component: component
      });
    }

    render() {
      const C = this.state.component;

      return C ? <C {...this.props} /> : null;
    }
  }

  return AsyncComponent;
}

我们在这里做了一些事情:

  1. 这个asyncComponent 函数接受一个importComponent 的参数,importComponent 调用时候将动态引入给定的组件。

  2. componentDidMount 我们只是简单地调用importComponent 函数,并将动态加载的组件保存在状态中。

  3. 最后,如果完成渲染,我们有条件地提供组件。在这里我们如果不写null的话,也可提供一个菊花图,代表着组件正在渲染。

使用异步组件

现在让我们使用我们的异步组件,而不是像开始的静态去引入。

import Home from './containers/Home';

我们要用asyncComponent组件来动态引入我们需要的组件。

tip: 别忘记 先 import asyncComponent from './AsyncComponent

const AsyncHome = asyncComponent(() => import('./containers/Home'));

现在让我们回到Notes项目并应用这些更改。

src/Routes.js

import React from 'react';
import { Route, Switch } from 'react-router-dom';
import asyncComponent from './components/AsyncComponent';
import AppliedRoute from './components/AppliedRoute';
import AuthenticatedRoute from './components/AuthenticatedRoute';
import UnauthenticatedRoute from './components/UnauthenticatedRoute';

const AsyncHome     = asyncComponent(() => import('./containers/Home'));
const AsyncLogin    = asyncComponent(() => import('./containers/Login'));
const AsyncNotes    = asyncComponent(() => import('./containers/Notes'));
const AsyncSignup   = asyncComponent(() => import('./containers/Signup'));
const AsyncNewNote  = asyncComponent(() => import('./containers/NewNote'));
const AsyncNotFound = asyncComponent(() => import('./containers/NotFound'));

export default ({ childProps }) => (
  <Switch>
    <AppliedRoute path="/" exact component={AsyncHome} props={childProps} />
    <UnauthenticatedRoute path="/login" exact component={AsyncLogin} props={childProps} />
    <UnauthenticatedRoute path="/signup" exact component={AsyncSignup} props={childProps} />
    <AuthenticatedRoute path="/notes/new" exact component={AsyncNewNote} props={childProps} />
    <AuthenticatedRoute path="/notes/:id" exact component={AsyncNotes} props={childProps} />
    { /* Finally, catch all unmatched routes */ }
    <Route component={AsyncNotFound} />
  </Switch>
);

 Portals

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

ReactDOM.createPortal(child, container)

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

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

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

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

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

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

通过 Portal 进行事件冒泡

尽管 portal 可以被放置在 DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。由于 portal 仍存在于 React 树, 且与 DOM 树 中的位置无关,那么无论其子节点是否是 portal,像 context 这样的功能特性都是不变的。

这包含事件冒泡。一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先

<html>
  <body>
    <div id="app-root"></div>
    <div id="modal-root"></div>
  </body>
</html>

在 #app-root 里的 Parent 组件能够捕获到未被捕获的从兄弟节点 #modal-root 冒泡上来的事件。

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

 react-fiber

setState 既存在异步情况也存在同步情况

1.异步情况 在React事件当中是异步操作(batchedUpdates)将多次state的更新合并为一次更新,提交react性能

2.同步情况 如果是在setTimeout事件或者自定义的dom事件中,都是同步的。跳出batchedUpdates

需要依赖状态更新后的值,对于class component 我们可以在componentDidMount或者componentDidUpdate来执行。对于function component可以在userEffect的回调函数中执行,不同模式下的react答案不一样:

legacy模式:reactDOM.render(<App/>,rootNode):命中batchedUpdates(批量处理)时异步的this.setState触发的更新是异步的。react将对此this.setState合并为一次更新,提交react性能。此时需要将this.setState放到异步中setTimetOut实现同步更新

State 的更新可能是异步的

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。

因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。

例如,此代码可能会无法更新计数器:

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:

// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

上面使用了箭头函数,不过使用普通的函数也同样可以:

// Correct
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

concurrent模式都是异步:ReactDom.createRoot(rooteNode).render(<App/>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值