React面试必备

459 阅读52分钟

1. React基础模块

基础模块 jsx

1. 我们写的 jsx 语法最后变成了什么

React JSX -> React element -> React fiber 流程

当我们在使用React和JSX编写组件时,我们的代码经过了几个重要的阶段:从我们编写的JSX代码,到React元素,最后变为React Fiber节点。下面是这个过程的详细解释:

  1. React JSX:React JSX 是一种 JavaScript 语法扩展,我们通常在 React 中使用 JSX 来描述 UI 应该呈现的样子。其基本语法和HTML非常相似,但实际上它更接近JavaScript。

  2. React Element:当我们编写JSX代码时,Babel编译器会将其编译为 React.createElement() 函数调用,这个函数会返回一个对象,这个对象就是React元素。React元素是对用户界面的一种简洁的描述,包括了组件类型、props和children等信息。

    例如:

 const element = <h1>Hello, world!</h1>;

会被编译为:

   const element = React.createElement("h1", null, "Hello, world!");
  1. React Fiber:当我们将React元素传递给 ReactDOM.render() 时,React开始构建Fiber树,Fiber树是React 16中新引入的核心算法Fiber的数据结构。每个React Element都会对应一个Fiber节点,用来记录组件的类型、状态以及与其他组件的关系等信息。

Fiber的主要目标是使Virtual DOM可以进行增量渲染(incremental rendering),即把渲染工作分解成一块一块的任务,并且能够把这些任务穿插在其他更新和用户交互任务中,从而达到更平滑的用户体验。

总的来说,从React JSX到React Element,再到React Fiber,这是一个将我们以JSX形式编写的代码转换为React能够理解并最终在浏览器中渲染的过程。

2. 如何理解 React element?

当我们编写 JSX 时,如 <div> Hello World </div>,这段 JSX 会被转化为 React Element。React Element 是对屏幕上的 UI 的描述。它包含三个主要属性:type(类型,例如 'div'),props(属性,例如 {children: 'Hello World'}),key(用于优化渲染性能的键,通常在列表渲染时用到)。例如,上面的 JSX 会被转化为如下的 React Element:

{
  type: 'div',
  props: {
    children: 'Hello World',
  },
  key: null,
  ...
}

3.老版本 React 为什么要引入 React

在早期版本的 React 中,我们需要导入整个 React 对象,即使我们并不直接使用它。主要原因是,我们写的 JSX 代码在背后其实是通过 React.createElement 来转换成真正的 JavaScript 对象的,这就需要 React 对象的存在。

例如,我们写的 JSX 如下:

<div>Hello World</div>

在编译过程中,上述 JSX 代码会被转化为如下的形式:

React.createElement('div', null, 'Hello World');

如果在文件顶部没有导入 React,那么在运行时,React.createElement 会因为找不到 React 而报错。

但是从 React 17 开始,新的 JSX Transform(JSX 转换器)可以在编译阶段自动引入必要的函数,而不再依赖于 React 对象。这样,即使我们的代码中没有显式地导入 React,编译器也能正确地处理 JSX 语法。因此,React 17 及其后续版本中,我们编写 JSX 时不再需要显式地 import React from 'react'

新的 JSX Transform 在编译 JSX 时,会引入新的入口函数,比如 jsxjsxs 函数,这些函数都是从 react/jsx-runtime 中导入的,而不再是从 React 对象中获取。具体来说,它会把我们编写的 JSX 代码转化成如下形式:

import { jsx } from 'react/jsx-runtime';

jsx('div', { children: 'Hello, world!' });

在上述代码中,我们可以看到,jsx 函数是直接从 react/jsx-runtime 导入的,不再依赖于 React 对象。因此,我们不再需要在文件头部显式地 import React from 'react'

这种改变带来了几个好处:

  1. 写 JSX 时不再需要导入 React。
  2. 对于那些只使用了 JSX 而没有使用任何其他 React 功能的文件,这能够减小打包大小。
  3. 它也使得未来 React 可以更好地支持像 tree-shaking 这样的代码优化策略。

需要注意的是,这种新的 JSX Transform 是从 React 17 开始引入的,并且需要配合支持新 JSX Transform 的构建工具(如 Babel 7.9.0 及以上版本)使用。

3.如何操作 React Element ,使其变成可控的

React Element 是 React 的基本单元,它是一个用来描述用户界面应该呈现什么样子的简单对象。React Element 是不可变的,一旦创建就不能更改它的子元素或者属性。

然而,我们可以通过状态(State)和属性(Props)来控制一个 React Component 的行为,而 React Component 又可以返回 React Element,从而在一定程度上间接地“控制” React Element。具体来说,我们可以通过改变状态或者属性,使得组件返回不同的 React Element,从而改变用户界面的显示。

例如,我们有一个 Button 组件,它可以返回一个按钮的 React Element,按钮的文本是 Button 组件的一个状态:

import React, { useState } from 'react';

function Button() {
  const [text, setText] = useState('Click me');

  return (
    <button onClick={() => setText('Clicked')}>
      {text}
    </button>
  );
}

export default Button;

在这个例子中,我们通过改变 text 状态,使得 Button 组件返回了不同的 React Element,从而改变了按钮的显示文本。

需要注意的是,虽然我们不能直接操作 React Element,但是我们可以通过 React.cloneElement() 方法来复制一个元素并改变它的属性,这可以在某些场景下提供便利,比如高阶组件(HOC)或者 React Router 等库可能会用到这个方法。然而在日常开发中,我们通常不直接操作 React Element,而是通过状态和属性来控制组件的行为。

4.createElement 和 cloneElement 区别

React.createElementReact.cloneElement 是 React 的两个核心函数,它们都可以用来创建 React Element,但是使用场景和方式是不同的。

React.createElement

React.createElement 是 React 用来创建 React Element 的基础函数。它的基本用法是:

React.createElement(type, [props], [...children])

其中,type 参数指定元素的类型,可以是一个字符串(表示 HTML 标签名)、React 组件(函数组件或类组件),或者是 React Fragment 等。props 参数是一个对象,表示元素的属性。children 参数是剩余的参数,表示元素的子元素。

例如:

const element = React.createElement('div', { className: 'my-div' }, 'Hello, world!');

以上代码创建了一个 div 元素,它的 className 属性是 'my-div',它的子元素是字符串 'Hello, world!'

React.cloneElement

React.cloneElement 用来复制一个元素,并改变它的属性。它的基本用法是:

React.cloneElement(element, [props], [...children])

其中,element 参数是需要被复制的元素。props 参数是一个对象,表示新的属性,这些属性将会和原来元素的属性合并。如果 props 中的属性在原来的元素中已经存在,那么新的属性会覆盖原来的属性。children 参数是剩余的参数,表示新的子元素,它们会替换原来的子元素。

例如:

const originalElement = React.createElement('div', { className: 'my-div' }, 'Hello, world!');
const newElement = React.cloneElement(originalElement, { className: 'my-new-div' }, 'Hello, React!');

以上代码首先创建了一个原始的元素 originalElement。然后,React.cloneElement 创建了一个新的元素 newElement,它的 className 属性被改变成了 'my-new-div',它的子元素被改变成了 'Hello, React!'

总的来说,React.createElementReact.cloneElement 都用来创建 React Element,但是 React.createElement 是创建新的元素,而 React.cloneElement 是复制已经存在的元素并改变它的属性和子元素。

5. React children 操作方法和应用场景?

React 提供了 React.Children API 来处理 this.props.children 这个特殊的 prop。

React.Children 有以下几个主要的方法:

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

  2. React.Children.forEach(children, function):类似于 React.Children.map,但它不返回数组。

  3. React.Children.count(children):返回 children 中的组件总数,等于传递给 map 或 forEach 的回调函数被调用的次数。

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

  5. React.Children.toArray(children):返回 children 的扁平数组,并为每个子节点分配一个 key。

以下是一些应用场景:

React.Children.map:你可能会遍历 children,并对每个 child 做一些变换。例如:

React.Children.map(this.props.children, child => {
  return React.cloneElement(child, { someProp: true });
});

React.Children.count:在需要知道传递给组件的子节点数量时,可以使用这个函数。例如,一个轮播组件可能需要知道其子元素的数量,以便于在底部生成正确数量的导航点。

React.Children.only:当你的组件只能接受一个子节点(而不是一个子节点数组)时,你可能会使用这个函数。例如,一个模态对话框组件可能只接受一个子节点。

React.Children.toArray:这个函数可以用来在需要将 children 转化为数组并执行某些操作,如排序或切片等操作时使用。

总的来说,React.Children API 可以帮助你处理 this.props.children,使你可以更容易地处理和操作 children。

基础模块 state

1. state 更新机制,state 改变到视图更新的流程

在React中,当一个组件的state变化时,会引发一个更新过程,该过程可以概括为以下几个步骤:

  1. 触发更新: 更新的触发通常来源于setState方法的调用,或者是因为父组件导致的props的改变。

  2. 生成新的虚拟DOM: 一旦组件的state或props改变,React将调用render方法生成新的虚拟DOM(这是一个JavaScript对象树,表示新的UI状态)。这阶段称为“Render”阶段。

  3. 对比新旧虚拟DOM: React通过对比新的虚拟DOM和之前的虚拟DOM来确定是否需要实际更新UI。这个过程称为"diffing"。

  4. 计算出最小的必要改变: React计算出如何以最小的操作量更新UI,只改变那些需要改变的部分。这是React的关键优化,使得React能够保持高效。

  5. 应用到实际的DOM: React将计算出的改变应用到实际的DOM上,然后UI将被更新。这个过程叫做"Reconciliation"(调和)阶段。

  6. Commit阶段: 此阶段在真实DOM上应用所有的改变,并调用各种生命周期方法,比如componentDidUpdate和React的useEffect

这个过程使得React在更新UI时能够最小化DOM操作,从而提高性能。同时,由于React控制了整个更新过程,它也能提供很多便利的特性,比如组件的生命周期方法,以及在未来的版本中可能会提供的时间切片和异步渲染等特性。

3. state 批量更新的规则,为什么会被打破

在React中,多次连续的状态更新会被批量处理以优化性能。即,如果你在一个事件处理函数中多次调用setState,React会将它们合并为一次更新,只触发一次重新渲染。

但是,有些情况下,这个批量更新的行为可能会被打破:

  1. Promise和异步函数中的setState:如果你在Promise的.then()回调函数或者异步函数中调用setState,那么这些状态更新将不会被批量处理,每次调用setState都会触发一次重新渲染。这是因为在Promise或异步函数中,setState的调用已经脱离了React的事件处理函数的上下文,React无法控制这些调用的执行时机。

  2. React的生命周期方法中的setState:在某些React的生命周期方法中(如componentDidMountcomponentDidUpdate),连续的setState调用也不会被批量处理。

要注意的是,从React 18开始,引入了新的并发模式(Concurrent Mode)和新的批处理函数startTransition,这些新特性可能会改变setState的批处理行为。具体情况会根据你的React版本和使用的模式有所不同。

要强制React进行批量更新,你可以使用ReactDOM.unstable_batchedUpdates()函数。将你的setState调用放在这个函数的回调函数中,React会将它们批量处理,即使是在Promise或异步函数中。但这是一个未稳定的API,可能会在未来的React版本中改变。

如何结局: 解决办法: React-Dom 中提供了批量更新方法 unstable_batchedUpdates,可以去手动批量更新:

import ReactDOM from 'react-dom'
const { unstable_batchedUpdates } = ReactDOM

setTimeout(()=>{
    unstable_batchedUpdates(()=>{
        this.setState({ number:this.state.number + 1 })
        console.log(this.state.number)
        this.setState({ number:this.state.number + 1})
        console.log(this.state.number)
        this.setState({ number:this.state.number + 1 })
        console.log(this.state.number) 
    })
})

实际工作中,unstable_batchedUpdates 可以用于 Ajax 数据交互之后,合并多次 setState,或者是多次 useState 。

3.setState 是同步还是异步的

在React中,setState不总是同步的,也不总是异步的。其行为依赖于它被调用的上下文。

在React事件处理器(如onClickonChange等)中,setState是异步的。这意味着如果你在事件处理器中连续调用setState,React可能会将这些更新批量处理(batch)以提高性能。在此上下文中,状态更新可能不会立即反映到组件的状态中。

这是一个例子:

handleClick() {
  this.setState({ count: this.state.count + 1 });
  this.setState({ count: this.state.count + 1 });
  // 打印的 count 可能并不是你预期的值,因为 setState 是异步的
  console.log(this.state.count);
}

在这个例子中,两个setState可能会被合并成一个,因此count可能只会增加1而不是2。

但是,在生命周期方法componentDidMountcomponentDidUpdate,或者在原生事件处理器(比如setTimeoutsetInterval的回调函数)中,setState是同步的。在这些情况下,状态更新会立即反映到组件的状态中。

例如:

componentDidMount() {
  this.setState({ count: this.state.count + 1 });
  // 打印的 count 将会是你预期的值,因为在 componentDidMount 中 setState 是同步的
  console.log(this.state.count);
}

在这个例子中,count将会增加1,因为setStatecomponentDidMount中是同步的。

总的来说,setState的行为(是同步还是异步)取决于它被调用的上下文。为了避免混淆,如果你需要在setState后获取最新的状态,你可以使用setState的回调函数,或者使用函数式的setState

// 使用 setState 的回调函数
this.setState({ count: this.state.count + 1 }, () => {
  console.log(this.state.count);  // 这将会打印出最新的 count
});

4.类组件的 setState 和函数组件的 useState 有什么共性和区别

  1. 相同点: 从原理角度出发,setState和 useState 更新视图,底层都调用了 scheduleUpdateOnFiber 方法,而且事件驱动情况下都有批量更新规则。
  2. 不同点:
  • 在不是 pureComponent 组件模式下, setState 不会浅比较两次 state 的值,只要调用 setState,在没有其他优化手段的前提下,就会执行更新。但是 useState 中的 dispatchAction 会默认比较两次 state 是否相同,然后决定是否更新组件。
  • setState 有专门监听 state 变化的回调函数 callback,可以获取最新state;但是在函数组件中,只能通过 useEffect 来执行 state 变化引起的副作用。
  • setState 在底层处理逻辑上主要是和老 state 进行合并处理,而 useState 更倾向于重新赋值。

5.函数组件的状态管理方法 useState + useRef

useState 负责更新,useRef 负责保存状态

useStateuseRef都是React Hooks API的一部分,它们提供了在函数组件中管理和使用状态的方式。

useState

useState是一个用于在函数组件中添加局部状态的Hook。在类组件中,你需要使用this.statethis.setState来管理状态,但在函数组件中,你可以使用useState。这是一个例子:

import React, { useState } from 'react';

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

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

export default Counter;

在这个例子中,我们通过调用useState,定义了一个新的状态变量countuseState返回一个包含两个元素的数组:当前状态和一个更新状态的函数,我们可以通过数组解构来获取它们。

useRef

useRef返回一个可变的ref对象,其.current属性被初始化为传递的参数(initialValue)。返回的对象将在组件的整个生命周期内持续存在。一个常见的用例是管理focus在特定DOM元素之间的移动,另一个用例是保存一个可变的值,这在本质上就像实例字段一样。

import React, { useRef } from 'react';

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

export default TextInputWithFocusButton;

在这个例子中,useRef用于获取input元素的引用,并在按钮点击时将焦点设置到这个元素上。

综上,useStateuseRef提供了强大的工具来在函数组件中管理状态和其他reactive操作。

6.state打印问题

  • state 打印问题:
js
复制代码
handleClick=()=>{
    setTimeout(()=>{
        this.setState({ number: 1  })
    })
    this.setState({ number: 2  })
    ReactDOM.flushSync(()=>{
        this.setState({ number: 3  })
    })
    this.setState({ number: 4  })
}
render(){
   console.log(this.state.number)
   return <button onClick={ this.handleClick } > 点击 </button>
}
  1. 首先,handleClick被触发,this.setState({ number: 2 })被调用。但由于React的setState在事件处理函数中是批量更新的,所以此时state并没有立即更新。
  2. 然后,ReactDOM.flushSync调用,这将使内部的this.setState({ number: 3 })同步执行,立即更新state,并触发重新渲染,所以在界面上会看到打印的是 3。
  3. flushSync后面的this.setState({ number: 4 })再次被调用,但此时state并没有立即更新,等待下一次的批量更新。
  4. 然后,事件处理函数结束,React开始批量更新state,此时number被设置为 4,再次触发渲染,所以在界面上看到的下一个打印值是 4。
  5. 最后,setTimeout里的this.setState({ number: 1 })在下一个事件循环中被调用,此时number被设置为 1,再次触发渲染,所以在界面上看到的最后一个打印值是 1。

基础模块 component

React 组件本质 : UI + update + 常规的类和函数 = React 组件 ,以及 React 对组件的底层处理逻辑。

1. 类组件特点,函数组件特点,两者有什么区别?

类组件来说,底层只需要实例化一次,实例中保存了组件的 state 等状态。对于每一次更新只需要调用 render 方法以及对应的生命周期就可以了。但是在函数组件中,每一次更新都是一次新的函数执行,一次函数组件的更新,里面的变量会重新声明。

2. 组件的通信方式?

  1. props 和 callback
  2. ref
  3. React-redux 或 React-mobx
  4. context
  5. event bus event bus例子

3. 组件的强化方式?

①类组件继承

对于类组件的强化,首先想到的是继承方式,之前开发的开源项目 react-keepalive-router 就是通过继承 React-Router 中的 Switch 和 Router ,来达到缓存页面的功能的。因为 React 中类组件,有良好的继承属性,所以可以针对一些基础组件,首先实现一部分基础功能,再针对项目要求进行有方向的改造强化添加额外功能。它的优势如下:

  1. 可以控制父类 render,还可以添加一些其他的渲染内容;
  2. 可以共享父类方法,还可以添加额外的方法和属性。

但是也有值得注意的地方,就是 state 和生命周期会被继承后的组件修改。像上述 demo 中,Person 组件中的 componentDidMount 生命周期将不会被执行。

②函数组件自定义 Hooks

③HOC高阶组件

4. React 对组件的处理和处理时机?

在 React 调和渲染 fiber 节点的时候,如果发现 fiber tag 是 ClassComponent = 1,则按照类组件逻辑处理,如果是 FunctionComponent = 0 则按照函数组件逻辑处理。当然 React 也提供了一些内置的组件,比如说 Suspense 、Profiler 等。

基础模块 生命周期

React 两个重要阶段,render 阶段和 commit 阶段。

React 在调和( render )阶段会深度遍历 React fiber 树,目的就是发现不同( diff ),不同的地方就是接下来需要更新的地方,对于变化的组件,就会执行 render 函数。

在一次调和过程完毕之后,就到了commit 阶段,commit 阶段会创建修改真实的 DOM 节点。

1. 生命周期的介绍,和用法?

image.png

组件初始化: 执行顺序:constructor -> getDerivedStateFromProps / componentWillMount -> render -> componentDidMount

image.png

更新阶段: componentWillReceiveProps( props 改变) / getDerivedStateFromProp -> shouldComponentUpdate -> componentWillUpdate -> render -> getSnapshotBeforeUpdate -> componentDidUpdate

image.png

销毁阶段:

就比较简单了,在一次调和更新中,如果发现元素被移除,就会打对应的 Deletion 标签 ,然后在 commit 阶段就会调用 componentWillUnmount 生命周期,接下来统一卸载组件以及 DOM 元素。

image.png

2. 生命周期的执行时机? 父与子生命周期的执行顺序?

生命周期的执行时机

3. 函数组件生命周期的代替方案?

React hooks也提供了 api ,用于弥补函数组件没有生命周期的缺陷。其原理主要是运用了 hooks 里面的 useEffect 和 useLayoutEffect

4. useEffect 和 useLayoutEffect 有什么区别,应用场景?

useEffect

useEffect(()=>{
    return destory
},dep)

useEffect 第一个参数 callback, 返回的 destory , destory 作为下一次callback执行之前调用,用于清除上一次 callback 产生的副作用。

第二个参数作为依赖项,是一个数组,可以有多个依赖项,依赖项改变,执行上一次callback 返回的 destory ,和执行新的 effect 第一个参数 callback 。

对于 useEffect 执行, React 处理逻辑是采用异步调用 ,对于每一个 effect 的 callback, React 会向 setTimeout回调函数一样,放入任务队列,等到主线程任务完成,DOM 更新,js 执行完成,视图绘制完毕,才执行。所以 effect 回调函数不会阻塞浏览器绘制视图。

useLayoutEffect:

useLayoutEffect 和 useEffect 不同的地方是采用了同步执行,那么和useEffect有什么区别呢?

  • 首先 useLayoutEffect 是在 DOM 更新之后,浏览器绘制之前,这样可以方便修改 DOM,获取 DOM 信息,这样浏览器只会绘制一次,如果修改 DOM 布局放在 useEffect ,那 useEffect 执行是在浏览器绘制视图之后,接下来又改 DOM ,就可能会导致浏览器再次回流和重绘。而且由于两次绘制,视图上可能会造成闪现突兀的效果。
  • useLayoutEffect callback 中代码执行会阻塞浏览器绘制。

一句话概括如何选择 useEffect 和 useLayoutEffect :修改 DOM ,改变布局就用 useLayoutEffect ,其他情况就用 useEffect 。

5. 废弃的生命周期,为什么要废弃?

React 在 16.3 版本中引入了新的生命周期方法,并且开始逐渐废弃了一些旧的生命周期方法。这个变化的主要原因是 React 的异步渲染模式的引入。在异步渲染模式中,渲染工作可以被暂停、终止或重新开始,这会导致旧的生命周期方法(例如 componentWillMountcomponentWillUpdatecomponentWillReceiveProps)可能会多次调用,或者在渲染过程中调用,而这不是我们预期的行为。

以下是被废弃的生命周期方法及其原因:

  1. componentWillMount:这个方法会在渲染前被调用,但是在异步渲染模式中,它可能会被调用多次或者在渲染过程中调用,这会导致一些意想不到的问题。

  2. componentWillReceiveProps:这个方法在组件接收新的 props 时会被调用,但是在异步渲染中,它可能会在不需要的时候被多次调用,或者在 props 还没有改变的时候就被调用。

  3. componentWillUpdate:这个方法在组件接收新的 props 或者 state 后,渲染前会被调用。但是它在异步渲染中也会有和 componentWillMountcomponentWillReceiveProps 相同的问题。

为了解决这些问题,React 引入了新的生命周期方法,例如 getDerivedStateFromPropsgetSnapshotBeforeUpdate,并且推荐使用 componentDidMountcomponentDidUpdatecomponentWillUnmount 以及 React Hooks(如 useEffect)来替代这些被废弃的生命周期方法。

基础模块 Ref

1. Ref 对象,以及两种 ref 对象创建方法。

useRef 和 createRef

①类组件React.createRef createRef 只做了一件事,就是创建了一个对象,对象上的 current 属性,用于保存通过 ref 获取的 DOM 元素,组件实例等。 createRef 一般用于类组件创建 Ref 对象,可以将 Ref 对象绑定在类组件实例上,这样更方便后续操作 Ref。

注意:不要在函数组件中使用 createRef,否则会造成 Ref 对象内容丢失等情况。

②函数组件 useRef

第二种方式就是函数组件创建 Ref ,可以用 hooks 中的 useRef 来达到同样的效果。 useRef 底层逻辑是和 createRef 差不多,就是 ref 保存位置不相同,类组件有一个实例 instance 能够维护像 ref 这种信息,但是由于函数组件每次更新都是一次新的开始,所有变量重新声明,所以 useRef 不能像 createRef 把 ref 对象直接暴露出去,如果这样每一次函数组件执行就会重新声明 Ref,此时 ref 就会随着函数组件执行被重置,这就解释了在函数组件中为什么不能用 createRef 的原因。

为了解决这个问题,hooks 和函数组件对应的 fiber 对象建立起关联,将 useRef 产生的 ref 对象挂到函数组件对应的 fiber 上,函数组件每次执行,只要组件不被销毁,函数组件对应的 fiber 对象一直存在,所以 ref 等信息就会被保存下来。对于 hooks 原理,后续章节会有对应的介绍。

2. Ref 有什么作用?

  1. 获取组件实例,DOM元素

  2. 组件通信

函数组件 forwardRef + useImperativeHandle

对于函数组件,本身是没有实例的,但是 React Hooks 提供了,useImperativeHandle 一方面第一个参数接受父组件传递的 ref 对象,另一方面第二个参数是一个函数,函数返回值,作为 ref 对象获取的内容。一起看一下 useImperativeHandle 的基本使用。

useImperativeHandle 接受三个参数:

  • 第一个参数 ref : 接受 forWardRef 传递过来的 ref 。
  • 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
  • 第三个参数 deps :依赖项 deps,依赖项更改形成新的 ref 对象。

forwardRef + useImperativeHandle 可以完全让函数组件也能流畅的使用 Ref 通信。其原理图如下所示:

image.png

// 子组件
function Son (props,ref) {
    const inputRef = useRef(null)
    const [ inputValue , setInputValue ] = useState('')
    useImperativeHandle(ref,()=>{
       const handleRefs = {
           onFocus(){              /* 声明方法用于聚焦input框 */
              inputRef.current.focus()
           },
           onChangeValue(value){   /* 声明方法用于改变input的值 */
               setInputValue(value)
           }
       }
       return handleRefs
    },[])
    return <div>
        <input placeholder="请输入内容"  ref={inputRef}  value={inputValue} />
    </div>
}

const ForwarSon = forwardRef(Son)
// 父组件
class Index extends React.Component{
    cur = null
    handerClick(){
       const { onFocus , onChangeValue } =this.cur
       onFocus() // 让子组件的输入框获取焦点
       onChangeValue('let us learn React!') // 让子组件input  
    }
    render(){
        return <div style={{ marginTop:'50px' }} >
            <ForwarSon ref={cur => (this.cur = cur)} />
            <button onClick={this.handerClick.bind(this)} >操控子组件</button>
        </div>
    }
}

流程分析:

  • 父组件用 ref 标记子组件,由于子组件 Son 是函数组件没有实例,所以用 forwardRef 转发 ref。
  • 子组件 Son 用 useImperativeHandle 接收父组件 ref,将让 input 聚焦的方法 onFocus 和 改变 input 输入框的值的方法 onChangeValue 传递给 ref 。
  • 父组件可以通过调用 ref 下的 onFocus 和 onChangeValue 控制子组件中 input 赋值和聚焦。
  1. 保存状态

3. Ref 原理 ?

commitAttachRefcommitDetachRef

4. Ref 获取的三种方式?

① Ref属性是一个字符串。 如上面代码片段,用一个字符串 ref 标记一个 DOM 元素,一个类组件(函数组件没有实例,不能被 Ref 标记)。React 在底层逻辑,会判断类型,如果是 DOM 元素,会把真实 DOM 绑定在组件 this.refs (组件实例下的 refs )属性上,如果是类组件,会把子组件的实例绑定在 this.refs 上。

② Ref 属性是一个函数。 当用一个函数来标记 Ref 的时候,将作为 callback 形式,等到真实 DOM 创建阶段,执行 callback ,获取的 DOM 元素或组件实例,将以回调函数第一个参数形式传入,所以可以像上述代码片段中,用组件实例下的属性 currentDom和 currentComponentInstance 来接收真实 DOM 和组件实例。

③ Ref属性是一个ref对象。

export default function Index(){
    const currentDom = React.useRef(null)
    React.useEffect(()=>{
        console.log( currentDom.current ) // div
    },[])
    return  <div ref={ currentDom } >ref对象模式获取元素或组件</div>
}

5. 如何跨层级传递 ref ?

forwardRef 转发 Ref: forwardRef 的初衷就是解决 ref 不能跨层级捕获和传递的问题。 forwardRef 接受了父级元素标记的 ref 信息,并把它转发下去,使得子组件可以通过 props 来接受到上一层级或者是更上层级的ref。

场景一: 跨层级获取

比如想要通过标记子组件 ref ,来获取孙组件的某一 DOM 元素,或者是组件实例。

// 孙组件
function Son (props){
    const { grandRef } = props
    return <div>
        <div> i am alien </div>
        <span ref={grandRef} >这个是想要获取元素</span>
    </div>
}
// 父组件
class Father extends React.Component{
    constructor(props){
        super(props)
    }
    render(){
        return <div>
            <Son grandRef={this.props.grandRef}  />
        </div>
    }
}
const NewFather = React.forwardRef((props,ref)=> <Father grandRef={ref}  {...props} />)
// 爷组件
class GrandFather extends React.Component{
    constructor(props){
        super(props)
    }
    node = null 
    componentDidMount(){
        console.log(this.node) // span #text 这个是想要获取元素
    }
    render(){
        return <div>
            <NewFather ref={(node)=> this.node = node } />
        </div>
    }
}

6. 父组件如何获取函数子组件内部状态?

在函数组件中,状态通常由 React Hook useState 进行管理,这个状态是私有的,无法从组件外部直接访问。所以,父组件不能直接获取函数子组件的内部状态。

但是,可以通过以下两种方式让父组件能够获取到子组件的状态:

  1. 回调函数: 子组件可以接受一个来自父组件的回调函数作为props,然后在状态改变时调用这个函数,并将新的状态作为参数传递给它。

    这是一个例子:

    import React, { useState } from 'react';
    
    function Child({ onStateChange }) {
      const [count, setCount] = useState(0);
    
      const increment = () => {
        setCount(prevCount => {
          const newCount = prevCount + 1;
          onStateChange(newCount);
          return newCount;
        });
      };
    
      return <button onClick={increment}>Increment</button>;
    }
    
    function Parent() {
      const handleStateChange = (newCount) => {
        console.log("Child's new count is " + newCount);
      };
    
      return <Child onStateChange={handleStateChange} />;
    }
    
    export default Parent;
    
  2. 使用Ref和ImperativeHandle:在某些情况下,你可能想要让父组件能够触发子组件的某些特定行为。对于函数组件,可以使用 useImperativeHandle 钩子以便让父组件能够获取子组件的内部引用,然后通过这个引用调用函数来获取内部状态。

    这是一个例子:

    import React, { useState, useImperativeHandle, forwardRef } from 'react';
    
    const Child = forwardRef((props, ref) => {
      const [count, setCount] = useState(0);
    
      useImperativeHandle(ref, () => ({
        getCount: () => count
      }));
    
      const increment = () => {
        setCount(prevCount => prevCount + 1);
      };
    
      return <button onClick={increment}>Increment</button>;
    });
    
    function Parent() {
      const childRef = React.useRef();
    
      const handleButtonClick = () => {
        console.log("Child's count is " + childRef.current.getCount());
      };
    
      return (
        <div>
          <Child ref={childRef} />
          <button onClick={handleButtonClick}>Get Child Count</button>
        </div>
      );
    }
    
    export default Parent;
    

注意,尽管这些技术可以在必要时使用,但是过度使用它们可能会导致代码难以理解和维护。在可能的情况下,尽量使用 props 和 state 让数据流向更清晰和预测。

7. 函数组件缓存数据

函数组件每一次 render ,函数上下文会重新执行,那么有一种情况就是,在执行一些事件方法改变数据或者保存新数据的时候,有没有必要更新视图,有没有必要把数据放到 state 中。如果视图层更新不依赖想要改变的数据,那么 state 改变带来的更新效果就是多余的。这时候更新无疑是一种性能上的浪费。

这种情况下,useRef 就派上用场了,上面讲到过,useRef 可以创建出一个 ref 原始对象,只要组件没有销毁,ref 对象就一直存在,那么完全可以把一些不依赖于视图更新的数据储存到 ref 对象中。这样做的好处有两个:

  • 第一个能够直接修改数据,不会造成函数组件冗余的更新作用。
  • 第二个 useRef 保存数据,如果有 useEffect ,useMemo 引用 ref 对象中的数据,无须将 ref 对象添加成 dep 依赖项,因为 useRef 始终指向一个内存空间,所以这样一点好处是可以随时访问到变化后的值。

举例:

useRef返回的对象始终保持相同的引用,因此在useEffectuseMemo的依赖数组中使用它并没有什么意义,因为这些引用永远不会改变,这意味着useEffectuseMemo永远不会因为这个依赖项而重新运行。

然而,useRef.current属性可以改变,而不会导致组件重新渲染。这就意味着你可以在useEffectuseMemo内部使用ref.current的值,而不必将ref作为依赖。换句话说,你可以在useEffectuseMemo内部"观察"到ref.current的最新值,即使这个值在回调函数被定义之后改变了。

下面是一个简单的例子:

const MyComponent = () => {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  });

  const doubleCount = useMemo(() => {
    return 2 * countRef.current; // 这将会是最新的 count 值的两倍
  }, []); // 请注意,我们没有将 countRef 作为依赖项

  return <div>{doubleCount}</div>;
};

在这个例子中,每次 count 改变时,我们通过 useEffect 更新了 countRef.current。然后我们定义了一个 useMemo,它返回的 doubleCount 将会是最新的 count 值的两倍。我们没有必要将 countRef 作为 useMemo 的依赖项,因为 countRef 的引用永远不会改变,而 countRef.current 将会始终是 count 的最新值。

基础模块Css in React

css 模块化的几个重要作用,如下

  • 防止全局污染,样式被覆盖
  • 命名混乱: 没有 css 模块化和统一的规范,会使得多人开发,没有一个规范,比如命名一个类名,有的人用驼峰.contextBox,有的人用下划线.context_box,还有的人用中划线.context-box,使得项目不堪入目。
  • css 代码冗余,体积庞大。 这种情况也普遍存在,因为 React 中各个组件是独立的,所以导致引入的 css 文件也是相互独立的,比如在两个 css 中,有很多相似的样式代码,如果没有用到 css 模块化,构建打包上线的时候全部打包在一起,那么无疑会增加项目的体积。

1. React css 模块化方案

  • 第一种 css module ,依赖于 webpack 构建和 css-loader 等 loader 处理,将 css 交给 js 来动态加载。
  • 第二种就是直接放弃 css ,css in js用 js 对象方式写 css ,然后作为 style 方式赋予给 React 组件的 DOM 元素,这种写法将不需要 .css .less .scss 等文件,取而代之的是每一个组件都有一个写对应样式的 js 文件。

2. css module 掌握

css Modules ,使得项目中可以像加载 js 模块一样加载 css ,本质上通过一定自定义的命名规则生成唯一性的 css 类名,从根本上解决 css 全局污染,样式覆盖的问题。对于 css modules 的配置,推荐使用 css-loader,因为它对 CSS Modules 的支持最好,而且很容易使用。

自定义命名规则

  • [path][name]__[local]  -> 开发环境,便于调试。可以直接通过 src-pages-cssModule-style 找到此类名对应的文件。
  • [hash:base64:5]  -> 生产环境,1WHQz 便于生产环境压缩类名。

全局变量

使用 :global(.className) 的语法,声明一个全局类名。凡是这样声明的 class ,都不会被编译成哈希字符串。

.text{
    color: blue;
}
:global(.text_bg) {
    background-color: pink;
}
import style from './style.css'
export default ()=><div>
    <div className={ style.text + ' text_bg'} >验证 CSS Modules </div>
</div>

CSS Modules 还提供一种显式的局部作用域语法:local(.text),等同于.text。

组合样式

CSS Modules 提供了一种 composes组合方式,实现对样式的复用。比如通过 composes 方式的实现上面的效果。

.base{ /* 基础样式 */
    color: blue;
}
.text { /* 继承基础样式 ,增加额外的样式 backgroundColor */
    composes:base;
    background-color: pink;
}

composes 可以将多个 class 类名添加到元素中。composes 还有一个更灵活的方法,支持动态引入别的模块下的类名。比如上述写的 .base 样式在另外一个文件中,完全可以如下这么写:

.text { /* 继承基础样式 ,增加额外的样式 backgroundColor */
    composes:base from './style1.css';  /* base 样式在 style1.css 文件中 */
    background-color: pink;
}

配置 less 和 sass

组合方案

  • 以约定对于全局样式或者是公共组件样式,可以用 .css 文件 ,不需要做 CSS Modules 处理,这样就不需要写 :global 等繁琐语法。
  • 对于项目中开发的页面和业务组件,统一用 scss 或者 less 等做 CSS Module,也就是 css 全局样式 + less / scss CSS Modules 方案。这样就会让 React 项目更加灵活的处理 CSS 模块化。

动态添加class

配合 classNames 库 实现更灵活的动态添加类名。

import classNames from 'classnames'
import Style from './index.less' /*  less css module */
/* 动态加载 */
export default function Index(){
    const [ theme , setTheme  ] = React.useState('light')
    return <div  >
        <button  
         className={ classNames(Style.base, theme === 'light' ? Style.light : Style.dark ) }  
         onClick={ ()=> setTheme(theme === 'light' ? 'dark' : 'light')  }
        > 
           切换主题 
        </button>
    </div>
}

总结

首先 CSS Modules 优点:

  • CSS Modules 的类名都有自己的私有域的,可以避免类名重复/覆盖,全局污染问题。
  • 引入 css 更加灵活,css 模块之间可以互相组合。
  • class 类名生成规则配置灵活,方便压缩 class 名。

CSS Modules 的注意事项:

  • 仅用 class 类名定义 css ,不使用其他选择器。
  • 不要嵌套 css .a{ .b{} } 或者重叠 css .a .b {} 

3. css in js 掌握

CSS IN JS 放弃css ,用 js 对象形式直接写 style:

概念和使用

在 index.js 写 React 组件

import React from 'react'
import Style from './style'

export default function Index(){
    return <div  style={ Style.boxStyle }  >
        <span style={ Style.textStyle }  >hi , i am CSS IN JS!</span>
    </div>
}

在同级目录下,新建 style.js 用来写样式

/* 容器的背景颜色 */
const boxStyle = {
    backgroundColor:'blue',
}
/* 字体颜色 */
const textStyle = {
    color:'orange'
}

export default {
    boxStyle,
    textStyle
}

灵活运用

由于 CSS IN JS 本质上就是运用 js 中对象形式保存样式, 所以 js 对象的操作方法都可以灵活的用在 CSS IN JS上。

  1. 拓展运算符实现样式继承
const baseStyle = { /* 基础样式 */ }

const containerStyle = { 
    ...baseStyle,  // 继承  baseStyle 样式
    color:'#ccc'   // 添加的额外样式
}

  1. 动态添加样式变得更加灵活
/* 暗色调  */
const dark = {
    backgroundColor:'black',
}
/* 亮色调 */
const light = {
    backgroundColor:'white',
}
<span style={ theme==='light' ? Style.light : Style.dark  }  >hi , i am CSS IN JS!</span>

  1. style-components库使用

可以把写好的 css 样式注入到组件中,项目中应用的已经是含有样式的组件。 styled-components的使用

总结

CSS IN JS 特点:

  • CSS IN JS 本质上放弃了 css ,变成了 css in line 形式,所以根本上解决了全局污染,样式混乱等问题。
  • 运用起来灵活,可以运用 js 特性,更灵活地实现样式继承,动态添加样式等场景。
  • 由于编译器对 js 模块化支持度更高,使得可以在项目中更快地找到 style.js 样式文件,以及快捷引入文件中的样式常量。
  • 无须 webpack 额外配置 css,less 等文件类型。

React 优化手段

React 提供了几种控制 render 的方式。我这里会介绍原理和使用。说到对render 的控制,究其本质,主要有以下两种方式:

  • 第一种就是从父组件直接隔断子组件的渲染,经典的就是 memo,缓存 element 对象。
  • 第二种就是组件从自身来控制是否 render ,比如:PureComponent ,shouldComponentUpdate 。

1. React 渲染控制的方法?

缓存 react element ,pureComponent ,Memo ,shouldComponentUpdate。

react element:

可以使用useMemo:

const cacheSomething = useMemo(create,deps)

useMemo:

  • 可以缓存 element 对象,从而达到按条件渲染组件,优化性能的作用。
  • 如果组件中不期望每次 render 都重新计算一些值,可以利用 useMemo 把它缓存起来。
  • 可以把函数和属性缓存起来,作为 PureComponent 的绑定方法,或者配合其他Hooks一起使用。

useMemo原理:

useMemo 会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在,但是 deps 中如果有一项改变,就会重新执行 create ,返回值作为新的值记录到 fiber 对象上。

每次执行 render 本质上 createElement 会产生一个新的 props,这个 props 将作为对应 fiber 的 pendingProps ,在此 fiber 更新调和阶段,React 会对比 fiber 上老 oldProps 和新的 newProp ( pendingProps )是否相等,如果相等函数组件就会放弃子组件的调和更新,从而子组件不会重新渲染;如果上述把 element 对象缓存起来,上面 props 也就和 fiber 上 oldProps 指向相同的内存空间,也就是相等,从而跳过了本次更新。

pureComponent

浅比较 state 和 props 是否相等:

  • 对于 props ,PureComponent 会浅比较 props 是否发生改变,再决定是否渲染组件,所以只有点击 numberA 才会促使组件重新渲染。
  • 对于 state ,如上也会浅比较处理,当上述触发 ‘ state 相同情况’ 按钮时,组件没有渲染。
  • 浅比较只会比较基础数据类型,对于引用类型,比如 demo 中 state 的 obj ,单纯的改变 obj 下属性是不会促使组件更新的,因为浅比较两次 obj 还是指向同一个内存空间,想要解决这个问题也容易,浅拷贝就可以解决,将如上 changeObjNumber 这么修改。这样就是重新创建了一个 obj ,所以浅比较会不相等,组件就会更新了。

❗避免使用箭头函数。不要给是 PureComponent 子组件绑定箭头函数,因为父组件每一次 render ,如果是箭头函数绑定的话,都会重新生成一个新的箭头函数, PureComponent 对比新老 props 时候,因为是新的函数,所以会判断不想等,而让组件直接渲染,PureComponent 作用终会失效。

Memo

  • 通过 memo 第二个参数,判断是否执行更新,如果没有那么第二个参数,那么以浅比较 props 为 diff 规则。如果相等,当前 fiber 完成工作,停止向下调和节点,所以被包裹的组件即将不更新。

  • memo 可以理解为包了一层的高阶组件,它的阻断更新机制,是通过控制下一级 children ,也就是 memo 包装的组件,是否继续调和渲染,来达到目的的。

shouldComponentUpdate

有的时候,把控制渲染,性能调优交给 React 组件本身处理显然是靠不住的,React 需要提供给使用者一种更灵活配置的自定义渲染方案,使用者可以自己决定是否更新当前组件,shouldComponentUpdate 就能达到这种效果。

shouldComponentUpdate 可以根据传入的新的 props 和 state ,或者 newContext 来确定是否更新组件。

2. shallowEqual 浅比较原理

  • 第一步,首先会直接比较新老 props 或者新老 state 是否相等。如果相等那么不更新组件。
  • 第二步,判断新老 state 或者 props ,有不是对象或者为 null 的,那么直接返回 false ,更新组件。
  • 第三步,通过 Object.keys 将新老 props 或者新老 state 的属性名 key 变成数组,判断数组的长度是否相等,如果不相等,证明有属性增加或者减少,那么更新组件。
  • 第四步,遍历老 props 或者老 state ,判断对应的新 props 或新 state ,有没有与之对应并且相等的(这个相等是浅比较),如果有一个不对应或者不相等,那么直接返回 false ,更新组件。 到此为止,浅比较流程结束, PureComponent 就是这么做渲染节流优化的。

3. React 中节流防抖运用

search输入框: 防抖

export default class Index extends React.Component{
    constructor(props){
        super(props)
        this.handleClick = debounce(this.handleClick,500) /* 防抖 500 毫秒 */
        this.handleChange = debounce(this.handleChange,300) /* 防抖 300 毫秒 */
    }
    handleClick= () => {
        console.log('点击事件-表单提交-调用接口')
    }
    handleChange= (e) => {
        console.log('搜索框-请求数据')
    }
    render(){
        return <div>
            <input  placeholder="搜索表单" onChange={this.handleChange}  /><br/>
            <button onClick={ this.handleClick } > 点击 </button>
        </div>
    }
}

频繁触发的事件中,比如监听滚动条滚动:节流

export default function Index(){
    /* useCallback 防止每次组件更新都重新绑定节流函数  */
    const handleScroll = React.useCallback(throttle(function(){
        /* 可以做一些操作,比如曝光上报等 */
    },300))
    return <div className="scrollIndex"  onScroll={handleScroll} >
        <div className="scrollContent" >hello,world</div>
   </div>
}

防抖节流总结:

  • 防抖函数一般用于表单搜索,点击事件等场景,目的就是为了防止短时间内多次触发事件。
  • 节流函数一般为了降低函数执行的频率,比如滚动条滚动。

4. 合理运用状态管理

React 中只要触发 setState 或 useState ,如果没有渲染控制的情况下,组件就会渲染,暴露一个问题就是,如果视图更新不依赖于当前 state ,那么这次渲染也就没有意义。所以对于视图不依赖的状态,就可以考虑不放在 state 中。

对于函数组件,因为不存在组件实例,但是函数组件有 hooks ,所以可以通过一个 useRef 实现同样的效果。

5. 按需引入,减少项目体积

6. 代码分割 lazy ,异步组件 Suspense 及其原理

React.lazyReact.Suspense 实际上是配合使用的,它们都是 React 中用来支持组件的懒加载(lazy loading)的特性。

  • React.lazy: 这是一个函数,它使你可以动态导入一个组件,并将其作为一个可渲染的 React 组件。这个函数的参数是一个动态 import() 表达式,它会返回一个 Promise,当 Promise resolve 时,会得到真正的 React 组件。这样,你可以将组件分割为独立的代码块,这些代码块只有在需要的时候才会被加载。
const LazyComponent = React.lazy(() => import('./LazyComponent'));
  • React.Suspense: 这是一个 React 组件,它使你可以“暂停”组件的渲染,直到某个条件得到满足。当使用 React.lazy 动态导入组件时,你需要使用 React.Suspense 来包裹这个组件。Suspense 组件有一个 fallback prop,用于在等待异步组件加载时显示的内容。
<React.Suspense fallback={<div>Loading...</div>}>
  <LazyComponent />
</React.Suspense>

这里的 <LazyComponent /> 是通过 React.lazy 动态导入的。在 LazyComponent 加载完成之前,会显示 React.Suspensefallback prop 中的内容。

总的来说,React.lazyReact.Suspense 是配合使用的。React.lazy 用于实现组件的懒加载,而 React.Suspense 则用于在等待组件加载时显示一些回退的 UI。

7. diff 算法,合理应用 key

8. 渲染错误边界:componentDidCatch

componentDidCatch 可以捕获异常,它接受两个参数:

  • 1 error —— 抛出的错误。
  • 2 info —— 带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息。

componentDidCatch 中可以再次触发 setState,来降级UI渲染,componentDidCatch() 会在commit阶段被调用,因此允许执行副作用。

 class Index extends React.Component{
   state={
       hasError:false
   }  
   componentDidCatch(...arg){
       uploadErrorLog(arg)  /* 上传错误日志 */
       this.setState({  /* 降级UI */
           hasError:true
       })
   }
   render(){  
      const { hasError } =this.state
      return <div>
          {  hasError ? <div>组件出现错误</div> : <ErrorTest />  }
          <div> hello, my name is alien! </div>
          <Test />
      </div>
   }
}

作用:

  • 可以调用 setState 促使组件渲染,并做一些错误拦截功能。
  • 监控组件,发生错误,上报错误日志。

9. 状态管理工具和 immutable.js 使用

Immutable.js: 是一个由 Facebook 开发的 JavaScript 库,用于创建不可变数据。在 JavaScript 中,我们经常需要修改对象或数组,但这可能会导致意料之外的副作用。Immutable.js 提供了一种创建和管理不可变数据的方法,使得状态的管理更加可预测和安全。

下面是一个 Immutable.js 的使用示例:

import { Map } from 'immutable';  
  
let book = Map({ title'Harry Potter' });  
  
function updateTitle(book, newTitle) {  
  return book.set('title', newTitle);  
}  
  
let updatedBook = updateTitle(book, 'New Title');  
  
console.log(book.get('title'));  // 'Harry Potter'  
console.log(updatedBook.get('title'));  // 'New Title'  

在这个示例中,我们首先创建了一个 Map 对象 book。然后我们尝试修改 book 的 title 属性,但是而不是直接修改 bookupdateTitle 函数返回了一个新的对象 updatedBook。这样,原来的 book 对象就没有被改变,我们可以更容易地管理和跟踪状态的变化。

Immutable.js 提供了一种创建不可变数据的机制,从而为优化状态管理提供了便利。这是如何实现的呢?

1. 轻松追踪变化: 当数据是不可变的,我们可以很容易地追踪数据的变化。任何对数据的更改都会返回一个新的数据结构,因此我们只需要比较数据的引用就能知道它是否发生了变化,而无需进行深层次的差异比较。

2. 纯函数: 在 Redux 中,reducer 必须是纯函数。使用 Immutable.js 可以更容易地保证函数的纯度,因为你不能改变数据,只能通过返回新数据来进行修改。

3. 性能优化: Immutable.js 使用了数据结构共享和懒操作等技术来避免不必要的复制和修改,从而提高性能。例如,当你修改一个 Immutable.js 的 List 时,它只需要复制改变的部分,并与原数据共享没有改变的部分。

4. 更好的合并和比较: Immutable.js 提供了简单而强大的 API 来合并和比较数据,比如 mergemergeDeepisisEqual 等。

5. 配合 React 进行性能优化: 使用 Immutable.js 和 shouldComponentUpdate 可以进行精确的 props 和 state 比较,避免不必要的渲染。

这是一个如何使用 Immutable.js 优化 Redux 的例子:

import { Map } from 'immutable';  
  
const initialState = Map({  
  count0,  
});  
  
function reducer(state = initialState, action) {  
  switch (action.type) {  
    case 'INCREMENT':  
      return state.update('count'value => value + 1);  
    default:  
      return state;  
  }  
}  
  
store.dispatch({ type'INCREMENT' });  

在这个例子中,我们使用 Map 创建了一个不可变的初始状态,并在 reducer 中用 update 方法返回了一个新的状态。这样我们就可以轻松地跟踪 count 的变化,并优化 Redux 的性能。

10. useMemo 和 memo 的区别?

useMemo 是一个 hooks, 参数一接受一个回调函数, 用来缓存组件
useMemo的参数二是一个依赖项,依赖项更新时,才会触发更新
memo 是一个高阶组件,他接受一个组件,在内部进行缓存,如果外部的props没有进行改变,不会触发更新
memo的参数二是一个回调函数, 可以自定义更新的条件,返回true(不更新)或者false(更新)

React 生态

单页面应用是使用一个 html 前提下,一次性加载 js , css 等资源,所有页面都在一个容器页面下,页面切换实质是组件的切换。

1. 两种路由模式 | spa 单页面路由原理

image.png

  • history:  history 是整个 React-router 的核心,里面包括两种路由模式下改变路由的方法,和监听路由变化方法等。
  • react-router:既然有了 history 路由监听/改变的核心,那么需要调度组件负责派发这些路由的更新,也需要容器组件通过路由更新,来渲染视图。所以说 React-router 在 history 核心基础上,增加了 Router ,Switch ,Route 等组件来处理视图渲染。
  • react-router-dom:  在 react-router 基础上,增加了一些 UI 层面的拓展比如 Link ,NavLink 。以及两种模式的根部路由 BrowserRouter ,HashRouter 。

两种路由模式 路由主要分为两种方式,一种是 history 模式,另一种是 Hash 模式。History 库对于两种模式下的监听和处理方法不同,稍后会讲到。 两种模式的样子:

1. history 模式下:http://www.xxx.com/home

① 改变路由

改变路由,指的是通过调用 api 实现的路由跳转,比如开发者在 React 应用中调用 history.push 改变路由,本质上是调用 window.history.pushState 方法。

window.history.pushState

history.pushState(state,title,path)
  • 1. state:一个与指定网址相关的状态对象, popstate 事件触发时,该对象会传入回调函数。如果不需要可填 null。
  • 2. title:新页面的标题,但是所有浏览器目前都忽略这个值,可填 null 。
  • 3. path:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个地址。

history.replaceState

history.replaceState(state,title,path)

参数和 pushState 一样,这个方法会修改当前的 history 对象记录, 但是 history.length 的长度不会改变。

② 监听路由popstate

window.addEventListener('popstate',function(e){
    /* 监听改变 */
})

同一个文档的 history 对象出现变化时,就会触发 popstate 事件history.pushState 可以使浏览器地址改变,但是无需刷新页面。注意⚠️的是:用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。 popstate 事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮或者调用 history.back()history.forward()history.go()方法。

总结: BrowserHistory 模式下的 history 库就是基于上面改变路由,监听路由的方法进行封装处理,最后形成 history 对象,并传递给 Router。

2. hash 模式下: http://www.xxx.com/#/home

哈希路由原理和history相似。

① 改变路由** **window.location.hash

通过 window.location.hash 属性获取和设置 hash 值。开发者在哈希路由模式下的应用中,切换路由,本质上是改变 window.location.hash 。

② 监听路由

onhashchange

window.addEventListener('hashchange',function(e){
    /* 监听改变 */
})

hash 路由模式下,监听路由变化用的是 hashchange 。

react-router

React Router v6 完全指南

react-redux

React-Redux,Redux,React三者关系

在深入研究 React-Redux 之前,应该先弄明白 React-Redux ,Redux , React 三者到底是什么关系。

  • Redux: 首先 Redux 是一个应用状态管理js库,它本身和 React 是没有关系的,换句话说,Redux 可以应用于其他框架构建的前端应用,甚至也可以应用于 Vue 中。
  • React-Redux:React-Redux 是连接 React 应用和 Redux 状态管理的桥梁。React-redux 主要专注两件事,一是如何向 React 应用中注入 redux 中的 Store ,二是如何根据 Store 的改变,把消息派发给应用中需要状态的每一个组件。
  • React:这个就不必多说了。

三者的关系图如下所示:

3.jpg

1. React Redux 和 Redux 使用

快速入门 redux 和 react-redux

2. Redux 设计模式 | 中间件原理

3. React Redux 原理

react-redux源码解析

image.png

Provider 创建Subscription,context保存上下文

  1. 首先创建一个 contextValue ,里面包含一个创建出来的父级 Subscription (我们姑且先称之为根级订阅器)和redux提供的store
  2. 通过react上下文context把 contextValue 传递给子孙组件。

Subscription订阅消息,发布更新

Subscription的作用是收集所有被 connect 包裹的组件的更新函数 onstatechange,然后形成一个 callback 链表,再由父级 Subscription 统一派发执行更新。

大致模型就是:

state更改 -> store.subscribe -> 触发 providerSubscriptionhandleChangeWrapper 也就是 notifyNestedSubs -> 通知 listeners.notify() -> 通知每个被 connect 容器组件的更新 -> callback 执行 -> 触发子组件Subscription 的 handleChangeWrapper ->触发子 onstatechange(可以提前透漏一下,onstatechange保存了更新组件的函数)。

1、2总结:

  • react-redux 中的 provider 作用 ,通过 reactcontext 传递 subscriptionredux 中的store ,并且建立了一个最顶部根 Subscription

  • Subscription 的作用:起到发布订阅作用,一方面订阅 connect 包裹组件的更新函数,一方面通过 store.subscribe 统一派发更新。

  • Subscription 如果存在这父级的情况,会把自身的更新函数,传递给父级 Subscription 来统一订阅。

connect 究竟做了什么?

用法:

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

mapStateToProps:

const mapStateToProps = state => ({ todos: state.todos })

作用很简单,组件依赖redux的 state,映射到业务组件的 props中,state改变触发,业务组件props改变,触发业务组件更新视图。当这个参数没有的时候,当前组件不会订阅 store 的改变。

mapDispatchToProps:

const mapDispatchToProps = dispatch => {
  return {
    increment: () => dispatch({ type: 'INCREMENT' }),
    decrement: () => dispatch({ type: 'DECREMENT' }),
    reset: () => dispatch({ type: 'RESET' })
  }
}

将 redux 中的dispatch 方法,映射到,业务组件的props中。

4. React Redux 中 connect 原理

react-mobx

1. Mobx-react 使用

2. Mobx 和 React Redux 区别

3. Mobx 原理,收集依赖,触发更新

umi

dva

React 设计模式

React 核心原理

事件原理

React 事件系统可分为三个部分:

  • 第一个部分是事件合成系统,初始化会注册不同的事件插件。
  • 第二个就是在一次渲染过程中,对事件标签中事件的收集,向 container 注册事件。
  • 第三个就是一次用户交互,事件触发,到事件执行一系列过程。

1. React 为什么有自己的事件系统? 

首先,对于不同的浏览器,对事件存在不同的兼容性,React 想实现一个兼容全浏览器的框架, 为了实现这个目标就需要创建一个兼容全浏览器的事件系统,以此抹平不同浏览器的差异。

其次,v17 之前 React 事件都是绑定在 document 上,v17 之后 React 把事件绑定在应用对应的容器 container 上,将事件绑定在同一容器统一管理,防止很多事件直接绑定在原生的 DOM 元素上。造成一些不可控的情况。由于不是绑定在真实的 DOM 上,所以 React 需要模拟一套事件流:事件捕获-> 事件源 -> 事件冒泡,也包括重写一下事件源对象 event 。

最后,这种事件系统,大部分处理逻辑都在底层处理了,这对后期的 ssr 和跨端支持度很高。

2. 什么是事件合成 ? 

  • React 的事件不是绑定在元素上的,而是统一绑定在顶部容器上,在 v17 之前是绑定在 document 上的,在 v17 改成了 app 容器上。这样更利于一个 html 下存在多个应用(微前端)。
  • 绑定事件并不是一次性绑定所有事件,比如发现了 onClick 事件,就会绑定 click 事件,比如发现 onChange 事件,会绑定 [blur,change ,focus ,keydown,keyup] 多个事件。
  • React 事件合成的概念:React 应用中,元素绑定的事件并不是原生事件,而是React 合成的事件,比如 onClick 是由 click 合成,onChange 是由 blur ,change ,focus 等多个事件合成。

3. 如何实现的批量更新?

4. 事件系统如何模拟冒泡和捕获阶段?

冒泡阶段和捕获阶段

export default function Index(){
    const handleClick=()=>{ console.log('模拟冒泡阶段执行') } 
    const handleClickCapture = ()=>{ console.log('模拟捕获阶段执行') }
    return <div>
        <button onClick={ handleClick  } onClickCapture={ handleClickCapture }  >点击</button>
    </div>
}
  • 冒泡阶段:开发者正常给 React 绑定的事件比如 onClick,onChange,默认会在模拟冒泡阶段执行。
  • 捕获阶段:如果想要在捕获阶段执行可以将事件后面加上 Capture 后缀,比如 onClickCapture,onChangeCapture。

阻止冒泡

React 中如果想要阻止事件向上冒泡,可以用 e.stopPropagation() 。

export default function Index(){
    const handleClick=(e)=> {
        e.stopPropagation() /* 阻止事件冒泡,handleFatherClick 事件讲不在触发 */
    }
    const handleFatherClick=()=> console.log('冒泡到父级')
    return <div onClick={ handleFatherClick } >
        <div onClick={ handleClick } >点击</div>
    </div>
}
  • React 阻止冒泡和原生事件中的写法差不多,当如上 handleClick上 阻止冒泡,父级元素的 handleFatherClick 将不再执行,但是底层原理完全不同,接下来会讲到其功能实现。

阻止默认行为

React 阻止默认行为和原生的事件也有一些区别。

原生事件:  e.preventDefault() 和 return false 可以用来阻止事件默认行为,由于在 React 中给元素的事件并不是真正的事件处理函数。所以导致 return false 方法在 React 应用中完全失去了作用。

React事件: 在React应用中,可以用 e.preventDefault() 阻止事件默认行为,这个方法并非是原生事件的 preventDefault ,由于 React 事件源 e 也是独立组建的,所以 preventDefault 也是单独处理的。

5. 如何通过 dom 元素找到与之匹配的fiber?

6. 为什么不能用 return false 来阻止事件的默认行为?

7. 事件是绑定在真实的dom上吗?如何不是绑定在哪里?

8. V17 对事件系统有哪些改变?

Hooks原理

  • 1 让函数组件也能做类组件的事,有自己的状态,可以处理一些副作用,能获取 ref ,也能做数据缓存。
  • 2 解决逻辑复用难的问题。
  • 3 放弃面向对象编程,拥抱函数式编程。

1. React Hooks 为什么必须在函数组件内部执行?React 如何能够监听 React Hooks 在外部执行并抛出异常?

react Hooks 是一种可以让你在函数组件中使用 state 和其他 React 功能的方法。Hooks 必须在函数组件的顶层调用,不能在循环、条件或嵌套函数中调用。这个规则确保了 Hook 在每一次渲染中都处于同样的顺序,这让 React 能够正确地保持多个 useState 和 useEffect 等 Hook 的状态。

hooks 对象本质上是主要以三种处理策略存在 React 中:

  • ContextOnlyDispatcher: 第一种形态是防止开发者在函数组件外部调用 hooks ,所以第一种就是报错形态,只要开发者调用了这个形态下的 hooks ,就会抛出异常。
  • HooksDispatcherOnMount: 第二种形态是函数组件初始化 mount ,因为之前讲过 hooks 是函数组件和对应 fiber 桥梁,这个时候的 hooks 作用就是建立这个桥梁,初次建立其 hooks 与 fiber 之间的关系。
  • HooksDispatcherOnUpdate:第三种形态是函数组件的更新,既然与 fiber 之间的桥已经建好了,那么组件再更新,就需要 hooks 去获取或者更新维护状态。

2. React Hooks 如何把状态保存起来?保存的信息存在了哪里?

函数组件对应 fiber 用 memoizedState 保存 hooks 信息,每一个 hooks 执行都会产生一个 hooks 对象,hooks 对象中,保存着当前 hooks 的信息,不同 hooks 保存的形式不同。每一个 hooks 通过 next 链表建立起关系。

3. React Hooks 为什么不能写在条件语句中?

hooks 为什么要通常放在顶部,hooks 不能写在 if 条件语句中,因为在更新过程中,如果通过 if 条件语句,增加或者删除 hooks,在复用 hooks 过程中,会产生复用 hooks 状态和当前 hooks 不一致的问题。

4. useMemo 内部引用 useRef 为什么不需要添加依赖项,而 useState 就要添加依赖项。

本质上 hooks 的状态是存在 fiber 上的,如果当前组件不销毁那么状态会一直保存下去,useRef 可以理解为就是一个对象,有一个固定的内存空间,所以无论是useMemo 内部如果引入了ref ,那么本质上就是引用了同一个对象,但是useState 不一样了,比如 state 是一个对象,那么如果想要更新组件,就需要浅拷贝一下,比如 dispatchAction({ ...state }) ,这样内存指向是不一样的,所以比如useMemo 引用了一个 state ,如果没有添加依赖项,那么会一直引用之前的,就是人们所说的‘闭包陷阱’。

5. useEffect 添加依赖项 props.a ,为什么 props.a 改变,useEffect 回调函数 create 重新执行。

6. React 内部如何区别 useEffect 和 useLayoutEffect ,执行时机有什么不同?

React 会用不同的 EffectTag 来标记不同的 effect,对于useEffect 会标记 UpdateEffect | PassiveEffect, UpdateEffect 是证明此次更新需要更新 effect ,HookPassive 是 useEffect 的标识符,对于 useLayoutEffect 第一次更新会打上 HookLayout 的标识符。React 就是在 commit 阶段,通过标识符,证明是 useEffect 还是 useLayoutEffect ,接下来 React 会同步处理 useLayoutEffect ,异步处理 useEffect 。

React 实践