以下部分源自掘金小册《React进阶实践指南》
1. 如何解决多实例?
解决 React 多实例问题。解决的思路如下:
- 将 react 相关的依赖进行预打包;
- 在之前的构建流程中将 react 相关依赖设置为 external,即排除在打包流程外;
- 通过
Import Maps将 react 强制指定为我们预打包好的 react 产物,这样就不会有多实例的问题了。
2. babel解析jsx流程
Babel 是一个 JavaScript 编译器,它可以将较新的 JavaScript 代码(如 ES6+ 或 JSX)转换为兼容性更好的代码。其主要目的是使开发人员能够在现有浏览器和环境中使用新的 JavaScript 特性和语法。
以下是 Babel 解析 JSX 的基本流程:
- 解析(Parsing) : 首先,Babel 将源代码作为输入并将其解析为一个抽象语法树(Abstract Syntax Tree, AST)。这个过程会将源代码字符串转换成一个具有结构化表示的对象。Babel 使用名为
@babel/parser的包来实现这一步骤。 - 转换(Transforming) : 接下来,Babel 对 AST 进行一系列的转换操作,以修改或添加新的特性和语法。这些转换操作由插件完成,每个插件负责处理某种特定的转换任务。例如,对于 JSX 转换,我们需要使用
@babel/plugin-transform-react-jsx插件。你可以通过 Babel 配置文件(如.babelrc或babel.config.js)定义所需的插件和预设(preset,预设是一组插件的集合)。 - 生成(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 对象,组件中调用的 setState 和 forceUpdate 本质上是调用了 updater 对象上的 enqueueSetState 和 enqueueForceUpdate 方法。
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组件五种主流的通信方式
- props 和 callback
- ref
- React-redux 或 React-mobx
- context
- 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的异同点
- 相同点: 从原理角度出发,setState和 useState 更新视图,底层都调用了 scheduleUpdateOnFiber 方法,而且事件驱动情况下都有批量更新规则。
- 不同点:
- 在不是 pureComponent 组件模式下, setState 不会浅比较两次 state 的值,只要调用 setState,在没有其他优化手段的前提下,就会执行更新。但是 useState 中的 dispatchAction 会默认比较两次 state 是否相同,然后决定是否更新组件。
- setState 有专门监听 state 变化的回调函数 callback,可以获取最新state;但是在函数组件中,只能通过 useEffect 来执行 state 变化引起的副作用。
- setState 在底层处理逻辑上主要是和老 state 进行合并处理,而 useState 更倾向于重新赋值。
15. setState原理总结
setState 是 React 类组件中用于更新组件状态(state)的核心方法。当调用 setState 时,React 会按以下步骤执行相关操作:
- 合并状态更新:
setState接收一个新的状态对象或一个返回新状态对象的函数。React 将这个新的状态片段与当前状态进行合并。请注意,新状态对象中的属性将覆盖旧状态对象中的对应属性。如果提供的是一个函数,该函数接收当前状态作为其第一个参数,以便基于现有状态计算新状态。 - 调度状态更新:React 不会立即更新状态并重新渲染组件。相反,它会将状态更新任务加入一个更新队列。这样做的目的是将多个连续或近似同时发生的状态更新批量处理,从而提高性能。此外,由于 JavaScript 是单线程的,
setState的调度机制还确保了所有状态更新都按照预期顺序执行。 - 重新渲染组件:在将所有待处理的状态更新合并后,React 会安排一次重新渲染。重新渲染过程首先调用组件的
render方法以获得新的虚拟 DOM。然后,React 使用 "调和"(Reconciliation)算法比较新旧虚拟 DOM 树,并确定如何最有效地更新实际 DOM。这种方法使 React 能够以高性能的方式更新 UI。
16.React 使用 css 模块化的思路:
- 第一种
css module,依赖于 webpack 构建和 css-loader 等 loader 处理,将 css 交给 js 来动态加载。 - 第二种就是直接放弃 css ,
css in js用 js 对象方式写 css ,然后作为 style 方式赋予给 React 组件的 DOM 元素,这种写法将不需要 .css .less .scss 等文件,取而代之的是每一个组件都有一个写对应样式的 js 文件。
17. lifecycle
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. Suspense和React.lazy的区别
React.lazy 和 React.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组件有一个fallbackprop,用于在等待异步组件加载时显示的内容。
<React.Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</React.Suspense>
这里的 <LazyComponent /> 是通过 React.lazy 动态导入的。在 LazyComponent 加载完成之前,会显示 React.Suspense 的 fallback prop 中的内容。
总的来说,React.lazy 和 React.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) 发挥功能,从而提升性能。