SPRINT-4

83 阅读3分钟

159、介绍下React中的useEffect

  • 在 React 中,useEffect 是一个用于处理副作用的 Hook。
  • ’副作用是指在组件生命周期中的某些特定时刻需要执行的操作,例如数据获取、订阅事件、手动操作 DOM 等。
  • useEffect 的作用就是在组件渲染完成后执行这些副作用操作。

useEffect 接收两个参数:一个副作用函数和一个依赖数组。

副作用函数是一个函数,它会在组件渲染之后执行。它可以包含任何副作用操作,如订阅、网络请求、DOM 操作等。示例代码如下:

import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // 执行副作用操作
    console.log('Component rendered');

    // 清理副作用
    return () => {
      console.log('Component unmounted');
    };
  }, []);

  return <div>My Component</div>;
}

在上述示例中,我们定义了一个 MyComponent 组件,并在其中使用了 useEffect。在副作用函数中,我们打印了一条消息来表示组件已经渲染完成。此外,我们还提供了一个返回函数,用于清理副作用。该函数将在组件卸载之前执行,以便做一些清理工作,如取消订阅或清除定时器。

第二个参数是一个依赖数组,用于指定副作用函数的依赖项。当依赖项发生变化时,副作用函数将重新执行。如果依赖数组为空,副作用函数只会在组件首次渲染时执行,并在组件卸载时执行清理操作。示例代码如下:

import { useEffect, useState } from 'react';

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

  useEffect(() => {
    console.log(`Count changed: ${count}`);
  }, [count]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Count: {count}</p>
    </div>
  );
}

在上述示例中,我们定义了一个 MyComponent 组件,并使用 useState 来保存一个计数器 count。在 useEffect 中,我们传入了 count 作为依赖项,这意味着只有当 count 发生变化时,副作用函数才会被触发。每次点击增加按钮时,count 都会发生变化,useEffect 会记录这个变化并打印相应的消息。

通过使用 useEffect,我们可以在 React 组件中处理各种副作用操作,并且可以在需要时进行清理。这使得我们能够更好地管理组件的生命周期和状态。

160、React中useEffect和useLayoutEffect区别

虽然 useEffectuseLayoutEffect 都是用于处理副作用操作的 Hook,但它们在执行时机和对组件渲染的影响上有所不同。

useEffect 是在组件渲染完成后异步执行的,它不会阻塞组件的渲染过程,适用于大多数情况。而 useLayoutEffect 是在组件渲染完成后同步执行的,它会阻塞组件的渲染,适用于需要准确获取布局信息或进行 DOM 操作的场景。

此外,useEffect 在组件的多次渲染中可能会乱序调用,而 useLayoutEffect 总是按顺序调用。useLayoutEffect 的副作用函数会立即执行,可能导致一些延迟问题,而 useEffect 的副作用函数可以在下一次渲染之前执行。

因此,根据具体的需求和场景,选择合适的 Hook 可以更好地管理组件的生命周期和副作用操作。

161、React 中组件的优化方法

  1. 使用 shouldComponentUpdateReact.memo 避免不必要的重新渲染:手动实现 shouldComponentUpdate 方法可以帮助我们优化组件的性能。通过判断当前 propsstate 的值是否发生了变化,如果没有变化,就可以避免不必要的重新渲染。类似地,使用 React.memo 包装函数式组件可以自动判断依赖项是否发生变化,如果没有变化就不会重新渲染组件。

  2. 注意避免在 render 函数中执行耗时操作:render 函数会在每次组件更新时都被调用,因此如果在 render 函数中执行耗时操作,会显著降低组件的性能。应该避免在 render 函数中执行耗时操作,并在需要的情况下把它们移到其他生命周期函数或其他组件中处理。

  3. 使用 React.lazySuspense 实现按需加载组件:对于较大的组件或路由页面,可以使用 React.lazySuspense 实现按需加载,即在需要时才进行组件加载,而不是在应用启动时一次性加载所有组件。这样可以提高应用程序的性能和加载速度。

  4. 避免在组件中直接修改 propsprops 应该被视为只读属性,并且在组件中应该避免直接修改 props 的值。这样做会导致组件失去可预测性,从而引起难以调试的问题。

  5. 使用 React.PureComponentReact.memo 实现浅比较:使用 React.PureComponent 类似于 shouldComponentUpdate 方法,它能够帮助我们避免不必要的重新渲染,但是它会自动比较前后状态浅层次的差异。另外,使用 React.memo 包装函数组件时也会使用浅比较来确定是否需要重新渲染。

162、React v18有哪些新特性

React 18 是即将发布的版本,虽然相关细节尚未确定,但是已经知道了一些新特性和改进点。下面是一些可能的 React 18 新特性:

  1. 改进的渲染模型:React 18 可能会引入基于时间切片的渲染模型,该模型可以将渲染任务分解成小的子任务,并在浏览器空闲时逐步完成任务,从而避免阻塞主线程及提高渲染性能。

  2. 新的组件交互方式:React 18 可能会引入新的组件交互方式,例如可中断的渲染、并行渲染、渐进式渲染等,以提高应用程序的交互性能。

  3. 更好的服务器端渲染(SSR)支持:React 18 可能会加强对服务器端渲染(SSR)方面的支持,并提供更好的性能特性,例如自动卡顿恢复、提高首次渲染性能等。

  4. 更好的开发者体验:React 18 可能会带来更好的开发者体验,并提供更多有用的工具和功能,例如更好的 DevTools、更高效的代码分割、更好的错误提示等。

总之,React 18 的目标是提高 React 的性能、可维护性和开发者体验,让开发者更加容易构建高质量的 Web 应用。

163、React从16-18的变化

React 从版本 16 到版本 18 的变化包括但不限于以下几个方面:

  1. 新增功能:React 16 引入了 Fiber 架构和 Portals 等新特性,支持异步渲染、增强的错误处理和更高效的组件重用等;React 18 引入了自适应渲染(Adaptive Rendering)和生命周期钩子重命名等新特性。

  2. Hooks:React 16 引入了 Hooks 概念,这是一种可重用状态逻辑的 API,使得函数组件具有了类组件中原本只有在 componentDidMount 和 componentDidUpdate 生命周期中才能使用的功能,如 state、effect 和 context 等。

  3. 性能优化:React 16 改进了虚拟 DOM 的实现,减少了内存和 CPU 开销;React 18 引入了改进的批量更新和分块渲染技术,通过更加智能地处理更新和并发渲染来提升应用性能。

  4. 代码拆分:React 16 和 React 18 中都可以使用 Suspense 组件来进行代码拆分,以提高应用加载速度和性能。

  5. TypeScript:React 16 和 React 18 中的类型定义都得到了改善,支持更完整的 TypeScript 类型检查和提示。

总的来说,从 16 到 18 版本,React 引入了新的功能和优化,同时也不断完善现有的功能和API,以提供更加高效、灵活和易用的开发体验。

164、React中如何实现keepalive,Offscreen(18实验新特性)

在React中实现keepalive和Offscreen的方法是使用React的新特性,例如React 18的Concurrent模式。Concurrent模式允许你在应用程序中更好地控制渲染优先级,从而实现类似keepalive和Offscreen的功能。

要实现keepalive,可以使用React的useMemouseEffect钩子来缓存组件的状态。这样,当组件重新渲染时,它将保留之前的状态,而不是重新创建一个新的实例。

对于Offscreen功能,可以使用React 18的useTransitionSuspense特性。useTransition允许你在组件之间平滑地过渡,而Suspense可以让你在组件加载时显示一个占位符。这两个特性结合起来,可以实现Offscreen的效果,即在组件不可见时,它们仍然保持在内存中,等待重新显示。

165、Redux的使用

Redux 是一种状态管理库,它可以用于 JavaScript 应用程序中的状态管理。Redux 的内部实现主要包括三个部分:

  1. State: Redux 中的状态存储在一个单一的对象中,即 store.getState() 返回的对象。这个对象是不可变的,因此每次更新状态时都会创建新的状态对象。

  2. Action: Action 是与状态交互的唯一途径。它们是描述性的对象,用于表示发生了什么事件或操作。每个 action 都必须有一个 type 字段,用于指定该 action 的类型。除了 type 字段外,action 还可以携带其他任意数据。

  3. Reducer: Redux 状态的更改由 reducer 函数处理,reducer 函数接受两个参数:旧状态和一个 action,并返回一个新状态。Reducer 函数必须是纯函数,即不会修改其输入参数,并且具有相同的输出,给定相同的输入。

在 Redux 的内部实现中,通过将 action 派发到 reducer 函数来更改状态,进而在应用程序中实现状态管理。在单向数据流的架构中,整个应用程序的状态是由一个单一的 store 维护,并且状态的更新是通过 dispatch 一个 action 触发的。然后,由 reducer 函数根据传入的 action 和当前状态计算出新的状态,并返回新的状态对象。

166、Redux的实现原理

Redux 是一个 JavaScript 应用程序的状态管理库,它允许我们以可预测和统一的方式管理应用程序的状态。

Redux 的内部实现主要由以下几个核心方法构成:

  1. createStore(reducer, [preloadedState], [enhancer]): 创建一个 Redux store 对象,它持有整个应用程序的 state。该方法接收三个参数:reducer 函数、初始状态和增强器中间件。Reducer 函数用于指定状态如何处理 action,初始状态是可选的,而增强器中间件则是 Redux 中间件的一个组合。

  2. dispatch(action): 用于触发 state 的更新。当调用 dispatch 方法时,它会将 action 对象传递给 reducer 函数,然后计算出新的 state,并更新 store 中的数据。

  3. subscribe(listener): 用于订阅 state 的变化。当 state 发生改变时,所有通过 subscribe 方法注册的回调函数都将被调用。

  4. combineReducers(reducers): 用于将多个 reducer 函数合并成一个。每个 reducer 函数都处理全局 state 树的某个子树,然后将它们组合在一起以生成最终的全局 state 树。

  5. applyMiddleware(...middlewares): 用于将多个 middleware 合并成一个。middleware 是一种扩展 dispatch 函数的方法,它可以捕获 action 并进行异步操作、日志记录、错误处理等。

Redux 的内部实现基于单向数据流的架构,整个应用程序的状态是由一个单一的 store 维护,并且状态的更新是通过 dispatch 一个 action 触发的。然后,由 reducer 函数根据传入的 action 和当前状态计算出新的状态对象,并返回新的状态对象。最终,所有订阅 store 变化的组件都将重新渲染,以反映最新的状态。

167、Redux中间件了解过吗

  • Redux中间件是一种在Redux应用程序中增强数据流的机制。它允许开发者在Redux的action被派发到reducer之前或之后,插入额外的逻辑。
  • 中间件可以用于处理异步操作、日志记录、错误处理、路由跳转等。
  • Redux中间件工作的基本原理是,它拦截Redux的dispatch方法,并在action到达reducer之前或之后执行特定的逻辑。这使得开发者能够在派发action时执行自定义的逻辑,例如发送网络请求、修改action等。

常见的Redux中间件有以下几种:

  1. Redux Thunk: Redux Thunk允许开发者在action中编写异步代码。它将函数类型的action识别为异步操作,并在合适的时机派发实际的action。这使得开发者能够在action中进行异步操作,例如发送网络请求,并在异步操作完成后更新应用状态。

  2. Redux Saga: Redux Saga是一个基于generator函数的Redux中间件。它使用了ES6的generator特性,以一种简洁而强大的方式来处理副作用(如异步操作)。通过定义saga函数,开发者可以非常直观地编写复杂的异步流程,例如监听多个action、并发请求等。

  3. Redux Observable: Redux Observable是基于RxJS的Redux中间件。它利用RxJS的强大功能来处理异步操作。通过使用Observable对象,开发者可以以声明式的方式组合和转换异步事件流,从而编写可维护和可测试的异步逻辑。

  4. Redux Promise: Redux Promise是一个简单的Redux中间件,用于处理基于Promise的异步操作。它允许开发者在action中返回一个Promise对象,当Promise对象被解决时,自动派发另一个action。

168、React-router-dom、React-router、history库三者什么关系

  • react-router 提供核心的路由和导航功能
  • react-router-domreact-router 进行了增强和封装,提供了浏览器端的路由组件
  • history 库提供了底层支持,管理应用程序的历史记录,被广泛地用于 react-routerreact-router-dom 中。

技术详解

react-routerreact-router-dom 是两个相关的库,都是用于实现 React 应用程序的客户端路由的。

react-router 是核心库,提供了声明式的路由和导航功能,而 react-router-dom 则是基于 react-router 的封装,提供了一些额外的功能,比如 BrowserRouterHashRouter 两种 Router 组件、Link 和 NavLink 组件等。

history 库则为路由提供了底层支持,提供了一些 API 来管理应用程序的历史记录,并在 react-routerreact-router-dom 中被广泛使用。

简单来说,react-routerreact-router-dom 是用于构建前端路由的库,而 history 库则提供了底层支持以实现路由功能。

169、路由的不同及原理

在 React 中,路由的不同模式实现原理主要体现在 URL 的表现形式和对浏览器历史记录的处理上。以下是 React 中路由不同模式的实现原理区别:

  1. Hash 模式

    • URL 表现形式:Hash 模式的 URL 包含一个带有 # 号的 hash 部分,例如 http://example.com/#/about
    • 实现原理:当 URL 的 hash 部分发生变化时,浏览器会触发 hashchange 事件,路由库会监听这个事件并根据新的 hash 值来渲染相应的组件。
    • 浏览器历史记录处理:在 Hash 模式下,改变 URL 的 hash 部分不会向服务器发送请求,因此不会生成新的历史记录,不会影响浏览器的前进后退功能。
  2. History 模式

    • URL 表现形式:History 模式的 URL 更加常规,不包含 # 号,例如 http://example.com/about
    • 实现原理:使用 HTML5 History API 中的 pushState 和 replaceState 方法来改变 URL,同时监听 popstate 事件以便在 URL 发生变化时进行相应的路由切换。
    • 浏览器历史记录处理:在 History 模式下,可以通过 pushState 和 replaceState 方法改变 URL,并且这些改变会向浏览器历史记录中添加新的条目,从而影响浏览器的前进后退功能。

总的来说,Hash 模式通过改变 URL 的 hash 部分来进行路由切换,不会向服务器发送请求,而 History 模式则通过 HTML5 History API 来改变整个 URL,可以更加自然地呈现 URL,同时能够利用浏览器的前进后退功能。在 React 中,这两种模式的实现原理会影响到路由库的具体实现方式以及开发者在配置路由时的选择。

170、React中状态管理的方式除了redux,还有哪些

除了 Redux,React 中还有一些其他流行的状态管理方案,包括:

  1. Context API:React 提供的 Context API 是一种轻量级的状态管理方案。它允许你将数据通过组件树进行传递,并在任何层级的组件中访问这些数据。通过创建一个 Context 对象,可以在提供者上下文中设置数据,并在消费者组件中使用 useContext 钩子来获取和更新数据。

  2. MobX:MobX 是另一个流行的状态管理库,它使用观察者模式来实现响应式状态管理。通过使用装饰器或函数式编程的方式,在需要观察的状态上添加 observable 标记,然后可以在组件中使用 useObserver 钩子或 @observer 装饰器来订阅状态的变化。MobX 还提供了对异步操作的良好支持。

  3. Zustand:Zustand 是一个简单、轻量级的状态管理库,采用了类似 Redux 的状态容器模式,但更加简洁。它支持 React Hooks,并且使用原生 JavaScript 对象来存储状态,通过调用 useState 创建状态容器,并提供了类似 Redux 的 dispatch 方法来更新状态。

  4. Recoil:Recoil 是 Facebook 推出的状态管理库,专门为 React 设计。它基于 React Hooks,并提供了原子状态的概念,使得在组件之间共享和管理状态变得更加简单。通过使用 Recoil 提供的 atom 和 selector,可以定义状态和派生状态,并在组件中使用 useRecoilState 和 useRecoilValue 等钩子来访问和更新状态。

  5. XState:XState 是一个功能强大的状态机库,可以用于管理复杂的状态和状态转换逻辑。它不仅限于 React,可以在任何 JavaScript 应用中使用。XState 使用有限状态机的概念来建模应用程序的状态,提供了丰富的 API 来定义状态和状态转换,以及处理异步操作和副作用。

以上是一些常见的 React 状态管理方案,每个方案都有其独特的优势和适用场景。根据项目的需求和复杂性,选择最适合的状态管理方案可以提高代码的可维护性和开发效率。

171、React中useState的执行本质

当组件初次渲染(挂载)时

  1. 初次渲染时,我们通过 useState 定义了多个状态;
  2. 调用一次useState ,都会在组件之外生成一条 Hook 记录,同时包括状态值(用 useState 给定的初始值初始化)和修改状态的 Setter 函数;
  3. 多次调用useState 生成的 Hook 记录形成了一条链表
  4. 触发 onClick 回调函数,调用 setS2 函数修改 s2 的状态,不仅修改了 Hook 记录中的状态值,还即将触发重渲染

组件重渲染时

在初次渲染结束之后、重渲染之前,Hook 记录链表依然存在。当我们逐个调用useState 的时候,useState 便返回了 Hook 链表中存储的状态,以及修改状态的 Setter

172、React中useState的底层实现原理

使用 useState 的时候遇到过一个问题:

通过 Setter 修改状态的时候,怎么读取上一个状态值,并在此基础上修改呢?如果看文档足够细致,应该会注意到 useState 有一个{函数式更新|Functional Update}的用法。

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}

传入 setCount 的是一个函数,它的参数是之前的状态,返回的是新的状态。熟悉 Redux 的朋友马上就指出来了:这其实就是一个 Reducer 函数。

useState底层实现原理

React 的源码中,useState 的实现使用了 useReducer。在 React 源码中有这么一个关键的函数 basicStateReducer

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

于是,当我们通过 setCount(prevCount => prevCount + 1) 改变状态时,传入的 action 就是一个 Reducer 函数,然后调用该函数并传入当前的 state,得到更新后的状态。而我们之前通过传入具体的值修改状态时(例如 setCount(5)),由于不是函数,所以直接取传入的值作为更新后的状态

传入的 action 是一个具体的值 (setCount(xx))

当传入 Setter 的是一个 Reducer 函数的时候:(setCount(c =>c+1))

173、React中useEffect的执行本质

注意其中一些细节:

  • useStateuseEffect 在每次调用时都被添加到 Hook 链表中;
  • useEffect 还会额外地在一个队列中添加一个等待执行的 Effect 函数;
  • 在渲染完成后,依次调用 Effect 队列中的每一个 Effect 函数。

React 官方文档 Rules of Hooks 中强调过一点:

Only call hooks at the top level. 只在最顶层使用 Hook。

具体地说,不要在循环、嵌套、条件语句中使用 Hook——

因为这些动态的语句很有可能会导致每次执行组件函数时调用 Hook 的顺序不能完全一致,导致 Hook 链表记录的数据失效。

174、React中useCallback的执行本质

该 Hook 用于在组件渲染完成后,将一些函数进行缓存,以避免因函数重复创建导致的性能问题。

通过useCallback,可以创建一个缓存函数,并在组件内使用该函数来代替重复创建的函数。

依赖数组在判断元素是否发生改变时使用了 Object.is 进行比较,因此当 deps 中某一元素为非原始类型时(例如函数、对象等),每次渲染都会发生改变,从而每次都会触发 Effect,失去了 deps 本身的意义。

Effect 无限循环

来看一下这段”永不停止“的计数器:

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

  useEffect(() => {
    setTimeout(() => setCount(count + 1), 1000);
  });

  return (
    <div className="App">
      <h1>{count}</h1>
    </div>
  );
}

如果你去运行这段代码,会发现数字永远在增长。我们来通过一段动画来演示一下这个”无限循环“到底是怎么回事:  

组件陷入了:渲染 => 触发 Effect => 修改状态 => 触发重渲染的无限循环

关于记忆化缓存(Memoization)

Memoization,一般称为记忆化缓存(或者“记忆”),它背后的思想很简单:假如我们有一个计算量很大的纯函数(给定相同的输入,一定会得到相同的输出),那么我们在第一次遇到特定输入的时候,把它的输出结果“记”(缓存)下来,那么下次碰到同样的输出,只需要从缓存里面拿出来直接返回就可以了,省去了计算的过程!

记忆化缓存(Memoization)的两个使用场景:

  1. 通过缓存计算结果,节省费时的计算
  2. 保证相同输入下返回值的引用相等

useCallback使用方法和原理解析

为了解决函数在多次渲染中的引用相等(Referential Equality)问题,React 引入了一个重要的 Hook—— useCallback。官方文档介绍的使用方法如下:

const memoizedCallback = useCallback(callback, deps);

第一个参数 callback 就是需要记忆的函数,第二个参数是deps 参数,同样也是一个依赖数组。在 Memoization 的上下文中,这个 deps 的作用相当于缓存中的键(Key),如果键没有改变,那么就直接返回缓存中的函数,并且确保是引用相同的函数

组件初次渲染(deps 为空数组的情况)

调用 useCallback 也是追加到 Hook 链表上,不过这里着重强调了这个函数 f1 所指向的内存位置,从而明确告诉我们:这个 f1 始终是指向同一个函数。然后返回的 onClick 则是指向 Hook 中存储的 f1。

组件重新渲染

重渲染的时候,再次调用 useCallback 同样返回给我们 f1 函数,并且这个函数还是指向同一块内存,从而使得 onClick 函数和上次渲染时真正做到了引用相等

175、React的渲染原理

在 React 中,渲染原理是指当组件状态发生变化时,React 是如何根据新的状态来更新 DOM 的过程。React 的渲染原理主要包括虚拟 DOM 的概念、调和过程以及批量更新机制。

  1. 虚拟 DOM

    • React 使用虚拟 DOM(Virtual DOM)作为内部数据结构,它是一个轻量级的 JavaScript 对象树,对应着真实 DOM 的层级结构。
    • 当组件的状态发生变化时,React 不会立即操作真实 DOM,而是先在虚拟 DOM 上进行操作和计算,然后通过对比新旧虚拟 DOM 的差异,最终只对必要的部分进行真实 DOM 的更新,从而提高性能。
  2. 调和过程

    • 当组件状态发生变化时,React 会触发重新渲染的过程。
    • 针对某一个组件的更新,在调和过程中,React 会生成新的虚拟 DOM 树,并与之前的虚拟 DOM 树进行比较,找出两者之间的差异。
    • 通过这种差异对比的方式,React 能够找出需要进行更新的部分,并尽可能地减少对真实 DOM 的操作,从而提高性能。
  3. 批量更新机制

    • React 使用批量更新机制来优化更新过程,避免频繁地对真实 DOM 进行操作。
    • 在 React 中,当组件状态发生变化时,React 会将多个状态变更合并为单一的更新操作,然后再统一进行虚拟 DOM 的比对和真实 DOM 的更新。

总的来说,React 的渲染原理基于虚拟 DOM 和调和过程,通过对比新旧虚拟 DOM 找出差异,并利用批量更新机制来最小化对真实 DOM 的操作,从而实现高效的页面更新。这种基于虚拟 DOM 的渲染原理使得 React 具有优秀的性能表现,并且可以方便地进行复杂页面的状态管理和更新。