# React

lastUpdated: 2023-9-01

# React最新的生命周期是怎样的?

React 16之后有三个生命周期被废弃(但并未删除)

  • componentWillMount

  • componentWillReceiveProps

  • componentWillUpdate

官方计划在17版本完全删除这三个函数,只保留UNSAVE_前缀的三个函数,目的是为了向下兼容,但是对于开发者而言应该尽量避免使用他们,而是使用新增的生命周期函数替代它们

目前React 16.8 +的生命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段

挂载阶段:

  • constructor: 构造函数,最先被执行,我们通常在构造函数里初始化state对象或者给自定义方法绑定this

  • getDerivedStateFromProps:static getDerivedStateFromProps(nextProps, prevState),这是个静态方法,当我们接收到新的属性想去修改我们state,可以使用getDerivedStateFromProps

  • render: render函数是纯函数,只返回需要渲染的东西,不应该包含其它的业务逻辑,可以返回原生的DOM、React组件、Fragment、Portals、字符串和数字、Boolean和null等内容

  • componentDidMount: 组件装载之后调用,此时我们可以获取到DOM节点并操作,比如对canvas,svg的操作,服务器请求,订阅都可以写在这个里面,但是记得在componentWillUnmount中取消订阅

更新阶段:

  • getDerivedStateFromProps: 此方法在更新个挂载阶段都可能会调用

  • shouldComponentUpdate:shouldComponentUpdate(nextProps, nextState),有两个参数nextProps和nextState,表示新的属性和变化之后的state,返回一个布尔值,true表示会触发重新渲染,false表示不会触发重新渲染,默认返回true,我们通常利用此生命周期来优化React程序性能

  • render: 更新阶段也会触发此生命周期

  • getSnapshotBeforeUpdate:getSnapshotBeforeUpdate(prevProps, prevState),这个方法在render之后,componentDidUpdate之前调用,有两个参数prevProps和prevState,表示之前的属性和之前的state,这个函数有一个返回值,会作为第三个参数传给componentDidUpdate,如果你不想要返回值,可以返回null,此生命周期必须与componentDidUpdate搭配使用

  • componentDidUpdate:componentDidUpdate(prevProps, prevState, snapshot),该方法在getSnapshotBeforeUpdate方法之后被调用,有三个参数prevProps,prevState,snapshot,表示之前的props,之前的state,和snapshot。第三个参数是getSnapshotBeforeUpdate返回的,如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或计算的过程迁移至 getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统一触发回调或更新状态。

卸载阶段:

  • componentWillUnmount: 当我们的组件被卸载或者销毁了就会调用,我们可以在这个函数里去清除一些定时器,取消网络请求,清理无效的DOM元素等垃圾清理工作 一个查看react生命周期的网站 (opens new window)

Hooks 与 React 生命周期 (opens new window)

# React事件机制

React基于浏览器事件机制实现了一套自己的事件机制,包括:事件注册、事件合成、事件冒泡、事件触发等。

事件代理

React的事件并没有绑定到具体的dom节点上,而是绑定在了document上,然后由统一的事件监听器去监听事件的触发

React在内部维护了一个映射表来记录事件与组件的事件处理函数的对应关系。当某个事件触发时,React根据映射表将时间分派给指定的事件处理函数。当一个组件挂载与卸载时,相应的事件处理函数会自动被添加到事件监听器的内部映射表中或从表中删除。这样做简化了事件处理和回收机制,效率也提升很大。

合成事件(SyntheticEvent)

合成事件是React模拟DOM原生事件的一个事件对象,这些合成事件并没有绑定到对应的真实DOM上,而是通过事件代理的方式,将所有的事件绑定到了document上。其优点如下:

  1. 兼容所有浏览器,兼容性好

  2. 方便React进行统一管理和进行事件处理。对于原生事件来说,浏览器会监听事件是否被触发,当事件触发时会创建一个事件对象,当多个事件被触发时就会创建多个事件对象,这样存在内部分配的问题。对于合成事件来说,有一个专门事件池来管理事件的创建和销毁,当需要使用事件时,就会在事件池中复用对象,事件回调结束后,再销毁事件对象上的属性,以便于下次再复用对象。这样做就不会为每个事件都创建一个事件对象,减少了内存的消耗,提升性能。

SyntheticEvent是React合成事件的基类,定义了合成事件的基础公共属性和方法。

React会根据当前的事件类型来使用不同的合成事件对象,比如单击事件SyntheticMouseEvent,焦点事件SyntheticFocusEvent等,这些事件都继承SyntheticEvent。

合成事件原理

当用户在为onClick添加函数时,React并没有将Click绑定到DOM上 而在document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装交给中间层SyntheticEvent (负责所有事件合成) 然后使用统一的分发函数 dispatchEvent 将封装的事件内容交由真正的处理函数执行

事件注册 React事件注册过程要进行两件事:事件注册;事件存储。

在document上注册事件

在React组件挂载阶段,根据组件内声明的事件类型(onClick、onChange等),在document上通过addEventListener注册事件,并指定统一的回调函数dispatchEvent。

在document上不管注册什么事件,都具有统一的回调函数dispatchEvent。所以对于同一种事件类型,不论在document上注册了几次,最终也只会保留一个有效实例,这样就可以减少内存消耗。

function TestComponent() {
  handleFatherClick=()=>{
		// ...
  }
 
  handleChildClick=()=>{
		// ...
  }
 
  return <div className="father" onClick={this.handleFatherClick}>
	<div className="child" onClick={this.handleChildClick}>child </div>
  </div>
}

在上述代码中,事件类型是onClick,由于React事件代理机制,会指定统一的回调函数dispatchEvent,所以最终只会在document上保留一个click事件,类似document.addEventListener('click', dispatchEvent),可以看出React的事件是在DOM事件流的冒泡阶段被触发执行。

存储事件

React为了在触发事件时可以查到对应的回调去执行,会把组件内的所有事件统一存放到一个对象中(映射表)。首先根据事件类型分类存储,例如click事件相关的统一存储到一个对象中,回调函数的存储采用键值对的形式,key代表组件的唯一标识,value对应的就是事件的回调函数。

React把所有事件和事件类型以及React组件进行了关联,在事件触发的时候根据当前的组件id与事件类型找到对应的回调函数。

事件注册关键步骤

  1. 首先React生成要挂载的组件的虚拟DOM(通过babel对jsx进行词法分析,然后调用React.createElement()方法返回一个对象,这个对象就是虚拟DOM)

  2. 然后处理组件的props,判断props内是否有声明为事件的属性比如 onClick,onChange,这个时候得到事件类型 click,change 和对应的事件处理程序 fn

  3. 将这些事件在document上注册

  4. 在组件挂载完成后,将事件处理函数存储到listenerBank(映射表)中

React的事件注册的关键步骤如下图:

事件触发

React的事件触发只会发生在DOM事件流的冒泡阶段,因为在document上注册时就默认是在冒泡阶段被触发执行。

其大致流程如下:

  1. 触发事件,开始DOM事件流:事件捕获阶段、处于目标阶段、事件冒泡阶段
  2. 当事件冒泡到document时,触发统一的事件回调函数ReactEventListener.dispatchEvent
  3. 根据原生事件对象(nativeEvent)找到事件触发节点对应的组件
  4. 开始事件的合成
  5. 根据当前事件类型生成指定的合成对象
  6. 封装原生事件和冒泡机制
  7. 查找当前元素以及它所有父级
  8. 在listenerBank查找事件回调并合成到event
  9. 批量执行合成事件内的回调函数
  10. 如果没有阻止冒泡,会将继续进行 DOM 事件流的冒泡(从 document 到 window),否则结束事件触发

dispatchEvent 进行事件触发

handleFatherClick=(e)=>{
        console.log('father click');
    }

    handleChildClick=(e)=>{
        console.log('child click');
    }

    render(){
        return <div className="box">
                    <div className="father" onClick={this.handleFatherClick}> father
                        <div className="child" onClick={this.handleChildClick}>child </div>
                    </div>
               </div>
    }

当我们点击child div时,会同时触发father的click事件。

在点击了child div时,浏览器会捕获到这个事件,然后经过冒泡,事件被冒泡到document上,交给统一事件处理函数dispatchEvent对事件进行分发,根据之前在listenerBank存储的键值对找到触发事件的组件,获取到触发这个事件的元素,遍历这个元素的所有父元素,依次对每一级元素进行处理。构造合成事件,将每一级的合成事件存储在eventQueue事件队列中,然后批量执行存储的回调函数。

react事件的执行顺序

事件的执行顺序为原生事件先执行,合成事件再执行。合成事件会冒泡到document上,所以尽量避免原生事件和合成事件混用。如果原生事件阻止冒泡,那么就会导致合成事件不执行。

react事件阻止冒泡的方式

react合成事件不能直接采用return false的方式来阻止浏览器的默认行为,而必须明确调用event.preventDefault()来阻止默认行为。

不能使用event.stopPropagation()来阻止React事件冒泡,必须调用event.preventDefault()来阻止React事件冒泡。

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

  1. 写法不同。原生事件使用全部小写;React事件是用小驼峰
  2. 事件函数处理语法不同。原生事件使用字符串定义事件;React使用函数的形式
  3. 阻止冒泡的方式不同。原生事件可以通过return false或 event.stopPropagation()来阻止冒泡;React事件只能过event.preventDefault()

事件处理函数中的 event 能否异步访问

不能。在包含回调函数的当前事件循环执行完后,所有的event属性都会失效。

function handleClick(event) {
	console.log(event.type) // => 有值
	setTimeout(function() {
		console.log(event.type) // => null
	}, 0)
	this.setState({
		clickEvent: event // => null,因为在回调函数当前事件循环执行完后,所有event会变成null
	})

	this.setState){
		clickEventType: event.type // => 有值
	}
}

所有的原生事件都有对应的合成事件吗?

不是。「几乎」所有事件都代理到了 document,说明有例外,比如audio、video标签的一些媒体事件(如 onplay、onpause 等),是 document 所不具有,这些事件只能够在这些标签上进行事件进行代理,但依旧用统一的入口分发函数(dispatchEvent)进行绑定。

# React受控组件和非受控组件

React是一个非常流行的JavaScript库,用于构建Web应用程序。React中有两种类型的组件,受控组件和非受控组件。在本文中,我们将深入研究这两种组件的区别,优缺点以及如何选择适当的组件类型。

什么是React组件

React组件是一种用于构建用户界面的代码单元。组件可以是类组件或函数组件,它们都可以接收输入并返回输出。React中的组件是可重用的,这意味着您可以在应用程序的不同部分使用相同的组件,以减少重复代码和提高应用程序的可维护性。

React中的组件可以是受控组件或非受控组件。受控组件是一种受React状态控制的组件,而非受控组件则是一种没有受React状态控制的组件。

什么是React受控组件

React受控组件是一种由React状态控制的组件。在受控组件中,组件的值由React状态管理。React状态是一个存储组件数据的JavaScript对象,可以随时更新。当组件的值发生变化时,React状态将更新,组件将重新渲染以反映新值。 例如,下面是一个简单的受控组件,它是一个文本框,它的值由React状态管理:

class ControlledInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = { value: '' };
  }
 
  handleChange(event) {
    this.setState({ value: event.target.value });
  }
 
  render() {
    return (
      <input
        type="text"
        value={this.state.value}
        onChange={(e) => this.handleChange(e)}
      />
    );
  }
}

在上面的代码中,我们创建了一个受控组件ControlledInput。该组件包含一个文本框,它的值由React状态value管理。我们还定义了一个handleChange函数,它在文本框的值发生变化时被调用,并更新React状态value。最后,我们在文本框的value属性中绑定了React状态value,这意味着文本框的值将始终与React状态value保持同步。

什么是React非受控组件

React非受控组件是一种不由React状态控制的组件。在非受控组件中,组件的值不由React状态管理。相反,组件的值由DOM节点直接管理。

例如,下面是一个简单的非受控组件,它是一个文本框,它的值不由React状态管理:

class UncontrolledInput extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
 
  handleClick() {
    console.log('Input value:', this.inputRef.current.value);
  }
 
  render() {
    return (
      <div>
        <input type="text" ref={this.inputRef} />
        <button onClick={() => this.handleClick()}>Click me</button>
      </div>
    );
  }
}

在上面的代码中,我们创建了一个非受控组件UncontrolledInput。该组件包含一个文本框,它的值不由React状态管理。相反,我们使用React.createRef()方法创建了一个名为inputRef的引用。然后,我们将该引用传递给文本框的ref属性,这意味着我们可以使用inputRef.current.value获取文本框的当前值。最后,我们定义了一个handleClick函数,它在点击按钮时被调用,并输出文本框的值。

受控组件和非受控组件的区别

React受控组件和非受控组件之间的最大区别是组件的值是由React状态控制还是由DOM节点控制。

在受控组件中,组件的值由React状态控制。这意味着当组件的值发生变化时,React状态将更新,组件将重新渲染以反映新值。另外,您可以使用React状态控制组件的值进行验证和处理。

在非受控组件中,组件的值由DOM节点控制。这意味着当组件的值发生变化时,它将直接反映在DOM节点上,而不会更新React状态。这使得非受控组件可以更快地更新,但也使得验证和处理组件的值更加困难。

受控组件和非受控组件的优缺点

React受控组件和非受控组件都有自己的优缺点。以下是它们的主要优缺点:

受控组件的优点

容易进行验证和处理:由于组件的值由React状态控制,因此您可以轻松地验证和处理组件的值。例如,您可以使用onChange事件来验证组件的值是否满足特定的条件,并更新React状态以反映验证结果。

有更好的可预测性:由于组件的值由React状态控制,因此组件的行为更容易预测。例如,当组件的值发生变化时,它将始终更新React状态并重新渲染,这使得应用程序更加可预测。

受控组件的缺点

更多的代码:由于组件的值由React状态控制,因此需要编写更多的代码来管理组件的值。这可能会导致代码更难以维护和理解。

性能问题:由于每次组件的值发生变化时,它都会更新React状态并重新渲染,这可能会影响性能。如果您在处理大量数据时使用受控组件,则可能会遇到性能问题。

非受控组件的优点

更快的更新:由于组件的值由DOM节点控制,因此非受控组件可以更快地更新。这使得非受控组件适用于需要高速更新的场景,例如实时搜索框。

更少的代码:由于组件的值不由React状态控制,因此需要编写更少的代码来管理组件的值。这使得代码更易于维护和理解。

非受控组件的缺点

难以进行验证和处理:由于组件的值不由React状态控制,因此难以对其进行验证和处理。例如,如果您需要确保组件的值满足特定条件,则必须使用DOM节点来验证组件的值,并且必须手动更新组件的值。

更少的可预测性:由于组件的值由DOM节点控制,因此组件的行为更难以预测。例如,当组件的值发生变化时,它不会更新React状态并重新渲染,这可能会导致应用程序的行为更加不可预测。

何时使用受控组件和非受控组件

React受控组件和非受控组件都有各自的用途。以下是它们的主要应用场景:

受控组件的应用场景

当需要对组件的值进行验证和处理时,应使用受控组件。例如,当您需要确保文本框的值仅包含数字时,您可以使用受控组件来验证文本框的值,并更新React状态以反映验证结果。

当需要确保组件的值与应用程序的状态同步时,应使用受控组件。例如,当您需要确保文本框的值与应用程序的状态同步时,您可以使用受控组件来更新React状态,并确保组件的值与应用程序的状态同步。

非受控组件的应用场景

当需要高速更新组件的值时,应使用非受控组件。例如,当您需要实现实时搜索框时,您可以使用非受控组件来快速更新文本框的值,并更新搜索结果。

当组件的值不需要与应用程序的状态同步时,应使用非受控组件。例如,当您需要实现一个单选按钮组时,您可以使用非受控组件来更新单选按钮的值,并且不需要将其与应用程序的状态同步。

结论

React受控组件和非受控组件都有各自的优点和缺点,并且应该根据应用程序的需求来选择使用哪种组件。受控组件提供更多的控制和可靠性,但需要编写更多的代码和更多的状态管理。非受控组件提供更快的更新速度和更少的代码,但可能会难以处理和验证组件的值,并且行为更加不可预测。

在开发React应用程序时,应根据具体情况选择使用受控组件或非受控组件。如果需要对组件的值进行验证和处理,并确保组件的值与应用程序的状态同步,则应使用受控组件。如果需要快速更新组件的值,并且组件的值不需要与应用程序的状态同步,则应使用非受控组件。

最后,值得注意的是,React受控组件和非受控组件并不是互相排斥的,它们可以在同一个应用程序中共存。例如,您可以在一个表单中使用受控组件来处理用户输入的数据,并在同一个表单中使用非受控组件来实现自动填充功能。在开发React应用程序时,应选择合适的组件类型来满足具体的需求,并且根据实际情况灵活使用不同类型的组件。

# React懒加载(lazy)实现原理

目录

  1. 代码分割

  2. React的懒加载

    2.1 import() 原理

    2.2 React.lazy 原理

    2.3 Suspense 原理

代码分割

(1)为什么要进行代码分割?

现在前端项目基本都采用打包技术,比如 Webpack,JS逻辑代码打包后会产生一个 bundle.js 文件,而随着我们引用的第三方库越来越多或业务逻辑代码越来越复杂,相应打包好的 bundle.js 文件体积就会越来越大,因为需要先请求加载资源之后,才会渲染页面,这就会严重影响到页面的首屏加载。

而为了解决这样的问题,避免大体积的代码包,我们则可以通过技术手段对代码包进行分割,能够创建多个包并在运行时动态地加载。现在像 Webpack、 Browserify等打包器都支持代码分割技术。

(2)什么时候应该考虑进行代码分割?

这里举一个平时开发中可能会遇到的场景,比如某个体积相对比较大的第三方库或插件(比如JS版的PDF预览库)只在单页应用(SPA)的某一个不是首页的页面使用了,这种情况就可以考虑代码分割,增加首屏的加载速度。

React的懒加载

示例代码:

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.lazy 和 Suspense 共同一起实现了 React 的懒加载,也就是我们常说了运行时动态加载,即 OtherComponent 组件文件被拆分打包为一个新的包(bundle)文件,并且只会在 OtherComponent 组件渲染时,才会被下载到本地。

那么上述中的代码拆分以及动态加载究竟是如何实现的呢?让我们来一起探究其原理是怎样的。

import() 原理

import() 函数是由TS39提出的一种动态加载模块的规范实现,其返回是一个 promise。在浏览器宿主环境中一个 import() 的参考实现如下:

function import(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    const tempGlobal = "__tempModuleLoadingVariable" +     Math.random().toString(32).substring(2);
    script.type = "module";
    script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;
 
    script.onload = () => {
      resolve(window[tempGlobal]);
      delete window[tempGlobal];
      script.remove();
    };
 
    script.onerror = () => {
      reject(new Error("Failed to load module script with URL " + url));
      delete window[tempGlobal];
      script.remove();
    };
 
    document.documentElement.appendChild(script);
  });
}

当 Webpack 解析到该 import() 语法时,会自动进行代码分割。

React.lazy 原理

以下 React 源码基于 16.8.0 版本

React.lazy 的源码实现如下:

export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
  let lazyType = {
    $$typeof: REACT_LAZY_TYPE,
    _ctor: ctor,
    // React uses these fields to store the result.
    _status: -1,
    _result: null,
  };
 
  return lazyType;
}

可以看到其返回了一个 LazyComponent 对象。

而对于 LazyComponent 对象的解析:

...
case LazyComponent: {
  const elementType = workInProgress.elementType;
  return mountLazyComponent(
    current,
    workInProgress,
    elementType,
    updateExpirationTime,
    renderExpirationTime,
  );
}
...
function mountLazyComponent(
  _current,
  workInProgress,
  elementType,
  updateExpirationTime,
  renderExpirationTime,
) { 
  ...
  let Component = readLazyComponentType(elementType);
  ...
}
// Pending = 0, Resolved = 1, Rejected = 2
export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {
  const status = lazyComponent._status;
  const result = lazyComponent._result;
  switch (status) {
    case Resolved: {
      const Component: T = result;
      return Component;
    }
    case Rejected: {
      const error: mixed = result;
      throw error;
    }
    case Pending: {
      const thenable: Thenable<T, mixed> = result;
      throw thenable;
    }
    default: { // lazyComponent 首次被渲染
      lazyComponent._status = Pending;
      const ctor = lazyComponent._ctor;
      const thenable = ctor();
      thenable.then(
        moduleObject => {
          if (lazyComponent._status === Pending) {
            const defaultExport = moduleObject.default;
            lazyComponent._status = Resolved;
            lazyComponent._result = defaultExport;
          }
        },
        error => {
          if (lazyComponent._status === Pending) {
            lazyComponent._status = Rejected;
            lazyComponent._result = error;
          }
        },
      );
      // Handle synchronous thenables.
      switch (lazyComponent._status) {
        case Resolved:
          return lazyComponent._result;
        case Rejected:
          throw lazyComponent._result;
      }
      lazyComponent._result = thenable;
      throw thenable;
    }
  }
}

注:如果 readLazyComponentType 函数多次处理同一个 lazyComponent,则可能进入Pending、Rejected等 case 中。

从上述代码中可以看出,对于最初 React.lazy() 所返回的 LazyComponent 对象,其 _status 默认是 -1,所以 首次渲染 时,会进入 readLazyComponentType 函数中的 default 的逻辑,这里才会真正异步执行 import(url) 操作,由于并未等待,随后会检查模块是否 Resolved,如果已经Resolved了(已经加载完毕)则直接返回 moduleObject.default (动态加载的模块的默认导出),否则将通过 throw 将 thenable 抛出到上层。

为什么要 throw 它?这就要涉及到 Suspense 的工作原理,我们接着往下分析。

Suspense 原理

由于 React 捕获异常并处理的代码逻辑比较多,这里就不贴源码,感兴趣可以去看 throwException 中的逻辑,其中就包含了如何处理捕获的异常。简单描述一下处理过程,React 捕获到异常之后,会判断异常是不是一个 thenable,如果是则会找到 SuspenseComponent ,如果 thenable 处于 pending 状态,则会将其 children 都渲染成 fallback 的值,一旦 thenable 被 resolve 则 SuspenseComponent 的子组件会重新渲染一次。

为了便于理解,我们也可以用 componentDidCatch 实现一个自己的 Suspense 组件,如下:

class Suspense extends React.Component {
  state = {
    promise: null
  }
 
  componentDidCatch(err) {
    // 判断 err 是否是 thenable
    if (err !== null && typeof err === 'object' && typeof err.then === 'function') {
      this.setState({ promise: err }, () => {
        err.then(() => {
          this.setState({
            promise: null
          })
        })
      })
    }
  }
 
  render() {
    const { fallback, children } = this.props
    const { promise } = this.state
    return <>{ promise ? fallback : children }</>
  }
}

小结

至此,我们分析完了 React 的懒加载原理。简单来说,React利用 React.lazy 与 import() 实现了渲染时的动态加载 ,并利用 Suspense 来处理异步加载资源时页面应该如何显示的问题。

# React的useEffect与uselayoutEffect的区别

useEffect和uselayoutEffect都是副作用hooks,两则非常相似,同样都接收两个参数: (1)第一个参数为函数,第二个参数为依赖列表,只有依赖更新时才会执行函数;返回一个函数,当页面刷新的或销毁的时候执行return后的代码;

(2)如果不接受第二个参数,那么在第一次渲染完成之后和每次更新渲染页面的时候,都会调用useEffect的回调函数;

useEffect和 useLayout的主要区别就是他们的执行时机不同,在浏览器中js线程与渲染线程是互斥的,当js线程执行时,渲染线程呈挂起状态,只有当js线程空闲时渲染线程才会执行,将生成的 dom绘制。useLayoutEffect在js线程执行完毕即dom更新之后立即执行,而useEffect是在渲染结束后才执行,也就是说 useLayoutEffect比 useEffect先执行。

# React的请求应该放在哪个生命周期中?

React的异步请求到底应该放在哪个生命周期里,有人认为在componentWillMount中可以提前进行异步请求,避免白屏,其实这个观点是有问题的.

由于JavaScript中异步事件的性质,当您启动API调用时,浏览器会在此期间返回执行其他工作。当React渲染一个组件时,它不会等待componentWillMount它完成任何事情 - React继续前进并继续render,没有办法“暂停”渲染以等待数据到达。

而且在componentWillMount请求会有一系列潜在的问题,首先,在服务器渲染时,如果在 componentWillMount 里获取数据,fetch data会执行两次,一次在服务端一次在客户端,这造成了多余的请求,其次,在React 16进行React Fiber重写后,componentWillMount可能在一次渲染中多次调用.

目前官方推荐的异步请求是在componentDidmount中进行.

如果有特殊需求需要提前请求,也可以在特殊情况下在constructor中请求:

react 17之后componentWillMount会被废弃,仅仅保留UNSAFE_componentWillMount

# setState到底是异步还是同步?

先给出答案: 有时表现出异步,有时表现出同步

setState只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的。 setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。 setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。

你真的理解setState吗? (opens new window)

setState详解 (opens new window)

# Hooks为什么不能写在条件语句或循环语句里

这是因为在 React 中,每次组件重新渲染时,Hooks 都会按照它们出现的顺序重新运行。这意味着,如果你在条件语句或循环语句中使用 Hooks,那么每次循环或条件判断都会重新创建 Hook 的状态和副作用,这会导致无法预测和不稳定的行为

# componentDidCatch

解析React16.0的componentDidCatch方法

这段时间看了下react一些新特性,想说说其中一个。React 16 将提供一个内置函数 componentDidCatch,如果 render() 函数抛出错误,该函数可以捕捉到错误信息,并且可以展示相应的错误信息,这个方法真的很赞!

那么componentDidCatch究竟可以做什么?有什么好处? 当有错误发生时, 我们可以友好地展示 fallback 组件; 可以捕捉到它的子元素(包括嵌套子元素)抛出的异常; 可以复用错误组件; 代码详解:

import React, { Component } from 'react'
export default class App extends Component {
  render() {
    return (
      <div>
        <PointerError>
          <SomeState></SomeState>
        </PointerError>
      </div>
    )
  }
}
class PointerError extends Component {
  // PointerError是错误捕获组件
  constructor(props) {
    super(props)
    this.state = {
      error: false,
      text: ''
    }
  }
  // parseStr(str) {
  // 格式化位置组件错误信息
  //   let res = str.match(/in[^\(]+\(/g)
  //   res = res.map(item => item.slice(3, -2))
  //   console.log('res', res)
  // }
  componentDidCatch(error, info) {
    console.log(error, info)
    alert('错误发生的位置:' + info.componentStack) //错误信息error.message, 错误堆栈error.stack, 组件堆栈info.componentStack
    this.setState({
      error,
      info,
      text: info.componentStack
    })
  }
  render() {
    if (this.state.error) {
      return (
        <div>
          <h1>错误是:{this.state.error.toString()}</h1>
          <h2>错误出现的位置是:{this.state.text}</h2>
          <img src="https://dss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2942945378,442701149&fm=26&gp=0.jpg" />
        </div>
      )
    }
    return this.props.children
  }
}

class SomeState extends Component {
  constructor(props) {
    super(props)
    this.state = { error: false }
  }
  render() {
    throw new Error('我发生了错误') //报错信息
    return (
      <div>
        <div>你已经正确的打开了页面</div>
      </div>
    )
  }
}

上面代码中声明了一个PointerError 组件和一个SomeState 组件,PointerError 就是我们说的错误提示组件,我在其子元素中(也就是SomeState 组件)抛出来一个错误,它内置的componentDidCatch()方法可以帮我们捕捉到错误信息,在控制台打印可看到:

这样的话就可以用一个错误信息页面来代替由于某个组件报错而页面异常了

另一个特性

componentDidCatch 它也是一个包含错误堆栈的 info 对象,这将告诉你组件在哪里失效!

{this.state.info && this.state.info.componentStack}

# React组件通信如何实现?

React组件间通信方式:

父组件向子组件通讯: 父组件可以向子组件通过传 props 的方式,向子组件进行通讯

子组件向父组件通讯: props+回调的方式,父组件向子组件传递props进行通讯,此props为作用域为父组件自身的函数,子组件调用该函数,将子组件想要传递的信息,作为参数,传递到父组件的作用域中

兄弟组件通信: 找到这两个兄弟节点共同的父节点,结合上面两种方式由父节点转发信息进行通信 跨层级通信:Context设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言,对于跨越多层的全局数据通过Context通信再适合不过 发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们可以通过引入event模块进行通信

全局状态管理工具: 借助Redux或者Mobx等全局状态管理工具进行通信,这种工具会维护一个全局状态中心Store,并根据不同的事件产生新的状态

# React有哪些优化性能是手段?

性能优化的手段很多时候是通用的详情见前端性能优化加载篇

# React如何进行组件/逻辑复用?

抛开已经被官方弃用的Mixin,组件抽象的技术目前有三种比较主流:

高阶组件:

属性代理

反向继承

渲染属性

react-hooks

组件复用详解见组件复用

# mixin、hoc、render props、react-hooks的优劣如何?

Mixin的缺陷:

组件与 Mixin 之间存在隐式依赖(Mixin 经常依赖组件的特定方法,但在定义组件时并不知道这种依赖关系)

多个 Mixin 之间可能产生冲突(比如定义了相同的state字段)

Mixin 倾向于增加更多状态,这降低了应用的可预测性(The more state in your application, the harder it is to reason about it.),导致复杂度剧增

隐式依赖导致依赖关系不透明,维护成本和理解成本迅速攀升:

难以快速理解组件行为,需要全盘了解所有依赖 Mixin 的扩展行为,及其之间的相互影响

组价自身的方法和state字段不敢轻易删改,因为难以确定有没有 Mixin 依赖它

Mixin 也难以维护,因为 Mixin 逻辑最后会被打平合并到一起,很难搞清楚一个 Mixin 的输入输出

HOC相比Mixin的优势:

HOC通过外层组件通过 Props 影响内层组件的状态,而不是直接改变其 State不存在冲突和互相干扰,这就降低了耦合度 不同于 Mixin 的打平+合并,HOC 具有天然的层级结构(组件树结构),这又降低了复杂度 HOC的缺陷:

扩展性限制: HOC 无法从外部访问子组件的 State因此无法通过shouldComponentUpdate滤掉不必要的更新,React 在支持 ES6 Class 之后提供了React.PureComponent来解决这个问题

Ref 传递问题: Ref 被隔断,后来的React.forwardRef 来解决这个问题

Wrapper Hell: HOC可能出现多层包裹组件的情况,多层抽象同样增加了复杂度和理解成本

命名冲突: 如果高阶组件多次嵌套,没有使用命名空间的话会产生冲突,然后覆盖老属性

不可见性: HOC相当于在原有组件外层再包装一个组件,你压根不知道外层的包装是啥,对于你是黑盒 Render Props优点:

上述HOC的缺点Render Props都可以解决

Render Props缺陷:

使用繁琐: HOC使用只需要借助装饰器语法通常一行代码就可以进行复用,Render Props无法做到如此简单

嵌套过深: Render Props虽然摆脱了组件多层嵌套的问题,但是转化为了函数回调的嵌套 React Hooks优点:

简洁: React Hooks解决了HOC和Render Props的嵌套问题,更加简洁

解耦: React Hooks可以更方便地把 UI 和状态分离,做到更彻底的解耦

组合: Hooks 中可以引用另外的 Hooks形成新的Hooks,组合变化万千

函数友好: React Hooks为函数组件而生,从而解决了类组件的几大问题:

this 指向容易错误

分割在不同声明周期中的逻辑使得代码难以理解和维护

代码复用成本高(高阶组件容易使代码量剧增)

React Hooks缺陷:

额外的学习成本(Functional Component 与 Class Component 之间的困惑)

写法上有限制(不能出现在条件、循环中),并且写法限制增加了重构成本

破坏了PureComponent、React.memo浅比较的性能优化效果(为了取最新的props和state,每次render()都要重新创建事件处函数)

在闭包场景可能会引用到旧的state、props值

内部实现上不直观(依赖一份可变的全局状态,不再那么“纯”)

React.memo并不能完全替代shouldComponentUpdate(因为拿不到 state change,只针对 props change)

关于react-hooks的评价来源于官方react-hooks RFC

# 你是如何理解fiber的?

React Fiber 是一种基于浏览器的单线程调度算法.

React 16之前 ,reconcilation 算法实际上是递归,想要中断递归是很困难的,React 16 开始使用了循环来代替之前的递归.

Fiber:一种将recocilation(递归 diff),拆分成无数个小任务的算法;它随时能够停止,恢复。停止恢复的时机取决于当前的一帧(16ms)内,还有没有足够的时间允许计算。

# 你对 Time Slice的理解?

时间分片

React 在渲染(render)的时候,不会阻塞现在的线程

如果你的设备足够快,你会感觉渲染是同步的

如果你设备非常慢,你会感觉还算是灵敏的

虽然是异步渲染,但是你将会看到完整的渲染,而不是一个组件一行行的渲染出来

同样书写组件的方式 也就是说,这是React背后在做的事情,对于我们开发者来说,是透明的,具体是什么样的效果呢?

有图表三个图表,有一个输入框,以及上面的三种模式 这个组件非常的巨大,而且在输入框每次**输入东西的时候,就会进去一直在渲染。**为了更好的看到渲染的性能,Dan为我们做了一个表。

我们先看看,同步模式:

同步模式下,我们都知道,我们没输入一个字符,React就开始渲染,当React渲染一颗巨大的树的时候,是非常卡的,所以才会有shouldUpdate的出现,在这里Dan也展示了,这种卡!

我们再来看看第二种(Debounced模式):

Debounced模式简单的来说,就是延迟渲染,比如,当你输入完成以后,再开始渲染所有的变化。 这么做的坏处就是,至少不会阻塞用户的输入了,但是依然有非常严重的卡顿。

切换到异步模式:

异步渲染模式就是不阻塞当前线程,继续跑。在视频里可以看到所有的输入,表上都会是原谅色的。

时间分片正是基于可随时打断、重启的Fiber架构,可打断当前任务,优先处理紧急且重要的任务,保证页面的流畅运行.

# redux的工作流程?

首先,我们看下几个核心概念:

Store:保存数据的地方,你可以把它看成一个容器,整个应用只能有一个Store。

State:Store对象包含所有数据,如果想得到某个时点的数据,就要对Store生成快照,这种时点的数据集合,就叫做State。

Action:State的变化,会导致View的变化。但是,用户接触不到State,只能接触到View。所以,State的变化必须是View导致的。Action就是View发出的通知,表示State应该要发生变化了。

Action Creator:View要发送多少种消息,就会有多少种Action。如果都手写,会很麻烦,所以我们定义一个函数来生成Action,这个函数就叫Action Creator。

Reducer:Store收到Action以后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫做Reducer。Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State。

dispatch:是View发出Action的唯一方法。

然后我们过下整个工作流程:

首先,用户(通过View)发出Action,发出方式就用到了dispatch方法。 然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State

State一旦有变化,Store就会调用监听函数,来更新View。 到这儿为止,一次用户交互流程结束。可以看到,在整个流程中数据都是单向流动的,这种方式保证了流程的清晰。

# react-redux是如何工作的?

Provider: Provider的作用是从最外部封装了整个应用,并向connect模块传递store

connect: 负责连接React和Redux

获取state: connect通过context获取Provider中的store,通过store.getState()获取整个store tree 上所有state

包装原组件: 将state和action通过props的方式传入到原组件内部wrapWithConnect返回一个ReactComponent对象Connect,Connect重新render外部传入的原组件

WrappedComponent,并把connect中传入的mapStateToProps, mapDispatchToProps与组件上原有的props合并后,通过属性的方式传给WrappedComponent

监听store tree变化: connect缓存了store tree中state的状态,通过当前state状态和变更前state状态进行比较,从而确定是否调用this.setState()方法触发Connect及其子组件的重新渲染

# redux与mobx的区别?

两者对比:

redux将数据保存在单一的store中,mobx将数据保存在分散的多个store中

redux使用plain object保存数据,需要手动处理变化后的操作;mobx适用observable保存数据,数据变化后自动处理响应的操作

redux使用不可变状态,这意味着状态是只读的,不能直接去修改它,而是应该返回一个新的状态,同时使用纯函数;mobx中的状态是可变的,可以直接对其进行修改

mobx相对来说比较简单,在其中有很多的抽象,mobx更多的使用面向对象的编程思维;redux会比较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用

mobx中有更多的抽象和封装,调试会比较困难,同时结果也难以预测;而redux提供能够进行时间回溯的开发工具,同时其纯函数以及更少的抽象,让调试变得更加的容易

场景辨析:

基于以上区别,我们可以简单得分析一下两者的不同使用场景.

mobx更适合数据不复杂的应用: mobx难以调试,很多状态无法回溯,面对复杂度高的应用时,往往力不从心.

redux适合有回溯需求的应用: 比如一个画板应用、一个表格应用,很多时候需要撤销、重做等操作,由于redux不可变的特性,天然支持这些操作.

mobx适合短平快的项目: mobx上手简单,样板代码少,可以很大程度上提高开发效率.

当然mobx和redux也并不一定是非此即彼的关系,你也可以在项目中用redux作为全局状态管理,用mobx作为组件局部状态管理器来用.

# redux中如何进行异步操作?

当然,我们可以在componentDidmount中直接进行请求无须借助redux.

但是在一定规模的项目中,上述方法很难进行异步流的管理,通常情况下我们会借助redux的异步中间件进行异步处理.

redux异步流中间件其实有很多,但是当下主流的异步中间件只有两种redux-thunk、redux-saga,当然redux-observable可能也有资格占据一席之地,其余的异步中间件不管是社区活跃度还是npm下载量都比较差了.

# redux异步中间件之间的优劣?

redux-thunk优点:

体积小: redux-thunk的实现方式很简单,只有不到20行代码

使用简单: redux-thunk没有引入像redux-saga或者redux-observable额外的范式,上手简单

redux-thunk缺陷:

样板代码过多: 与redux本身一样,通常一个请求需要大量的代码,而且很多都是重复性质的

耦合严重: 异步操作与redux的action偶合在一起,不方便管理

功能孱弱: 有一些实际开发中常用的功能需要自己进行封装

redux-saga优点:

异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中

action摆脱thunk function: dispatch 的参数依然是一个纯粹的 action (FSA),而不是充满 “黑魔法” thunk function

异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理

功能强大: redux-saga提供了大量的Saga 辅助函数和Effect 创建器供开发者使用,开发者无须封装或者简单封装即可使用

灵活: redux-saga可以将多个Saga可以串行/并行组合起来,形成一个非常实用的异步flow 易测试,提供了各种case的测试方案,包括mock task,分支覆盖等等

redux-saga缺陷:

额外的学习成本: redux-saga不仅在使用难以理解的 generator function,而且有数十个API,学习成本远超redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与redux-observable不同,redux-observable虽然也有额外学习成本但是背后是rxjs和一整套思想

体积庞大: 体积略大,代码近2000行,min版25KB左右

功能过剩: 实际上并发控制等功能很难用到,但是我们依然需要引入这些代码

ts支持不友好: yield无法返回TS类型

redux-observable优点:

功能最强: 由于背靠rxjs这个强大的响应式编程的库,借助rxjs的操作符,你可以几乎做任何你能想到的异步处理

背靠rxjs: 由于有rxjs的加持,如果你已经学习了rxjs,redux-observable的学习成本并不高,而且随着rxjs的升级redux-observable也会变得更强大

redux-observable缺陷:

学习成本奇高: 如果你不会rxjs,则需要额外学习两个复杂的库

社区一般: redux-observable的下载量只有redux-saga的1/5,社区也不够活跃,在复杂异步流中间件这个层面redux-saga仍处于领导地位

关于redux-saga与redux-observable的详细比较可见此链接

# 函数组件 和 类组件 的区别

函数组件 和 类组件

函数组件

函数组件也称无状态组件,顾名思义就是以函数形态存在的 React 组件。 在hooks出现之前,react 中的函数组件通常只考虑负责UI的渲染,没有自身的状态,没有业务逻辑代码,是一个纯函数。下面这个函数组件就是一个纯函数,它的输出只由参数props决定,不受其他任何因素影响。

function DemoFunction(props) {
  const { text } = props
  return (
    <div className="demoFunction">
      <p>{`function 组件所接收到的来自外界的文本内容是:[${text}]`}</p>
    </div>
  );
}

但是这种函数组件一旦我们需要给组件加状 态,那就只能将组件重写为类组件,因为函数组件没有实例,没有生命周期。所以我们说在 hook 之前的函数组件和类组件最大的区别又是状态的有无。

Hook

React Hooks 是从 React 16.8 版本推出的新特性,目的是解决 React 的状态共享以及组件生命周期管理混乱的问题。React Hooks 的出现标志着,React 不会再存在无状态组件的情况,React 将只有类组件和函数组件的概念。

hooks为函数组件提供了状态,也支持在函数组件中进行数据获取、订阅事件解绑事件等等。下面先介绍几个最基本的hook作为基础知识。

useState:通过 useState 为组件提供状态。useState 的参数是 state 的初始值,他只有在组件第一次渲染的时候会生效,他的返回值是一个数组,第一个是 state,第二个是设置state的函数。

const [count, setCount] = useState(0);
// useEffect:副作用。通常在副作用中进行ajax请求,事件的绑定与解绑,设置定时器与清除等等。

  // useEffect第一个参数是一个回调函数,在里面进行业务逻辑代码的书写
  // 第二个参数是依赖项数组,如果数组中的依赖发生变化,那么该副作用就会重新执行,
  // 如果不设置第二个参数,那么当该组件每渲染一次,副作用就会执行一次
  useEffect(() => {
    console.log("useEffect副作用执行");
    //用setTimeout模拟ajax请求
    setTimeout(() => {
      setList(result);
    }, 3000);
  }, [list]);

useCallback:用于缓存函数,第一个参数为要缓存的函数,第二个参数为依赖项数组,如果依赖发生了变化,那么就会生成一个新的函数;否则当组件重新渲染时,不会重新定义这个函数,而是会取缓存。

useMemo:用于缓存函数的返回值,第一个参数为要缓存的函数(注意实际被缓存的是函数被执行过后的值),第二个参数为依赖项数组,如果依赖发生了变化,那么就会重新执行这个函数,得到新的返回值;否则当组件重新渲染时,不会重新执行这个函数,而是直接取被缓存的该函数的返回值。

useCallback,useMemo 都是用作优化函数式组件性能的。

类组件

在 React 中,类组件就是基于 ES6 语法,通过继承 React.component 得到的组件。

class Demo extends React.Component {
  // 初始化类组件的 state
  state = {
    text: "111"
  };
  // 编写生命周期方法 didMount
  componentDidMount() {
    // 省略业务逻辑
  }
  // 编写自定义的实例方法
  changeText = (newText) => {
    // 更新 state
    this.setState({
      text: newText
    });
  };
  // 编写生命周期方法 render
  render() {
    return (
      <div className="demoClass">
        <p>{this.state.text}</p>
        <button onClick={() => this.changeText(222)}>点我修改</button>
      </div>
    );
  }
}

函数组件和类组件区别

表面差异:

类组件有生命周期,函数组件没有

类组件需要继承 Class,函数组件不需要

类组件可以获取实例化的 this,并且基于 this 做各种操作,函数组件不行

类组件内部可以定义并维护 state, 函数组件为无状态组件(可以通过hooks实现) ​ 函数组件相比较类组件,优点是更轻量与灵活,便于逻辑的拆分复用。

函数式组件捕获了渲染时所使用的值,这是两类组件最大的不同。

示例点击查看在线演示

// index.js
import React from "react";
import ReactDOM from "react-dom";

import ProfilePageFunction from './ProfilePageFunction';
import ProfilePageClass from './ProfilePageClass';
class App extends React.Component {
  state = {
    user: 'Dan',
  };
  render() {
    return (
      <>
        <label>
          <b>Choose profile to view: </b>
          <select
            value={this.state.user}
            onChange={e => this.setState({ user: e.target.value })}
          >
            <option value="Dan">Dan</option>
            <option value="Sophie">Sophie</option>
            <option value="Sunil">Sunil</option>
          </select>
        </label>
        <h1>Welcome to {this.state.user}’s profile!</h1>
        <p>
          <ProfilePageFunction user={this.state.user} />
          <b> (function)</b>
        </p>
        <p>
          <ProfilePageClass user={this.state.user} />
          <b> (class)</b>
        </p>
        <p>
          Can you spot the difference in the behavior?
        </p>
      </>
    )
  }
}


const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
ProfilePageClass.js

import React from 'react';

class ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

export default ProfilePage;
ProfilePageFunction.js

import React from 'react';

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

export default ProfilePage;

在函数组件的测试情况下,下拉框中选中 Dan,点击关注按钮,迅速将下拉框切换到Sophie,3秒之后,alert弹窗内容仍然是 Followed Dan 在类组件的测试情况下,重复相同的动作,3秒之后,alert弹窗将会显示 Followed Sophie

此案例中,当父组件的state改变时,子组件进行重新渲染,子组件的props改变;

类组件:

改变props(Dan-Sophie),类组件也改变了值,永远保持一致。

原因:类组件捕获最新的值(永远保持一致)当实例的props属性发生修改时,class组件直接使用this(组件的实例),所以可以直接获取组件最新的props。 函数组件:函数式组件捕获了渲染所用的值

当我改变props时(Dan->Sophie),你会发现函数组件会渲染之前的值Dan这个名字。 原因:函数式组件捕获了渲染所使用的值。在函数组件中,之前的props参数,已经因为javascript闭包的特性,保存在内存之中,无法从外部进行修改(维护多个人的状态)。所以在定时器执行callback时,打印的还是旧值。************

# Comment area