React学习笔记

184 阅读14分钟

以下部分源自掘金小册《React进阶实践指南》

1. 如何解决多实例?

解决 React 多实例问题。解决的思路如下:

  1. 将 react 相关的依赖进行预打包;
  2. 在之前的构建流程中将 react 相关依赖设置为 external,即排除在打包流程外;
  3. 通过 Import Maps 将 react 强制指定为我们预打包好的 react 产物,这样就不会有多实例的问题了。

2. babel解析jsx流程

Babel 是一个 JavaScript 编译器,它可以将较新的 JavaScript 代码(如 ES6+ 或 JSX)转换为兼容性更好的代码。其主要目的是使开发人员能够在现有浏览器和环境中使用新的 JavaScript 特性和语法。

以下是 Babel 解析 JSX 的基本流程:

  1. 解析(Parsing) : 首先,Babel 将源代码作为输入并将其解析为一个抽象语法树(Abstract Syntax Tree, AST)。这个过程会将源代码字符串转换成一个具有结构化表示的对象。Babel 使用名为 @babel/parser 的包来实现这一步骤。
  2. 转换(Transforming) : 接下来,Babel 对 AST 进行一系列的转换操作,以修改或添加新的特性和语法。这些转换操作由插件完成,每个插件负责处理某种特定的转换任务。例如,对于 JSX 转换,我们需要使用 @babel/plugin-transform-react-jsx 插件。你可以通过 Babel 配置文件(如 .babelrcbabel.config.js)定义所需的插件和预设(preset,预设是一组插件的集合)。
  3. 生成(Generating) : 最后,Babel 将经过转换的 AST 转换回 JavaScript 代码。这个过程称为“代码生成”,通常使用名为 @babel/generator 的包执行。生成的代码将替换原始 JSX,以便在浏览器和其他 JavaScript 环境中运行。

总结一下,Babel 解析 JSX 的核心流程包括解析、转换和生成三个阶段。解析阶段将源代码转换为 AST,然后通过插件进行各种转换操作,最后将修改后的 AST 转换回 JavaScript 代码并输出。

要配置 Babel 解析 JSX,你需要在项目中安装相应的 Babel 插件,并在 Babel 配置文件中启用它们。例如,使用以下命令安装必要的包:

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react

然后,在项目根目录下创建一个 .babelrc 文件,并添加以下配置:

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ]
}

这个配置文件指示 Babel 使用 @babel/preset-env@babel/preset-react 预设。其中,@babel/preset-react 包含了对 JSX 语法的支持。现在,Babel 已经配置好了,可以解析 JSX 代码。

3. 类组件的定义

在 class 组件中,除了继承 React.Component ,底层还加入了 updater 对象,组件中调用的 setStateforceUpdate 本质上是调用了 updater 对象上的 enqueueSetStateenqueueForceUpdate 方法。

function Component(props, context, updater) {
  this.props = props;      //绑定props
  this.context = context;  //绑定context
  this.refs = emptyObject; //绑定ref
  this.updater = updater || ReactNoopUpdateQueue; //上面所属的updater 对象
}
/* 绑定setState 方法 */
Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
}
/* 绑定forceupdate 方法 */
Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
}

如上可以看出 Component 底层 React 的处理逻辑是,类组件执行构造函数过程中会在实例上绑定 props 和 context ,初始化置空 refs 属性,原型链上绑定setState、forceUpdate 方法。对于 updater,React 在实例化类组件之后会单独绑定 update 对象。

4. 函数组件

不要尝试给函数组件 prototype 绑定属性或方法,即使绑定了也没有任何作用,因为通过上面源码中 React 对函数组件的调用,是采用直接执行函数的方式,而不是通过new的方式。

5. 函数组件和类组件的区别

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

6. React组件五种主流的通信方式

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

7. React 组件本质

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

8. 触发一次setState ,在 React 底层主要做了那些事

render 阶段 render 函数执行 -> commit 阶段真实 DOM 替换 -> setState 回调函数执行 callback

9. setState批量更新

在 React 事件执行之前通过 isBatchingEventUpdates=true 打开开关,开启事件批量更新,当该事件结束,再通过 isBatchingEventUpdates = false; 关闭开关,然后在 scheduleUpdateOnFiber (setState底层开始调度更新的函数)中根据这个开关来确定是否进行批量更新。

但是异步事件批量更新将会被打破(例如setTimeOut)

解决办法: 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 。

10. 提升更新优先级

React-dom 提供了 flushSync ,flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。

11. React 同一级别更新优先级关系

flushSync 中的 setState  >  正常执行上下文中 setState  >  setTimeout ,Promise 中的 setState。

12. 如何监听 state 变化?

  • 类组件 setState 中,有第二个参数 callback 或者是生命周期componentDidUpdate 可以检测监听到 state 改变或是组件更新。

  • 那么在函数组件中,通常可以把 state 作为依赖项传入 useEffect 第二个参数 deps ,但是注意 useEffect 初始化会默认执行一次。

13. 函数组件使用 useState 的 dispatchAction 更新 state 的时候,不要传入相同的 state

在 useState 的 dispatchAction 处理逻辑中,会浅比较两次 state ,发现 state 相同,不会开启更新调度任务。

14. setState与useState的异同点

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

15. setState原理总结

setState 是 React 类组件中用于更新组件状态(state)的核心方法。当调用 setState 时,React 会按以下步骤执行相关操作:

  1. 合并状态更新setState 接收一个新的状态对象或一个返回新状态对象的函数。React 将这个新的状态片段与当前状态进行合并。请注意,新状态对象中的属性将覆盖旧状态对象中的对应属性。如果提供的是一个函数,该函数接收当前状态作为其第一个参数,以便基于现有状态计算新状态。
  2. 调度状态更新:React 不会立即更新状态并重新渲染组件。相反,它会将状态更新任务加入一个更新队列。这样做的目的是将多个连续或近似同时发生的状态更新批量处理,从而提高性能。此外,由于 JavaScript 是单线程的,setState 的调度机制还确保了所有状态更新都按照预期顺序执行。
  3. 重新渲染组件:在将所有待处理的状态更新合并后,React 会安排一次重新渲染。重新渲染过程首先调用组件的 render 方法以获得新的虚拟 DOM。然后,React 使用 "调和"(Reconciliation)算法比较新旧虚拟 DOM 树,并确定如何最有效地更新实际 DOM。这种方法使 React 能够以高性能的方式更新 UI。

16.React 使用 css 模块化的思路:

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

17. lifecycle

_5CP_TD(Z{YFZRB8NW6U1.jpg

18.useMemo 用法:

const cacheSomething = useMemo(create,deps)
  • create:第一个参数为一个函数,函数的返回值作为缓存值,如上 demo 中把 Children 对应的 element 对象,缓存起来。
  • deps: 第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。
  • cacheSomething:返回值,执行 create 的返回值。如果 deps 中有依赖项改变,返回的重新执行 create 产生的值,否则取上一次缓存值。

19.useMemo应用场景:

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

20. useCallback 和 useMemo 有什么区别?

useCallback 第一个参数就是缓存的内容,useMemo 需要执行第一个函数,返回值为缓存的内容,比起 useCallback , useMemo 更像是缓存了一段逻辑,或者说执行这段逻辑获取的结果。那么对于缓存 element 用 useCallback 可以吗,答案是当然可以了。

export default function (){
    const callback = React.useCallback(function handerCallback(){},[])
    return <Index callback={callback}  />
}

21. memo

  • 通过 memo 第二个参数,判断是否执行更新,如果没有那么第二个参数,那么以浅比较 props 为 diff 规则。如果相等,当前 fiber 完成工作,停止向下调和节点,所以被包裹的组件即将不更新。
  • memo 可以理解为包了一层的高阶组件,它的阻断更新机制,是通过控制下一级 children ,也就是 memo 包装的组件,是否继续调和渲染,来达到目的的。

22. useMemo 和 memo 的区别?

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

23. 异步渲染

Suspense:以同步的代码来实现异步操作的方案。Suspense 让组件等待异步操作,异步请求结束后再进行组件的渲染。(试验阶段)

用法:

// 子组件
function UserInfo() {
  // 获取用户数据信息,然后再渲染组件。
  const user = getUserInfo();
  return <h1>{user.name}</h1>;
}
// 父组件
export default function Index(){
    return <Suspense fallback={<h1>Loading...</h1>}>
        <UserInfo/>
    </Suspense>
}

fallback中的内容是当UserInfo处于数据加载时展示的内容。

传统模式:挂载组件-> 请求数据 -> 再渲染组件。
异步模式:请求数据-> 渲染组件。

24. 动态加载(懒加载)

React.lazy

基本使用:

const LazyComponent = React.lazy(() => import('./test.js'))

export default function Index(){
   return <Suspense fallback={<div>loading...</div>} >
       <LazyComponent />
   </Suspense>
}

React.lazy 接受一个函数,这个函数需要动态调用 import() 。它必须返回一个 Promise ,该 Promise 需要 resolve 一个 default export 的 React 组件。

25. 渲染错误边界

componentDidCatch 和 static getDerivedStateFromError() 两个额外的生命周期,去挽救由于渲染阶段出现问题造成 UI 界面无法显示的情况。

26. SuspenseReact.lazy的区别

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。

27.React对于大量数据的处理方案

1. 时间分片: 主要解决初次加载,一次性渲染大量数据造成的卡顿现象。

浏览器执 js 速度要比渲染 DOM 速度快的多。 时间分片,并没有本质减少浏览器的工作量,而是把一次性任务分割开来,给用户一种流畅的体验效果。就像造一个房子,如果一口气完成,那么会把人累死,所以可以设置任务,每次完成任务一部分,这样就能有效合理地解决问题。

2. 虚拟列表:一种长列表的解决方案

何为虚拟列表,就是在长列表滚动过程中,只有视图区域显示的是真实 DOM ,滚动过程中,不断截取视图的有效区域,让人视觉上感觉列表是在滚动。达到无限滚动的效果。

虚拟列表划分可以分为三个区域:视图区 + 缓冲区 + 虚拟区。

  • 视图区:视图区就是能够直观看到的列表区,此时的元素都是真实的 DOM 元素。

  • 缓冲区:缓冲区是为了防止用户上滑或者下滑过程中,出现白屏等效果。(缓冲区和视图区为渲染真实的 DOM )

  • 虚拟区:对于用户看不见的区域(除了缓冲区),剩下的区域,不需要渲染真实的 DOM 元素。虚拟列表就是通过这个方式来减少页面上 DOM 元素的数量。

28. 防抖节流

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

29. 按需引入

按需引入本质上是为项目瘦身,开发者在做 React 项目的时候,会用到 antd 之类的 UI 库,值得思考的一件事是,开发者如果只是用到了 antd 中的个别组件,比如 Button,就要把整个样式库引进来,打包就会发现,体积因为引入了整个样式文件大了很多。所以可以通过 .babelrc 实现按需引入。

30. react动画

三种 React 使用动画的方式,以及它们的权重

① 首选:动态添加类名

第一种方式是通过 transition,animation 实现动画然后写在 class 类名里面,通过动态切换类名,达到动画的目的。

这种方式是最优先推荐的方式,这种方式既不需要频繁 setState ,也不需要改变 DOM 。

② 其次:操纵原生 DOM

如果第一种方式不能满足要求的话,或者必须做一些 js 实现复杂的动画效果,那么可以获取原生 DOM ,然后单独操作 DOM 实现动画功能,这样就避免了 setState 改变带来 React Fiber 深度调和渲染的影响。

③ 再者:setState + css3

如果 ① 和 ② 都不能满足要求,一定要使用 setState 实时改变DOM元素状态的话,那么尽量采用 css3 , css3 开启硬件加速,使 GPU (Graphics Processing Unit) 发挥功能,从而提升性能。