引言
最近在准备面试, 所以整理了些常见的 React
相关的面试题!!!! 有需求的欢迎 👏🏻👏🏻 点赞、收藏, 同时欢迎 👏🏻👏🏻 大家在评论区留下面试中经常被问到的问题, 一起讨论讨论(我也可以悄摸记下准备准备)!! 最后文章写得仓促如果错误, 请多多见谅!!
补充: 慢慢的
React
相关题目整理越来越多, 并且目前掘金
编辑器不知道为啥内容多了编辑、修改起来很卡, 所以就针对该系列内容做了拆分(每篇 10 题)!!!!
一、Redux
- 页面上用户通过
dispatch
方法触发一个Action
:dispatch(Action)
Store
接收到Action
Store
调用Reducer
函数, 并将Action
和当前状态作为参数传递给它Reducer
函数根据Action
类型执行相应的处理, 并返回新的状态Store
更新状态, 并通知所有订阅状态的组件(视图)- 组件(视图)收到通知, 获取新状态, 重新渲染
1.1 createStore 实现原理
- 一个状态
state
用于存储状态 - 一个监听器列表, 当状态改变时会遍历该列表, 执行里面的所有方法
subscribe
: 注册监听器action
: 有效载体, 必须包含action.type
, 以及额外数据dispatch
: 执行reducer(state, action)
、遍历执行所有监听器(触发组件状态更新、从而引起页面重新渲染)reducer
: 纯函数(state, action)
==> 根据action.type
处理计算 ==> 返回新状态
1.2 react-redux
Provider
: 创建context
, 添加全局store
connect
: 高阶组件
- 通过
context
获取redux store
- 添加监听器, 当通过
dispatch
更新状态时执行该监听器, 监听器将执行第一参数(回调函数state => ({})
) 将返回值作为高阶组件的state
- 将第二参数使用
dispathc
进行包裹返回新函数:(... arg) => dispatch(fun(... arg))
- 最后将
state
和封装后的方法挂载到组件上
1.3 中间件
理解: 中间件其实就是要对 redux
的 store.dispatch
方法做一些改造, 来定制一些功能
Redux-thunk
: 实现原理
- 本来
dispatch
参数只能是action
对象,redux-thunk
中间件对dispatch
进行了封装, 允许action
是一个函数 - 在
dispatch
中如果发现action
是函数则执行action(dispatch, getState);
(延迟dispatch
), 否则执行dispatch(action)
js
// 下面方式使用了 mapDispatchToProps
// 正常情况下, openModalAction 函数应该返回一个 action 对象
// redux-thunk 中间件对 dispatch 进行了封装, 所以允许 action 是一个函数
export const openModalAction = ({ code, data, ...rest }) => {
return dispatch => {
dispatch(openModal({ code, data, ...rest }));
};
};
1.4 redux 优缺点
优点:
- 单一数据源: 所有状态都存在一个对象中, 使得开发、调试都会变得比较容易
State
是只读的: 如果要修改状态只能通过触发action
来修改,action
是一个普通对象, 可以很方便被日志打印、序列化、储存...... 因此状态的修改过程就会变得有迹可寻, 比较方便得跟踪数据的变化redux
使用纯函数(reducer
)来修改状态, 同一个action
返回的state
相同, 这样的话让状态的修改过程变得可控, 测试起来也方便
缺点: 啰嗦, 存在 Action
和 Reducer
, 如果要添加一个新的状态需要写一堆模版代码, 但是现在市面上已经有很多成熟的方案(工具)可以帮我们简化这一步, 比如 Redux Toolkit
js
export default createSlice({
initialState,
name: 'user',
reducers: {
updateUser: (state, { payload }) => ({ ...state, ...payload }),
},
});
1.5 和 mobx 的区别
- 单一数据、数据分散
- 响应式编程、函数式编程
- 状态修改和页面响应被抽象化封装到内部, 不易监测、调试
mobx
更适合业务不是很复杂、快速开发的项目
1.6 redux-thunk 和 redux-sage 区别
-
redux-thunk
允许action
是一个函数, 当aciton
是一个函数时会进行执行并传入dispatch
, 对于redux-thunk
的整个流程来说, 它是等异步任务执行完成之后, 我们再去调用dispatch
, 然后去store
去调用reduces
-
redux-saga
则是redux
的action
基础上, 重新开辟了一个async action
的分支, 单独处理异步任务, 当我们dispatch
的action
类型不在reducer
中时,redux-saga
的监听函数takeEvery
就会监听到, 等异步任务有结果就执行put
方法, 相当于dispatch
再一次触发dispatch
-
saga
自己基本上完全弄了一套asyc
的事件监听机制, 代码量大大增加, 从我自己的使用体验来看redux-thunk
更简单, 和redux
本身联系地更紧密, 尤其是整个生态都向函数式编程靠拢的今天,redux-thunk
的高阶函数看上去更加契合这个闭环
二、组件之间传参方法
2.1 父子间通信
这种父子通信方式也就是典型的单向数据流, 父组件通过
props
传递数据, 子组件不能直接修改props
, 而是必须通过调用父组件函数的方式告知父组件修改数据
-
父组件通过
props
传递数据给子组件 -
子组件通过调用父组件传来的
函数
传递数据给父组件(自定义事件) -
非常规方法: 父组件通过
ref
获取子组件的实例对象
2.2 兄弟间通信
状态提升: 在父组件中创建共同的状态、事件函数, 其中一个兄弟组件调用父组件传递过来的事件函数修改父组件中的状态, 然后父组件将状态传递给另一个兄弟组件
2.3 任意组件之间进行通信
- 使用
Context
js
import { createContext, useContext } from 'react';
const ThemeContext = createContext(null);
function App({ children }) {
const theme = useContext(ThemeContext);
return (<div>{theme}</div>)
}
function MyApp() {
return (
<ThemeContext.Provider value="dark">
<App />
</ThemeContext.Provider>
)
}
- 使用
Redux
等状态管理工具
三、受控组件和非受控组件
3.1 受控组件
组件内部
state
或值完全受prop
控制的组件
就像 antd
里 Input
组件, 可以通过 props
传一个 value
使得 Input
变为受控组件, Input
组件内部状态(值)就由 props
控制
js
import { Input } from 'antd';
<Input value="写死或者设置为状态值"/>
补充:
getDerivedStateFromProps
的作用
state
只受到props
的影响- 只有当
state
与prop
不同时, 才去修改state
3.2 非受控组件
组件内部
state
或值不受props
控制的组件, 由组件内部自己管理
就像 antd
里 Input
组件, 如果不给组件传 value
值, 那么组件就是非受控组件, Input
组件内由自己管理 value
, 这时如果要想拿到表单的 value
则只能通过 ref
等手段, 手动获取
注意的是: Input
组件内部, 使用了 input
标签将 value
和状态进行绑定, 那么对于 input
标签来说它是受控的, 所以受控组件只是相对
js
import { Input } from 'antd';
<Input/>
3.3 什么时候使用受控组件、什么时候使用非受控
当组件内部值或状态和外部存在交互逻辑时, 则需要将其作为受控组件进行使用
-
当组件状态(值)只由自身交换控制, 不受外部影响时, 可使用非受控组件: 比如
Antd
Input
组件, 如果输入框的内容只随着用户输入时改变, 那么就可以使用非受控组件 -
当组件状态(值)除了受自身交换控制、还受到外部影响时, 可使用受控组件: 比如
Antd
Input
组件, 需要和其他控件产生联动对组件的值进行相应的格式化 -
当组件状态(值)和外部需要交换时, 可使用受控组件: 比如
Antd
单选框, 当选中时需要隐藏页面上内容时, 一般就会将单选框最为受控组件进行使用
3.4 参考
四、Ref 相关
4.1 作用
- 在函数组件中, 当我们希望组件能够
记住
或者说存储
某些信息, 但呢又不希望该信息触发新的渲染时, 就可以使用 ref 来存储 - 用于访问真实
DOM
元素 - 当父组件需要获取子组件实例对象时, 也可通过
ref
来实现
4.2 获取真实 DOM
: 三种创建方式
- 推荐使用
API
:React.createRef()
、useRef
js
// 类组件, 使用 createRef
this.ref = React.createRef();
<div ref={this.ref}></div>
// 函数组件, 使用 useRef
const ref = React.useRef();
<div ref={ref}></div>
ref
回调函数方式
js
// 类组件
bindRef = ele => {
this.bodyRef = ele;
};
<div ref={this.bindRef}></div>
// 函数组件
const bindRef = useCallback((ele) => {
}, []);
<div ref={bindRef}></div>
- 字符串(仅限类组件中使用)
js
// 会自动在 this 上绑定 bodyRef, 等于当前元素
<div ref="bodyRef"></div>
4.3 获取子组件实例
- 子组件为类组件, 直接绑定
ref
, 就能够拿到整个子组件的实例对象
js
class A extends Component {}
const App = () => {
const ref = useRef()
return (<A ref={ref}/>)
}
- 函数组件:
forwardRef
+useImperativeHandle
js
import { forwardRef, useImperativeHandle } from 'react';
const A = (props, ref) => {
useImperativeHandle(ref, () => {
// 返回要绑定的实例对象
return {};
}, []);
}
const App = forwardRef(A);
4.4 转发 ref
- 可使用
React.forwardRef
进行转发
js
// React.forwardRef 返回一个组件
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
- 使用不同的属性名称将
ref
进行转发(常见于类组件, 毕竟forwardRef
不能用于类组件)
js
class A extends Component {
render () {
return (
<div ref={this.props.innerRef}>
1
</div>
);
}
}
const bodyRef = useRef()
<A innerRef={bodyRef} />
五、Fragment
在 React
中如果需要渲染多个元素, 需要使用元素进行包裹, 否则将会报错
- 报错原因, 主要原因还是在
JSX
编译这块
js
// 编译前
const dom = (
<ChildA />
<ChildB />
<ChildC />
);
// 编译后, 这样很明显是有问题的
const dom = (
React.createElement(......)
React.createElement(......)
React.createElement(......)
);
- 上面错误代码解决办法就是, 使用
div
等标签进行包裹, 这样就能够通过编译
js
// 编译前
const dom = (
<div>
<ChildA />
<ChildB />
<ChildC />
</div>
);
function MyComponent() {
return React.createElement(
'div',
null,
React.createElement(......),
React.createElement(......),
React.createElement(......),
);
}
- 上面处理会有个问题, 就是会添加了额外节点,
Fragment
出现就为了解决上面的问题, 通过Fragment
可以将子列表分组, 最终在渲染为真实DOM
节点时会将其忽略(不会进行渲染)
js
// 编译前
const dom = (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
// 编译后
function MyComponent() {
return React.createElement(
React.Fragment,
null,
React.createElement(......),
React.createElement(......),
React.createElement(......),
);
}
Fragment
简写形式<></>
js
const dom = (
<>
<ChildA />
<ChildB />
<ChildC />
</>
);
Fragment
对应ReactElement
元素类型(type
) 为Symbol('react.fragment')
js
console.log(
<>
<div> 1</div>
<div> 2</div>
</>,
);
{
type: Symbol('react.fragment'),
...
}
补充: 有一点是需要注意的, <>
并不接受任何属性, 包括 key
、ref
属性
六、React 元素中 $$typeof 的作用
用于标识
React
元素, 该属性值为Symbol
, 主要为了防止XOO
攻击
补充:XSS
攻击通常指的是通过利用网页开发时留下的漏洞, 通过巧妙的方法注入恶意指令代码到网页, 使用户加载并执行攻击者恶意制造的网页程序。
- 已知
JSX
语法将被编译为React.createElement
后返回一个对象(React
元素)
js
{
type: 'marquee',
props: {
bgcolor: '#ffa7c4',
children: 'hi',
},
key: null,
ref: null,
$$typeof: Symbol.for('react.element'), // 标识 React 元素
}
- 由于服务器可以存储任意的
JSON
数据, 如果在没有$$typeof
情况下, 就很容易被伪造(手动创建React
元素, 在页面进行注入)
js
// 假设后端返回了这样一串数据(React 元素)
const message = {
type: 'div',
props: {
dangerouslySetInnerHTML: {
__html: '/* 把你想的搁着 */'
},
},
};
// 前端这么现实数据
<p>{message}</p>
-
由于
JSON
不支持Symbol
类型数据, 所以只要在React
元素中添加Symbol
类型数据$$typeof
,React
在处理元素时只需通过$$typeof
就能够识别出非法元素(伪造元素)
-
如果浏览器不支持
Symbols
怎么办?
- 那这种保护方案就无效了 但是 React 仍然会加上 $$typeof 字段以保证一致性
- 但这样只会设置一个数字 ------
0xeac7
- 而之所以设置
0xeac7
, 只是因为0xeac7
看起来有点像React
七、Hooks
React 16.8
的新增特性, 它可以让你在不编写class
的情况下使用state
以及其他的React
特性
17.1 和类组件对比有什么优点
优点:
-
更简洁: 相比于传统的
class
组件, 使用Hooks
可以将组件的逻辑拆分成更小, 这使得组件代码更加简洁、易读、好维护 -
易上手: 使用
Hooks
你可以在函数组件中使用状态和其他React
特性, 无需编写class
从而避免了繁琐的class
组件的声明和继承、同时也无需考虑this
指向等问题 -
逻辑复用: 自定义
Hooks
允许将组件之间的状态逻辑进行抽离, 作为一个独立的可组合和可共享单元, 从而减少了重复代码的出现 -
更好的可测试性: 通过
Hooks
可以将组件渲染、和业务逻辑分离进行分离, 使得组件的测试变得更加容易。可以针对每个Hook
编写单独的测试,确保其正确性, 同时保持组件测试的简洁性。 -
灵活性:
Hooks
的设计允许你在组件内部使用多个不同的Hook
, 这使得你可以在一个函数组件中使用各种各样的特性, 而不必担心组件层次的嵌套和复杂性 -
有助于代码拆分: 使用
Hooks
可以更容易地拆分组件, 将组件的不同部分拆分成更小的逻辑单元,有助于更好地组织和管理代码。 -
类组件在业务不断扩展的情况下, 容易变得臃肿难以维护, 往往相关的业务被拆分到多个生命周期里, 或者一个生命周期中存在多个不相关的业务, 而
Hook
的出现, 可以将业务拆分为更小的函数, 对业务逻辑进行更为细腻的控制, 使得组件更容易理解、维护 -
可以把状态分开管理, 逻辑上更清晰了, 更方便维护了
-
补充: 类组件中如果需要复用状态逻辑, 只能通过高阶组件来实现, 没有
hooks
简洁, 而且还多了一层组件嵌套
缺点:
-
陡峭的学习曲线: 对于那些熟悉传统
class
组件的开发者来说, 学习Hooks
可能需要一些时间。Hooks
改变了组件的编写方式, 并且需要理解如何正确地使用useState
、useEffect
、useContext
等钩子函数 -
使用规则:
Hooks
有一些使用规则, 例如在条件语句中不可使用, 或者只能在函数组件的最顶层使用。违反这些规则可能导致bug
和意想不到的行为。 -
性能问题: 尽管
Hooks
通常可以优化组件逻辑, 但不正确地使用它们可能导致性能问题。比如, 在useEffect
中没有正确处理依赖项数组可能会导致不必要的重复执行。
怎么避免 hooks
的常见问题:
- 不要在
useEffect
里面写太多的依赖项, 划分这些依赖项成多个单一功能的useEffect
其实这点是遵循了软件设计的单一职责模式
- 拆分组件, 细化组件的粒度, 复杂业务场景中使用
hooks
应尽可能地细分组件, 使得组件的功能尽可能单一, 这样的hooks
组件更好维护 - 能通过事件触发数据更新, 就尽量通过事件方式去实现, 尽量避免在
useEffect
中依赖A
状态然后去修改B
状态
7.2 常用的几个 Hooks
useState
: 用于定义组件状态, 需要注意的是该方法在更新状态时会进行浅比较, 如果待更新状态值和当前状态值一致, 则不会进行更新, 不会引起组件的重新渲染
js
const [state, setState] = useState(0);
setState(0); // 不会引起组件重新渲染
useEffect
: 让函数型组件拥有处理副作⽤
的能⼒, 每次依赖项改变, 都会触发回调函数的执行, 通过它可模拟类似类组件
中的部分⽣命周期useLayoutEffect
: 与useEffect
相同, 但它会在所有的DOM
变更之后同步调用useInsertionEffect
: 在任何DOM
突变之前触发, 主要是解决CSS-in-JS
在渲染中注入样式的性能问题useMemo
: 可以监测某个值的变化, 根据变化值计算新值,useMemo
会缓存计算结果, 如果监测值没有发⽣变化, 即使组件重新渲染, 也不会重新计算useRef
: 获取DOM
元素对象、记录非状态数据、获取子组件实例对象useCallback
: 可让您在重新渲染之间缓存函数定义, 使组件重新渲染时得到相同的函数实例useImperativeHandle
用于绑定ref
useReducer
: 使用简易版Redux
useContext
: 使用context
useDebugValue
: 可以在React DevTools
中向自定义Hook
添加一个标签, 方便追踪数据useId
: 生成唯一ID
, 是hook
所以只能在组件的顶层或您自己的Hook
中调用它, 您不能在循环或条件内调用它、不应该用于生成列表中的键useDeferredValue
: 用于推迟更新部分UI
useSyncExternalStore
: 使用外部store
useTransition
: 允许在不阻塞UI
的情况下更新状态
7.3 useEffect、useLayoutEffect、useInsertionEffect 之间的区别
-
useInsertionEffect
: 应该是DOM
变更之前执行 -
useLayoutEffect
:DOM
已经按照VDOM
更新了, 此时DOM
已经在内存中更新了, 但是还没有更新到屏幕上 -
useEffect
: 则是浏览器完成渲染之后执行 -
所以三者执行顺序:
useInsertionEffect(DOM 变更前)
、useLayoutEffect(DOM 变更后)
、useEffect
-
useLayoutEffect
与useEffect
基本相同, 但它会在所有的DOM
变更之后同步
调用, 一般可以使用它来读取DOM
布局并同步触发重渲染, 为了避免阻塞视觉更新, 我们需要尽可能使用标准的useEffect
-
useEffect
和useLayoutEffect
都可用于模拟componentDidUpdate
componentDidMount
-
当父子组件都用到
useEffect
时, 子组件中的会比父组件中的先触发
7.4 React.memo
- 在类组件的时代时代, 为了性能优化我们经常会选择使用
PureComponent
, 组件每次默认会对props
进行一次浅比较
, 只有当 props 发生变更, 才会触发 render
js
class MyComponent extends PureComponent {
render () {}
}
- 当然在类组件中, 我们除了使用
PureComponent
还可以在shouldComponentUpdate
生命周期中, 对props
进行比较, 进行更深层次的控制;
补充:
shouldComponentUpdate
当收到新的props
或state
时, 在渲染之前都会被调用- 这里的比较可以是浅比较、也可以是深比较, 主要看代码实现
- 当
shouldComponentUpdate
返回为true
的时候, 当前组件进行render
, 如果返回的是false
则不进行render
js
class MyComponent extends Component {
shouldComponentUpdate(){
if (需要 Render) {
// 会进行渲染
return true
}
// 不会进行渲染
return false
}
render () {}
}
- 在函数组件中, 我们是无法使用上面两种方式来限制
render
的, 但是React
贴心的提供了React.memo
这个HOC(高阶组件)
, 它的作用和PureComponent
很相似, 只是它是专门为函数组件设计的
React.memo 使用说明
- 默认情况下会对组件
props
进行浅比较
, 只有props
变更才会触发render
- 允许传入第二参数, 该参数是个函数, 该函数接收
2
个参数, 两个参数分别是新旧props
,- 注意: 与
shouldComponentUpdate
不同的是,arePropsEqual
返回true
时, 不会触发render
, 如果返回false
则会, 和shouldComponentUpdate
刚好与其相反
js
// 组件
function MyComponent(props) {}
// 比较方法
function areEqual(prevProps, nextProps) {
if (需要 Render) {
// 会进行渲染
return false
}
// 不会进行渲染
return true
}
export default React.memo(MyComponent, areEqual);
作用: 性能优化, 如果本组件中的数据没有发⽣变化, 阻⽌组件更新, 类似类组件中的
PureComponent
和shouldComponentUpdate
7.5 使用时需要注意什么
-
遵守
Hooks
使用规则:Hooks
只能在函数组件的顶层使用, 或者在自定义hooks
中使用, 不能在循环、条件或嵌套函数中使用hooks
-
依赖数组: 在使用
useEffect
或useCallback
等hooks
时, 务必提供依赖数组作为第二个参数。忽略或者错误的依赖数组可能导致意外行为, 比如过度重新渲染或内存泄漏 -
避免无限循环: 在使用
useEffect
时要小心无限循环, 确保依赖数组中有正确的依赖项, 并且effect
的逻辑不会触发不必要的重新渲染 -
状态不可变性: 避免直接修改状态对象, 也不要试图通过
push
、pop
、splice
等直接更改数组 -
单一职责
组件
、useEffects
-
尽量避免通过
useEffect
来处理actions
:useEffect
监听某个状态A
, 内部又去修改A
, 这样就容易造成死循环 -
如果某个数据的变更不需要触发
render
, 或者该数据没有在jsx
中被使用, 那么就不要使用useState
改用useRef
进行记录
7.6 为什么 hooks 不能写在循环或者条件判断语句里?
Hooks
只能在函数组件的顶层使用, 或者在自定义hooks
中使用, 不能在循环、条件或嵌套函数中使用hooks
js
export default () => {
const [name, setName] = useState('1');
if (!name) {
return null;
}
const [age, setAge] = useState();
const handler = useCallback(() => {
setName(null);
}, []);
return (
<div onClick={handler}>
点击我
</div>
);
};
原因: React
需要利用 调用顺序
来正确更新相应的状态, 以及调用相应的钩子函数, 一旦在循环或条件分支语句中调用 Hooks
, 就容易导致调用顺序的不一致性, 从而产生难以预料到的后果
这里拿 useState
来举例:
hooks
为了在函数组件中引入状态, 维护了一个有序表- 首次执行时会将每个
useState
的初始值,依次
存到有序表里 - 每次更新也都会按照
索引
修改指定位置的值 - 每次
render
会将对应索引
的值作为状态返回 - 那么试想下, 如果我们将
useState
写在判断条件下, 可能会导致useState
不执行, 那么这个有序列表就会出现混乱
js
export default () => {
const [name, setName] = useState('1');
if (!name) {
return null;
}
const [age, setAge] = useState();
const handler = useCallback(() => {
setName(null);
}, []);
return (
<div onClick={handler}>
点击我会报错
</div>
);
};
总结:
hooks
是将state
原子化, 使用类似索引的方式来记录状态值, 当连续创建状态A
B
, 就会有索引0
对应着A
, 索引1
对应这B
, 如果使用在循环、条件、嵌套函数内使用Hook
就很容易造成索引错乱
7.7 如何打破了 React Hook 必须按顺序、不能在条件语句中调用的枷锁?
总结一下: 之前是通过顺序来查找, 现在通过唯一 key
来查找
实现则需要去修改源码, 参考: 我打破了 React Hook 必须按顺序、不能在条件语句中调用的枷锁
7.8 为什么 useState 返回的是一个数组?
-
useState
要返回两个值, 一个是当前状态, 另一个则是修改状态的方法, 那么这里它就有两种方式可以返回这两个值: 数组、对象 -
那么问题就回到, 数组和对象解构赋值的区别了:
- 数组的元素是按次序排列的, 数组解构时变量的取值由数组元素的位置决定, 变量名可以任意命名, 如下:
js
const [name, setName] = useState()
const [age, setAge] = useState()
- 对象的属性没有次序, 解构时变量名必须与属性同名才能取到正确的值, 假设
useState
返回的是一个对象, 那么就得这么使用:
js
const { state: name, setState: setName } = useState()
const { state: age, setState: setAge} = useState()
- 上面例子可以得出结果,
useState
返回数组相比于对象会更灵活、解构起来也会更简洁、方便
- 当然最终
useState
返回的是啥, 还是由具体实现决定, 如果useState
返回的是对象, 也不是不行
7.9 简单实现 hooks
js
// 一、实现useState
const { render } = require("react-dom");
let memoriedStates = [];
let lastIndex = 0;
function useState(initialState) {
memoriedStates[lastIndex] = memoriedStates[lastIndex] || initialState;
function setState(newState) {
memoriedStates[lastIndex] = newState;
// 状态更新完毕,调用render函数。重新更新视图
render();
}
// 返回最新状态和更新函数,注意 index 要前进
return [memoriedStates[lastIndex++], setState];
}
// 二、实现useEffect
let lastDendencies; // 存放依赖项的数组
function useEffect(callback, dependencies) {
if (lastDendencies) {
// 判断传入的依赖项是不是都没有变化,只要有以一项改变,就需要执行callback
const isChange = dependencies && dependencies.some((dep, index) => dep !== lastDendencies[index]);
if (isChange) {
// 一开始没有值,需要更新一次(相当于componentDidMount)
typeof callback === 'function' && callback();
// 更新依赖项
lastDendencies = dependencies;
}
} else {
// 一开始没有值,需要更新一次(相当于componentDidMount)
typeof callback === 'function' && callback();
// 更新依赖项
lastDendencies = dependencies;
}
}
// 三、实现useCallback
let lastCallback; // 最新的回调函数
let lastCallbackDependencies = []; // 回调函数的依赖项
function useCallback(callback, dependencies = []) {
if (lastCallback) {
const isChange = dependencies && dependencies.some((dep, index) = dep !== lastCallbackDependencies[index]);
if (isChange) {
// 只要有一个依赖项改变了,就更新回调(重新创建)
lastCallback = callback;
lastCallbackDependencies = dependencies;
}
} else {
lastCallback = callback;
lastCallbackDependencies = dependencies;
}
// 最后需要返回最新的函数
return lastCallback;
}
// 四、实现useRef
let lastRef;
function useRef(initialValue = null){
lastRef = lastRef != undefined ? lastRef : initialValue;
// 本质上就是返回一个对象,对象种有一个current属性,值为初始化传入的值,如果没有传入初始值,则默认为null
return {
current: lastRef
}
}
// 五、实现useContext
function useContext(context){
// 很简单,就是返回context的_currentValue值
return context._currentValue;
}
// 六、实现useReducer
let lastState;
function useReducer(reducer, initialState){
lastState = lastState !== undefined ? lastState : initialState;
// dispatch一个action,内部就是自动调用reducer来计算新的值返回
function dispatch(action){
lastState = reducer(lastState, action);
// 更新完毕后,需要重新渲染视图
render();
}
// 最后返回一个的状态值和派发action的方法
return [lastState, dispatch];
}
7.10 useCallback 和 useMemo 的区别?
- 可以
useMemo
来实现useCallback
吗?
可以, useMemo
只要返回一个函数即可
拓展知识:
useCallback
是「useMemo
的返回值为函数」时的特殊情况, 是React
提供的便捷方式。在React Server Hooks
代码 中,useCallback
就是基于useMemo
实现的, 尽管React Client Hooks
没有使用同一份代码, 但useCallback
的代码逻辑和useMemo
的代码逻辑仍是一样的
八、性能优化
- 跳过不必要的组件更新
PureComponent
、React.memo
、shouldComponentUpdate
useMemo
、useCallback
来生成稳定值- 状态下放, 缩小状态影响范围: 如果一个状态只在某部分子树中使用, 那么可以将这部分子树提取为组件, 并将该状态移动到该组件内部
- 列表项使用
key
属性: useMemo
返回虚拟DOM
: 利用useMemo
可以缓存计算结果的特点, 如果useMemo
返回的是组件的虚拟DOM
, 则将在useMemo
依赖不变时, 跳过组件的Render
阶段- 对于
props
可以跳过回调函数改变
触发的Render
: 对于一些回调函数(事件)的变更, 其实并不需要触发render
, 实现方式参考: 跳过回调函数改变触发的 Render 过程 - 自定义
Hooks
按需更新: 假设我们自定义的Hook
暴露的状态, 有多个属性值, 但是调用则只使用了若干个, 那么其他属性的变更, 不应该引起render
, 实现方案参考: demo - 动画库直接修改
DOM
属性: 当一个动画启动后, 每次动画属性改变不会引起组件重新Render
而是直接修改了dom
上相关属性值, 比如拖拽动作可以通过操作原生DOM
而不是通过状态来记录位置, 从而触发组件的render
- 组件按需挂载:
- 懒加载: 通过
Webpack
的动态导入和React.lazy
方法来实现 - 懒渲染: 懒渲染指当组件进入或即将进入可视区域时才渲染组件, 常见的组件
Modal/Drawer
等 - 虚拟列表
- 批量更新:
- 类组件,
setState
自带批量更新操作 - 函数组件, 尽量将相关的状态进行合并, 然后进行批量更新
- 按优先级更新, 及时响应用户: 举个例子当页面弹出一个
Modal
, 当用户点击确定
按钮后, 代码将执行两个操作, 1、关闭 Modal; 2、 处理Modal
传回的数据并展示给用户; 同时假设第二个操作需要执行500ms
时, 那么用户会明显感觉到从点击按钮到Modal
被关闭之间的延迟, 如下代码 如果setNumbers
这一步处理时间耗时, 那么就会出现明显的卡顿
js
const slowHandle = () => {
setShowInput(false)
// 计算耗时 500s
setNumbers([...numbers, +inputValue].sort((a, b) => a - b))
}
解决办法: 通过 setTimeout
将耗时任务放到下一个宏任务中去执行
js
const fastHandle = () => {
// 优先响应用户行为
setShowInput(false)
// 将耗时任务移动到下一个宏任务执行
setTimeout(() => {
setNumbers([...numbers, +inputValue].sort((a, b) => a - b))
})
}
- 缓存优化:
React
组件中常用useMemo
缓存上次计算的结果, 一般用在计算非常耗时的场景中, 如: 遍历大列表做统计信息, 当然useMemo
只能缓存上一次结果, 如果需要缓存所以结果则需要自定义一个缓存表, 进行处理- 当然对于接口数据缓存来说, 如果实时性比较高的, 那么我们可以先取缓存时间, 然后通过
requestIdleCallback
在系统闲暇时重新发起请求获取数据, 这样在请求比较耗时情况下, 可以优化用户的体验
- 通过
debounce
、throttle
优化频繁触发的回调函数
总结:
- 如果是因为存在不必要更新的组件进入了
Render
过程, 则选择跳过不必要的组件更新进行优化- 如果是因为页面挂载了太多不可见的组件, 则选择
懒加载
、懒渲染
或虚拟列表
进行优化。- 如果是因为多次设置状态, 引起了多次状态更新, 则选择批量更新或
debounce(防抖)
、throttle(节流)
优化频繁触发的回调进行优化- 如果组件
Render
逻辑的确非常耗时, 我们需要先定位到耗时代码(这里我们可以选择使用React
官方提供的性能分析插件、或者使用chrome
自带的性能分析插件), 并判断能否通过缓存优化它, 如果可以则选择缓存优化, 否则选择按优先级更新, 及时响应用户, 将组件逻辑进行拆解, 以便更快响应用户
补充:
- 在组件中为
window
注册的全局事件、定时器等, 在组件卸载前要清理掉. 防止组件卸载后继续执行影响应用性能 - 使用
Fragment
避免额外标记 - 不要使用内联函数定义
- 避免使用内联样式属性
- 为组件创建错误边界
8.1 参考
九、React 18 更新内容有哪些?
主基调: 并发性是 React 18 的主要优势之一
9.1 彻底放弃 IE
17
还修复了IE
兼容问题18
就彻底放弃了对IE
的支持
9.2 自动批处理
- 我们都知道在
React 18
之前:
- 在合成事件、生命周期中如果多次修改
state
, 会进行批处理, 然后只会触发一次render
- 在定时器、
promise.then
、原生事件处理函数中不会进行批处理 - 这里之所以会有两种不同情况, 主要原因是早期对于
批处理
是通过一个状态作为批处理依据, 具体可查阅上文7.2 React 的更新机制: 异步 OR 同步
部分内
js
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount((c) => c + 1);
setFlag((f) => !f);
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={flag ? { color: "blue" } : { color: "black" }}>{count}</h1>
</div>
);
}
- 在
React 18
之后所有的更新都将自动批处理:
- 主要原因是不再通过
状态
来作为批处理依据, 而是基于fiber
增加调度的流程来实现的, 以更新的「优先级」为依据来进行批处理 - 他通过对于
Root
上是否存在当前任务的调度信息, 以及任务的更新优先级是否发生变化, 以此来决定是否要开启一个新的updateScheduled
- 而只要复用同一个
Scheduled
一个Scheduled
只会出发一次render
也就完成了自动批处理的实现 - 参考: React18精读一: Automatic Batching 自动批处理
- 如何退出批处理:
flushSync
批处理是一个破坏性改动,, 如果你想退出批量更新, 可以使用react-dom
中提供的flushSync
函数
js
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
const App: React.FC = () => {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={() => {
flushSync(() => {
setCount1(count => count + 1);
});
// 第一次更新
flushSync(() => {
setCount2(count => count + 1);
});
// 第二次更新
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};
export default App;
9.3 Render API
修改了将组件挂载到
root
节点的一个api
- 旧版本
js
import React from 'react';
import ReactDOM from 'react-dom';
const root = document.getElementById('root')!;
ReactDOM.render(<App />, root);
18
版本: 支持并发模式渲染
js
// React 18
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = document.getElementById('root')!;
ReactDOM.createRoot(root).render(<App />);
9.4 删除: 卸载组件时的更新状态的警告
- 背景:
18
之前我们如果在组件卸载后, 尝试修改状态就会在控制台抛出异常
js
useEffect(() => {
function handleChange() {
setState(store.getState())
}
store.subscribe(handleChange)
return () => store.unsubscribe(handleChange)
// 这里如果没有解除订阅, 那么控制台将会抛出如下错误
// Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
}, [])
-
目的: 如上代码如果忘记调用
unsubscribe
解除订阅, 就会出现内存泄露!! 所以为了避免出现这种问题, 所以就设置了上面的警告信息!! -
为什么要去除: 主要原因还是警告具有误导性, 如下代码是一个比较常见的场景, 在执行
post('/someapi')
期间如果组件卸载, 后面调用setPending
, 在18
之前这里将会抛出错误, 但实际上这里并没有太大的毛病, 也并没有存在内存泄露问题!!!
js
async function handleSubmit() {
setPending(true)
await post('/someapi') // component might unmount while we're waiting
setPending(false)
}
- 在
18
之前人们为了抑制这个错误, 经常写如下代码:
js
let isMountedRef = useRef(false)
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
}
}, [])
async function handleSubmit() {
setPending(true)
await post('/someapi')
if (!isMountedRef.current) {
setPending(false)
}
}
- 看起来上面解决方法实际比原来的问题更加糟糕, 所以最后还是删了吧.... 后面看看有没其他手段来规避内存泄露
9.5 关于 React 组件的返回值
-
在
React 17
中, 如果需要返回一个空组件, 只允许返回null
, 如果返回了undefined
控制台则会在运行时抛出一个错误 -
在
React 18
中, 既能返回null
, 也能返回undefined
(但是React 18
的dts
文件还是会检查, 只允许返回null
, 这里我们可以忽略这个类型错误 -
之前为什么要这么设计: 在编码过程中忘记
return
, 是比较容易犯的一个错误, 为了帮助用户发现这个问题, 所以就有了这个警告 -
那现在为什么又允许了:
- 在
Suspense
中允许为fallback
为undefined
, 所以为了保持一致性, 顾允许返回undefined
- 考虑到现在类型系统和
Eslint
都已经非常成熟、健壮, 通过它们就可以很好避免这类低级错误了
9.6 严格模式下第二次渲染期间抑制日志
- 背景:
- 为了防止组件内有什么意外的副作用, 而引起
BUG
, 所以严格模式下React
在开发模式中会刻意执行两次渲染, 尽可能把问题提前暴露出来, 来提前预防 - 而
React
为了让日志更容易阅读, 通过修改console
中的方法, 取消了其中一次渲染的控制台日志
-
问题: 开发人员在调试过程中会存在很多困惑
-
展望未来:
React
将不再默认在第二次渲染期间抑制日志, 如果安装了React DevTools > 4.18.0
, 第二次渲染期间的日志现在将以柔和的颜色显示在控制台中
9.7 Suspense
- 更新前: 如果
Suspense
组件没有提供fallback
属性,React
就会跳过它, 继续讲错误向往传递, 知道被最近的Suspense
捕获到
js
<Suspense fallback={<Loading />}> // <--- 这个边界被使用,显示 Loading 组件
<Suspense> // <--- 这个边界被跳过,没有 fallback 属性
<Page />
</Suspense>
</Suspense>
- 更新后: 如果
Suspense
组件没有提供fallback
属性, 错误不会往外层传递, 而是展示为空
js
<Suspense fallback={<Loading />}> // <--- 不使用
<Suspense> // <--- 这个边界被使用, 将 fallback 渲染为 null
<Page />
</Suspense>
</Suspense>
- 为什么要做调整:
Suspense
的错误如果一直往外透传, 那么这样会导致混乱、难以调试的情况发生
9.8 Concurrent Mode(并发模式)
并不是一个功能, 而是一个底层设计
- 使用新版本的
createRoot(root).render
来挂载节点将启用并发模式
, 但是并没开启并发更新
, 要相启用相应的并发更新
需要使用相应的api
- 开启并发更新:
useTransition
, 如下代码useTransition
返回两个数组参数
isPending
表示是否正在等待中useTransition
接收一个回调函数, 函数中的状态修改将被标记为非紧急渲染
任务, 这样的话在大量的任务下也能保持UI
能够快速的响应, 从而来显著改善用户交互
js
import React, { useState, useEffect, useTransition } from 'react';
const App = () => {
const [list, setList] = useState([]);
const [isPending, startTransition] = useTransition();
useEffect(() => {
// 使用了并发特性,开启并发更新
startTransition(() => {
setList(new Array(10000).fill(null));
});
}, []);
return (
<>
{list.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};
- 开启并发更新:
useDeferredValue
, 返回一个延迟响应的值, 可以让一个state
延迟生效, 只有当前没有紧急更新时, 该值才会变为最新值! 同useTransition
一样都是标记了一次非紧急更新
js
import React, { useState, useEffect, useDeferredValue } from 'react';
const App = () => {
const [list, setList] = useState([]);
// 使用了并发特性,开启并发更新
const deferredList = useDeferredValue(list);
useEffect(() => {
setList(new Array(10000).fill(null));
}, []);
return (
<>
{deferredList.map((_, i) => (
<div key={i}>{i}</div>
))}
</>
);
};
useDeferredValue
与useTransition
的区别
- 相同: 从功能、作用以及内部的实现上来讲, 他们是一样的都是标记成了延迟更新任务
- 不同:
useTransition
是把更新任务变成了延迟更新任务, 而useDeferredValue
是产生一个新的值, 这个值作为延时状态。(一个用来包装方法, 一个用来包装值)
- 简单总结: 所有的东西都是基于
fiber
架构实现的,fiber
为状态更新提供了可中断的能力
- 并发更新的意义就是
交替执行
不同的任务(任务可以划分优先级, 高优先级的先执行), 当预留的时间不够用时,React
将线程控制权交还给浏览器, 等待下一帧时间到来, 然后继续被中断的工作 - 并发模式是实现并发更新的基本前提, 同时时间切片是实现并发更新的具体手段
9.9 几个新的 API
useId
:
- 参考: 为了生成唯一id, React18专门引入了新Hook: useId
- 主要适用于
SSR
(服务端渲染), 在应用的服务端或客户端之间生成唯一且稳定的id
- 背后的原理: 通过该组件在组件树中的层级结构来生成
id
这样就能够保证服务端或客户端之间的id
是稳定的
useSyncExternalStore
:- 参考: React 18 撕裂介绍
- 参考: React 的并发悖论
- 视图撕裂: 对于开启并发更新的
React
, 更新流程可能中断, 相同组件可能是在中断前后不同的宏任务中 render, 传递给他们的state
、props
可能并不相同, 这就导致同一次更新, 同一个状态前后UI
不一致的情况 React
的API
已经原生的解决的并发特性下的撕裂(tear
)问题, 但是对于redux
等外部框架它在控制状态时可能并非直接使用的React
的API
(useState
), 而是自己在外部维护了一个store
对象, 它脱离了React
的管理, 也就无法依靠React
自动解决撕裂问题。因此,React
对外提供了这样一个API
, 帮助这类框架开发者(有外部store
需求的)解决撕裂问题- 对于如何解决外部框架的并发特性下的撕裂(
tear
)问题,React
目前并没有好的一个方案, 目前useSyncExternalStore
的作用其实是状态管理库触发的更新都以同步的方式执行, 这样就不会有同步时机的问题了
useInsertionEffect
: 这个Hooks
只建议css-in-js
库来使用, 这个Hooks
执行时机在DOM
生成之后,useLayoutEffect
之前,它的工作原理大致和 useLayoutEffect
相同, 只是此时无法访问DOM
节点的引用, 一般用于提前注入<style>
脚本
十、使用 React 需要注意的事项有哪些?
-
state
不可直接进行修改改 -
不要在循环、条件或嵌套函数中调用
Hook
, 必须始终在React
函数的顶层使用Hook
-
列表渲染需要设置唯一且稳定的
key
-
忘记以大写字母作为组件的名称开头
-
最好保持组件的代码量较少, 一个组件对应一个功能, 这样不仅可以节省我们的时间, 也有助于我们调试代码
-
类组件中注意
this
指向: 在JSX
中给DOM
绑定事件时, 回调函数默认情况下无法访问当前组件, 即回调函数中this
不可用, 一般情况下我们可以通过bind()
来改变函数的上下文来使其可用, 当然这里其实还可以使用箭头函数声明函数 -
过度使用
Redux
: 尽管Redux
很有用, 但您无需使用它来管理应用程序中的每个状态
本小节到处结束...