React
1.React中的事件机制
JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了组件树的根上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。
2.React 高阶组件、Render props、hooks 有什么区别,为什么要不断迭代
-
HOC
简单来说就是一个函数接受一个组件,这个函数中又定义了一个组件,在这个新定义的组件中可以写一些属性方法等公共代码,然后把这些属性方法传给接受的组件,最后return新定义的组件。
// hoc的定义 function withSubscription(WrappedComponent, selectData) { return class extends React.Component { constructor(props) { super(props); this.state = { data: selectData(DataSource, props) }; } // 一些通用的逻辑处理 render() { // ... 并使用新数据渲染被包装的组件! return <WrappedComponent data={this.state.data} {...this.props} />; } }; // 使用 const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource, props) => DataSource.getBlogPost(props.id));
-
Render props
父组件向Render props组件传递了一个render函数,这个render函数定义了渲染内容;Render props组件通过this.props.render获取函数然后写入参数。
// DataProvider组件内部的渲染逻辑如下 class DataProvider extends React.Components { state = { name: 'Tom' } render() { return ( <div> <p>共享数据组件自己内部的渲染逻辑</p> { this.props.render(this.state) } </div> ); } } // 调用方式 <DataProvider render={data => ( <h1>Hello {data.name}</h1> )}/>
-
Hooks
可以在里面用其他的hook,直接return出数据,使用的时候useXxxx就行了,非常方便。
3.对React-Fiber的理解,它解决了什么问题?
-
可中断的渲染
以前react更新过程是同步的,一旦开始就会进行到底,这可能导致主线程被占据,页面卡顿做不了其他的事情。现在react把更新过程拆分成多个小任务,这样渲染的过程就能被其他优先级更高的任务插队,从而更好的响应用户交互。
-
优先级管理
比如用户输入,图表更新,在浏览器原生的任务调度机制下,用户的输入可能会被图表更新所堵塞,但是在Fiber的操作下,用户的输入是优先于图表更新的。 React Fiber 如何在任务调度方面比浏览器提供更细粒度的控制。
-
并发渲染
在传统架构中,React 是单线程的,无法充分利用多核 CPU 的优势。Fiber 使得 React 能够更好地利用现代多核处理器,提高渲染性能。
4.React.PureComponent
在函数组件里面就是React.memo
,react会自动浅比较一下传入props,变化了就更新组件。
5.类组件中的生命周期
-
挂载阶段
-
constructor
用于初始化 state 和绑定方法。
constructor(props) { super(props); this.state = { count: 0 }; }
-
static getDerivedStateFromProps
根据 props 更新 state。是一个静态方法。返回值会合并到state中
static getDerivedStateFromProps(nextProps, prevState) { if (nextProps.someValue !== prevState.someValue) { return { someValue: nextProps.someValue }; } return null; }
-
render
返回要渲染的元素。
render() { return <div>{this.state.count}</div>; }
-
componentDidMount
组件挂载后调用,可以进行 DOM 操作或数据请求。
componentDidMount() { // 进行数据请求或订阅操作 }
-
-
更新阶段
-
getDerivedStateFromProps
在每次组件更新时调用(见上)。
-
shouldComponentUpdate
决定组件是否应该重新渲染。返回
true
或false
。shouldComponentUpdate(nextProps, nextState) { return nextState.count !== this.state.count; }
-
render
重新渲染
-
getSnapshotBeforeUpdate
在更新前获取一些信息(例如,DOM 状态)。
getSnapshotBeforeUpdate(prevProps, prevState) { if (prevProps.someValue !== this.props.someValue) { return { scrollPosition: window.scrollY }; } return null; }
-
componentDidUpdate
组件更新后调用,可以进行 DOM 操作或数据请求。
componentDidUpdate(prevProps, prevState, snapshot) { if (snapshot !== null) { window.scrollTo(0, snapshot.scrollPosition); } }
-
-
卸载阶段
componentWillUnmount
componentWillUnmount() { // 进行清理工作,如取消订阅 }
6.哪些方法会触发 React 重新渲染?重新渲染会做些什么?
-
State 变化:
- 当组件的 state 发生变化时,会触发重新渲染。
- 使用
this.setState
方法更新 state 会导致重新渲染。
-
Props 变化:
- 当父组件传递给子组件的 props 发生变化时,子组件会重新渲染。
- 这也是为什么组件应该是纯函数的一个原因,即它们的输出应该完全由输入(props 和 state)决定。
-
强制更新:
- 使用
this.forceUpdate
方法可以强制组件重新渲染。通常不推荐使用这个方法,因为它绕过了 React 的优化机制。
- 使用
-
Context 变化:
- 当使用 React Context 时,如果 Context 的值发生变化,所有使用该 Context 的组件都会重新渲染。
-
父组件变化:
- 父组件的渲染会带着子组件一起重新渲染一遍。
重新渲染就会用diff算法来对新旧VNode进行比对。
7.React如何判断什么时候重新渲染组件?
写了componentShouldUpdate就根据返回值来判断,true就重新渲染,false就放弃这次渲染。
8.无状态组件
就是没有自己的状态,就负责展示,可以传递props给它展示。
9.如何获取DOM元素
-
在类组件中使用
ref
在类组件中,你可以通过创建一个
ref
并将其附加到一个 DOM 元素上来获取对该元素的引用。import React, { Component } from 'react'; class MyComponent extends Component { constructor(props) { super(props); // 创建一个 ref this.myRef = React.createRef(); } componentDidMount() { // 访问 DOM 元素 console.log(this.myRef.current); } render() { return <div ref={this.myRef}>Hello, World!</div>; } } export default MyComponent;
10.React中可以在render访问refs吗?为什么?
不能,要在render之后。
11.对React的插槽(Portals)的理解,如何使用,有哪些使用场景
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
React 提供了 ReactDOM.createPortal
方法来创建一个 Portal。这个方法接收两个参数:
- 子组件(或元素)
- DOM 节点
import React from 'react';
import ReactDOM from 'react-dom';
class Modal extends React.Component {
render() {
return ReactDOM.createPortal(
<div className="modal">
{this.props.children}
</div>,
document.getElementById('modal-root')
);
}
}
// 在你的 HTML 文件中,需要有一个 id 为 'modal-root' 的元素,就是入口文件
// <div id="modal-root"></div>
class App extends React.Component {
render() {
return (
<div className="app">
<h1>My App</h1>
<Modal>
<p>This is a modal!</p>
</Modal>
</div>
);
}
}
export default App;
应用:弹窗类用的最多。
12.在React中如何避免不必要的render?
- shouldComponentUpdate
- PureComponent
- React.memo
13.对 React context 的理解
用来组件通信的
import React from 'react';
// 创建一个 Context 对象,并设置默认值
const ThemeContext = React.createContext('light');
export default ThemeContext;
//父组件
<ThemeContext.Provider value={this.state.theme}>
<div>
<button onClick={this.toggleTheme}>Toggle Theme</button>
<ThemedComponent />
</div>
</ThemeContext.Provider>
//子组件
<ThemeContext.Consumer>
{theme => (
<div style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
<h2>Themed Component</h2>
<p>The current theme is {theme}</p>
</div>
)}
</ThemeContext.Consumer>
14.React中什么是受控组件和非控组件
指那些表单数据由React组件状态state控制的表单元素,表单元素的值和组件的状态同步。
15.React中refs的作用是什么?有哪些应用场景?
直接获取DOM元素或者React组件实例,但是函数组件没有实例,直接获取的话会报错,所以要用到forwardRef
const A = forwardRef((props,ref)=>{
//接收两个参数
// props表示传参
// 转发ref给其他能获取到实例的元素
return <input ref={ref} type="text" />
})
import React, { useRef, useEffect, forwardRef } from 'react';
// 定义一个函数组件并使用 React.forwardRef 转发 ref
const FunctionComponent = forwardRef((props, ref) => (
<input ref={ref} type="text" />
));
//------------------------------------------------------------------
const ParentComponent = () => {
const inputRef = useRef(null);
useEffect(() => {
// 组件挂载后,让输入框自动获得焦点
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
return <FunctionComponent ref={inputRef} />;
};
export default ParentComponent;
16.React中除了在构造函数中绑定this,还有别的方式吗?
一起列举出来
-
在构造函数中绑定
this
class MyComponent extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; this.handleClick = this.handleClick.bind(this); } handleClick() { this.setState({ count: this.state.count + 1 }); } render() { return <button onClick={this.handleClick}>Click me</button>; } }
-
使用箭头函数定义类方法(类字段语法)
class MyComponent extends React.Component { state = { count: 0 }; handleClick = () => { this.setState({ count: this.state.count + 1 }); }; render() { return <button onClick={this.handleClick}>Click me</button>; } }
-
在jsx中使用箭头函数
class MyComponent extends React.Component { state = { count: 0 }; handleClick() { this.setState({ count: this.state.count + 1 }); } render() { return <button onClick={()=>this.handleClick()}>Click me</button>; } }
-
在jsx中用bind,
this.handleClick.bind(this);
17.React组件的构造函数有什么作用?它是必须的吗?
构造函数的主要作用:初始化状态和绑定事件处理器中的 this
。
所以如果props不涉及初始化,又不需要处理this的问题的话,那就可以不用构造函数。
class LikeButton extends React.Component {
constructor() {
super();
this.state = {
liked: false
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({liked: !this.state.liked});
}
render() {
const text = this.state.liked ? 'liked' : 'haven't liked';
return (
<div onClick={this.handleClick}>
You {text} this. Click to toggle.
</div>
);
}
}
ReactDOM.render(
<LikeButton />,
document.getElementById('example')
);
18.setState的用法和原理
推荐使用函数式的,可以获取到正确的状态。
this.setState((prevState, props) => ({
...
}),callback);
//第二个参数是回调函数,state完成更新后执行
用useState()
也是的,如果依赖旧状态,也建议写函数式的
import React, { useState } from 'react';
function Counter() {
// 声明一个叫做 "count" 的状态变量,并赋初始值为 0
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount((prevCount )=>prevCount +1)}>
Click me
</button>
</div>
);
}
- 状态更新请求: 当
setState
被调用时,React 接收到一个状态更新请求,这个请求可以是一个对象或者一个函数。 - 合并状态: React 将新的状态与当前状态合并(浅合并),但不会立即更新
this.state
。相反,React 会将状态更新放入一个队列中,稍后处理。 - 批量更新: 为了提高性能,React 会批量处理多个
setState
调用。在事件处理、生命周期方法或合成事件中,状态更新不会立即生效,而是会被暂存并在稍后的一个批处理中一起处理。但是如果在一些原生事件中,setState
可能是同步的,因为合成事件不会处理。 - 重新渲染: 批处理完成后,React 会重新计算组件的状态和属性,并触发组件的重新渲染。React 会调用
render
方法生成新的虚拟 DOM,然后通过与旧的虚拟 DOM 进行对比(diffing),找到需要更新的部分,并进行实际的 DOM 更新。
19.React中defaultProps
在 React 16.3 及更高版本中,推荐使用 defaultProps
静态属性来定义默认属性值:
-
类组件
class MyComponent extends React.Component { static defaultProps = { name: 'Default Name' }; render() { return <div>{this.props.name}</div>; } } // 使用 defaultProps 的效果与 getDefaultProps 相同 <MyComponent /> // 渲染结果:<div>Default Name</div>
-
函数组件
直接使用默认入参就行了
20.React中的setState和replaceState的区别是什么?
replaceState已经被废弃了,不了解。
21.React性能优化在哪个生命周期?它优化的原理是什么?
shouldComponentUpdate(nextProps,nextState),可以决定组件是否更新。
22.state和 props 触发更新的生命周期分别有什么区别?
getDerivedStateFromProps
仅在 props
变化时调用。
23.React-Router的实现原理是什么?
客户端路由实现的思想:
- 基于 hash 的路由:通过监听
hashchange
事件,感知 hash 的变化 -
- 改变 hash 可以直接通过 location.hash=xxx
- 基于 H5 history 路由:
-
- 改变 url 可以通过 history.pushState 和 resplaceState 等,会将URL压入堆栈,同时能够应用
history.go()
等 API - 监听 url 的变化可以通过自定义事件触发实现
- 改变 url 可以通过 history.pushState 和 resplaceState 等,会将URL压入堆栈,同时能够应用
24.React-Router怎么设置重定向?
使用 <Navigate>
组件
25.React-Router如何获取URL的参数?
-
路径参数
// 定义路由 <Route path="/user/:id" component={UserComponent} /> // 获取参数 const { id } = useParams();
-
查询参数
// 定义路由 <Route path="/search" component={SearchComponent} /> // 获取查询参数 const [searchParams] = useSearchParams(); const query = searchParams.get('query');
-
state参数
const navigate = useNavigate(); navigate('/user', { state: { id: 123, name: 'John Doe' } }); // 导航并通过 state 传递参数 --------------------------- const location = useLocation(); const userState = location.state; // 获取通过 state 传递的参数
-
其他的钩子
import React from 'react'; import { useMatch } from 'react-router-dom'; const About = () => { const match = useMatch('/about'); return ( <div> <h1>About Page</h1> {match && <p>This is the About page</p>} </div> ); }; export default About;
26.useLayoutEffect
用法和useEffect一样,普通的useEffect会再浏览器完成绘制之后执行,但是useLayoutEffect会在DOM变更后就执行(浏览器绘制之前) ,这有什么卵用呢?
比如说要写一个tooltip,当上方的空间不够了,那就应该尝试出现在下方,所以要在浏览器完成绘制之前也就是DOM一旦变更就可以去测量上方的空间够不够了,上方空间不够用立马就更改他的位置,让浏览器再次渲染一遍。
27.React 数据持久化有什么实践吗?
redux-persist
28.Redux原理
在React中,组件通过connect连接到 Redux ,如果要访问 Redux,需要dispatch一个包含 type和负载(payload) 的 Action。Action 中的 payload 是可选的,Action 将其转发给 Reducer。
当Reducer收到Action时,通过 switch…case 语法比较 Action 中type。 匹配时,更新对应的内容返回新的 state。
当Redux状态更改时,连接到Redux的组件将接收新的状态作为props。当组件接收到这些props时,它将进入更新阶段并重新渲染 UI。
import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from './actions';
function App({ count, increment, decrement }) {
return (
<div>
<button onClick={increment}> + </button>
<p>{count}</p>
<button onClick={decrement}> - </button>
</div>
)
}
//把store中的数据映射到组件的props中
const mapStateToProps = state => {
return {
count: state.count,
}
}
//把actions映射到组件的props中
const mapDispatchToProps = {
increment,
decrement,
}
// 使用 connect 函数连接组件和 Redux store
export default connect(mapStateToProps, mapDispatchToProps)(App)
29.reducer
reducer
函数接收当前状态 state
和一个动作 action
,然后根据 action.type
返回新的状态。
函数组件中使用useReducer
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
</div>
);
}
export default Counter;
30.useTransition
延迟的更新:使用 useTransition
标记的状态更新会被延迟,直到所有高优先级的更新完成。
可中断的更新:过渡更新可以被高优先级的更新中断,以保持应用的响应性。
指示过渡状态:可以获取过渡是否进行中的状态,以便在 UI 中展示加载指示器等。
import React, { useState, useTransition } from 'react';
function App() {
const [isPending, startTransition] = useTransition();
const [count, setCount] = useState(0);
const handleClick = () => {
startTransition(() => {
setCount(count + 1);
});
};
return (
<div>
<button onClick={handleClick}>Increment</button>
{isPending ? <p>Loading...</p> : <p>Count: {count}</p>}
</div>
);
}
export default App;
31.useDeferredValue
创造一个延迟更新的值,第一次返回旧值,当其他优先级高的任务执行完了再创造一个新值。
32.React.lazy()和Suspense
用于路由懒加载
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Suspense, lazy } from 'react';
const Home = lazy(() => import('./routes/Home'))
const count = lazy(() => import('./routes/count'))
export default class App extends Component {
render() {
return (
<div>
<Router>
<Suspense fallback={<h1>Loading...</h1>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/count" component={count}/>
...
</Switch>
</Suspense>
</Router>
</div>
)
}
}
33.JSX是怎么变成真实的DOM的
JSX 解析和编译:JSX 通过 Babel 编译成 React.createElement
调用。
创建虚拟 DOM:React.createElement
创建虚拟 DOM 对象。
构建虚拟 DOM 树:React 通过虚拟 DOM 对象构建虚拟 DOM 树。
比较和更新虚拟 DOM 树:React 比较新旧虚拟 DOM 树,找出差异。
更新真实 DOM:React 将差异转换为实际的 DOM 操作,更新真实 DOM。
渲染真实 DOM:React 将虚拟 DOM 树中的元素渲染成真实的 DOM 元素,并插入页面。
34.为什么 Hooks 不能在条件判断中使用?
React 依赖 Hook 调用顺序来正确关联状态和副作用。
35.如何通过 DOM 元素找到对应的 Fiber 对象
具体来说,React 在内部创建 DOM 元素时,会在 DOM 元素上附加一个 __reactFiber$
前缀的属性,该属性的值是 fiber
对象。
36.React源码
36-1.JSX转换成ReactElement的过程
调用了createElement方法
/**
* 创建React Element
* type 元素类型
* config 配置属性
* children 子元素
* type 元素类型
* 1.分离props属性和特殊属性
* 2.将子元素挂在到props.children中
* 3.为props属性赋默认值
* 4.创建并返回ReactElement
*/
function createElement(type,config,children){
...
}
如何判断一个参数是ReactElement?
看对象的$$typeof属性
object.$$typeof === REACT_ELEMENT_TYPE
36-2.React架构
- Scheduler(调度层):调度任务的优先级,高优先级的先进入协调器。
- Reconciler(协调层):构建Fiber数据结构,对比Fiber对象找出差异,记录Fiber对象要进行的DOM操作。
- Renderer(渲染层):将变化的部分渲染到页面上。
调度层和协调层的工作是在内存当中进行的,渲染层设定的就是不可以打断的,所以不会出现DOM渲染不完整的问题。
36-3. Fiber数据结构
type Fiber= {
/************DOM实例相关************/
//表示Fiber节点的类型 用于协调和渲染的时候做判断的
tag:WorkTag,
//React元素的类型 DOM元素就是字符串("div","span") React元素就是组件的构造函数或者类
type:any,
//与Fiber节点关联的实例对象,对于DOM元素来说是实际的DOM节点,对于类组件来说是组件实例
stateNode: any,
/************构建Fiber树相关的************/
//父Fiber节点,表示树结构中的父节点
return: Fiber | null,
//子Fiber节点,表示树结构中的第一个子节点。
child: Fiber | null,
//兄弟Fiber节点,表示树结构中当前节点的下一个兄弟节点。
sibling: Fiber | null,
//有了这些数据,无论出于Fiber树种哪个位置,都能快速的找到父级 子级 兄弟级节点了
//指的是 workInProgress Fiber节点
alternate: Fiber | null,
/************状态数据相关的************/
//将用于渲染的新属性
pendingProps: any,
//上一次渲染时使用的属性
memoizedProps: any,
//上一次渲染时使用的状态
memoizedState: any,
/************副作用相关************/
//该Fiber对应的组件所产生的状态更新都会放在这个队列里
updateQueue:updateQueue<any> | null,
//用来记录当前Fiber要执行的DOM操作,比如说当前对应的DOM节点要做插入 删除等操作
effectTag:sudeEffectTag,
//指向当前Fiber节点的第一个副作用Fiber节点。
firstEffect:Fiber | null,
//指向当前Fiber节点的下一个副作用Fiber节点。
nextEffect:Fiber | null,
//指向当前Fiber节点的最后一个副作用Fiber节点。
lastEffect:Fiber | null,
//通过设置不同的expirationTime,React可以控制各个更新任务的执行顺序,确保用户交互等高优先级任务能及时响应。
expirationTime:ExpirationTime,
//表示当前Fiber节点的渲染模式 并发渲染,同步渲染,不使用特定的渲染模式等等
mode:TypeOfMode,
}
36-4.双缓存技术
React最多同时存在两颗Fiber树,屏幕中看到的讲座current Fiber树,当发生更新的时候,React会在内存里面重新构建一颗workInProgress Fiber树。当workInProgress Fiber树构建好了直接替换current Fiber树,更新就更流畅了。
36-5.FiberRoot和RootFiber
-
FiberRoot
是React应用的顶层结构,包含了整个应用的渲染状态和调度信息。它主要负责管理整个应用的渲染和更新过程。
-
RootFiber
是整个React应用的顶层Fiber节点,是
FiberRoot
的一个属性。rootFiber
表示当前渲染的Fiber树的根节点,是从这个节点开始递归遍历整个Fiber树。
36-6.整个过程
render
方法的调用其实是returnlegacyRenderSubtreeIntoContainer
的结果。legacyRenderSubtreeIntoContainer
是为container
(<div id = 'app'>
)创建或者获取FiberRoot
,然后开始下一步调用updateContainer
。- 更新过程就是调用了
updateContainer
,updateContainer
会创建任务(初始化渲染或者更新渲染)放在任务队列里面,等待浏览器空闲执行。 - 任务执行调用
scheduleUpdateOnFiber
,这会进入渲染阶段,渲染阶段包括了协调和提交两个子阶段。 - 在协调阶段会对比新旧Fiber树的差异,React会建立一个Effect List,用于在提交阶段执行所有的副作用。每个Fiber节点都会有一个effectTag,用于标记要执行的操作,例如更新、插入、删除等等。
- 提交阶段又分了三个小阶段,Before Mutation、Mutation和Layout。
- Before Mutation会处理DOM更新前要处理的副作用,例如
getSnapshotBeforeUpdate
。 - Mutation就处理DOM节点的添加更新等。
- Layout会执行所有需要再DOM变更之后处理的副作用例如
componentDidMount
和componentDidUpdate
。 - DOM的更新。
- 在所有的更新完成之后,React会执行需要清理的任务,例如
useEffect
中的回调函数。
项目优化
框架中的项目都差不多
-
编码阶段
- 使用路由懒加载、异步组件
- 第三方模块按需导入
- 图片懒加载
- 防抖和节流
- 减少响应式数据
- 减少组件的嵌套
- 虚拟列表
-
打包阶段
- SplitChunksPlugin分包
- Tree Shaking树摇
- TerserPlugin用于压缩JS文件
- css-minimizer-webpack-plugin压缩CSS文件
- hash生成唯一的文件名
- thread-loader使用多线程加载,提升构建速度。
- cache-loader缓存编译的结果
- happyPack使用多线程加载,提升构建速度。
- webpack-bundle-analyzer分析打包的结果
- SpeedMeasurePlugin Webpack 构建过程中的各个步骤所花费的时间。
-
用户体验
- 使用cdn加载第三方模块
- 利用缓存
总结
专注收集整理Java面试题,不定期给大伙分享面试中的高频题与大厂难题。
如果你觉得文章对你有用,可以点赞和关注哦!❤
欢迎大家在评论区留下你宝贵的建议📢