React 面试题
React
React
hooks api
- useState
使用场景:当你需要在函数组件中存储状态时使用。 用法: const [state, setState] = useState(initialState); 作用:该Hook返回一个 state 变量和一个更新该变量的函数。
- useEffect
使用场景:用于处理副作用,相当于类组件中的 componentDidMount,componentDidUpdate 和 componentWillUnmount生命周期。 用法:useEffect(()=>{/副作用逻辑/},[dependencies]); 作用:执行副作用操作,如数据获取、订阅、手动变更DOM等。
- useReducer
使用场景:当 state 逻辑复杂或下一个 state 依赖之前的值时。 用法: const [state, dispatch] = useReducer(reducer, initialState); 作用:通过 reducer 函数管理复杂组件的状态逻辑。
- useCallback
使用场景:缓存函数,以避免子组件因接收到新的函数实例而进行不必要的重新渲染。 用法:const memoizedCallback = useCallback(() =>{/* 回调逻辑*/},[dependencies]);** **用法:返回一个记忆版本的回调函数。
- useMemo
使用场景:缓存昂贵的计算结果,只有依赖项变化时才重新计算 用法: const memoizedValue = useMemo(() =>computeExpensiveValue(a, b), [a, bl); 作用:返回一个记忆化的值。
场景1:执行某函数需要大量时间,使用useMemo来优化,在不必要执行函数的时候不执行函数
场景2:每次组件更新会重新执行,内部的引用类型变量会重新创建,这会导致使用到引用类型变量的组件重新渲染,使用useMemo来让每次的变量相同
- useRef
使用场景:需要持久化一个可变的值而不造成组件的重新渲染。 用法: const myRef = useRef(initialValue); 作用:返回一个可变的 ref 对象,.current 属性被初始化为传入的参数 (initialValue)。
- useLayoutEffect
使用场景:与 useEffect 类似,但它会在所有的 DOM 更改之后同步触发重渲染。 **用法:useLayoutEffect(() =>{/*副作用逻辑 */},[dependencies]);** 作用:读取布局并同步触发重新渲染。
- useContext
使用场景:当你需要访问 React Context 中的值时使用。 用法:const contextValue = useContext(MyContext); 作用:允许组件订阅 React Context 无须嵌套。
| useEffect | useLayoutEffect | |
|---|---|---|
| 调用时机 | 组件渲染完成后异步执行 | 组件渲染完成后同步执行 |
| 执行时机 | 浏览器完成绘制后,对用户来说不可见 | 浏览器绘制前,对用户来说可见 |
| 阻塞渲染 | 不会阻塞浏览器的渲染过程 | 会阻塞浏览器的渲染过程 |
| 副作用操作 | 适用于大多数副作用操作 | 适用于需要立即执行、对布局有影响的副作用操作 |
| 清理函数 | 可以返回一个清理函数,用于清除副作用操作 | 可以返回一个清理函数,用于清除副作用操作 |
| 使用建议 | 在大多数情况下使用 useEffect | 在需要同步执行、对布局有影响的情况下使用 useLayoutEffect |
| 底层函数 | 调用的 mountEffectImpl方法 | 调用的 mountEffectImpl方法,在使用上也没什么差异,基本可以直接替换 |
useState为什么返回的是一个数组
因为React团队认为使用数组可以更好地解决一些问题。就是考察数组和对象的**解构赋值**:
如果 useState 返回数组,那么你可以顺便对数组中的变量命名,代码看起来也比较干净。而如果是对象的话返回的值必须和 useState 内部实现返回的对象同名,
useState同步更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(2);
fn(); // 点击后打印 0
};
const fn = () => {
console.log(count);
};
return (
<div className="App">
<button onClick={handleClick}>点击</button>
</div>
);
}
为什么在 fn 中打印出来的 count 是 0 呢?
因为 React 合成事件中,为了减少 render 次数,提高性能,React 会将多次状态更新收集起来,最后一次更新,所以在 React 合成事件中,状态更新是异步的,fn 和 setCount 在同一个宏任务中,这时候 React 还没有 render,所以获取到的 count 还是上一次闭包里的值 0。
- 使用useEffect监听state变化,当
useEffect监听到state变化时,再去执行下一步操作; - 用回调函数传参 方法获取最新的值,同时用 promise 变成同步方法 ;
- 在setState(value)的同时,把 value 赋一份给
useRef,** **后面的业务逻辑,都用a.current去操作。
什么是JSX
JSX 是 JavaScript XML 的简写。是 React 使用的一种文件,它利用 JavaScript 的表现力和类似 HTML 的模板语法。这使得 HTML 文件非常容易理解。此文件能使应用非常可靠,并能够提高其性能。
1
2
3
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
会编译为:
1
2
3
4
5
React.createElement(
MyButton,
{color: 'blue', shadowSize: 2},
'Click Me'
)
React的特点
- 一切皆为组件;
- 使用JSX (JavaScript扩展);
- 它操作虚拟DOM 而不是真正的DOM;
- 遵循单向数据流或单向数据绑定;
- 它可以进行服务器端渲染。
React的优点
- 提高了应用程序的性能;
- 可以在客户端和服务器端使用;
- 因为JSX,代码的可读性增强。
React的局限性
- React只是一个库,不是一个成熟的框架
- 它的库非常大,需要时间来理解
- 对于新手程序员来说,理解起来有点困难
- 由于使用内联模板和JSX,编码变得很复杂
React 的工作原理
React 会创建一个虚拟 DOM(virtual DOM)。当一个组件中的状态改变时,React 首先会通过 “diffing” 算法来标记虚拟 DOM 中的改变;
第二步是调节(reconciliation),会用 diff 的结果来更新 DOM。
Model改变之后可能是调用了setState触发了virtual dom的更新再用diff算法来把virtual DOM比较real DOM看看是哪个dom节点更新了再渲染real dom
区分真实DOM和虚拟DOM
| Real DOM | Virtual DOM |
|---|---|
| + 更新缓慢。 + 可以直接更新 HTML。 + 如果元素更新,则创建新DOM。 + DOM操作代价很高。 + 消耗的内存较多。 | + 更新更快。 + 无法直接更新 HTML。 + 如果元素更新,则更新 JSX 。 + DOM 操作非常简单。 + 很少的内存消耗。 |
为什么虚拟dom会提高性能
虚拟的DOM的核心思想:
- 提供一种方便的工具,使得开发效率得到保证
- 保证最小化的DOM操作,提高执行效率。
虚拟 dom 相当于在js和真实dom中间加了一个缓存利用dom diff算法避免了没有必要的dom操作从而提高性能
react diff 原理
- 把树形结构按照层级分解,只比较同级元素。
- 给列表结构的每个单元添加唯一的 key 属性,方便比较。
- React 只会匹配相同 class 的 component(这里面的 class 指的是组件的名字)。
- 合并操作,调用 component 的 setState 方法的时候, React 将其标记为 dirty.到每一个事件循环结束, React 检查所有标记 dirty 的 component 重新绘制.
- 选择性子树渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能。
状态(state)和属性(props)之间的区别
| props | state | |
|---|---|---|
| 数据来源不同 | 从父组件传递给子组件的**属性** | 组件内部维护的**状态** |
| 可读性和可写性不同 | 只读的,即子组件无法直接修改它们的值 | 可读可写的,即组件自己维护的状态 |
| 影响组件更新的方式不同 | 当props发生变化时,也会触发组件重新渲染,但如果父组件重新渲染时,props没有发生变化,那么子组件不会重新渲染。 | state发生变化时,会触发组件重新渲染 |
| 作用域不同 | 可以在组件内部和外部访问,因为props是从父组件传递而来的。 | 在组件内部访问 |
| 使用场景不同 | 实现组件之间的数据传递 | 管理组件内部状态 |
refs 是什么
Refs 是 React 提供能访问 DOM 元素或组件实例的句柄。我们可以为元素添加 ref 属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CustomForm extends Component {
handleSubmit = () => {
console.log("Input Value: ", this.input.value)
}
render () {
return (
<form onSubmit={this.handleSubmit}>
<input
type='text'
ref={(input) => this.input = input} />
<button type='submit'>Submit</button>
</form>
)
}
}
//上述代码中的 input 域包含了一个 ref 属性,该属性声明的回调函数会接收 input 对应的 DOM 元素,我们将其绑定到 this 指针以便在其他的类函数中使用。
React 中 keys 的作用
Keys是React用于追踪哪些列表中元素被 增删改 的辅助标识。
在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性;
在 React Diff 算法中React 会借助元素的 Key 值来判断该元素是新创建的还是被移动而来的从而减少不必要的元素重渲染;
此外React 还需要借助 Key 值来判断元素与本地状态的关联关系。
setState 何时异步何时同步
| 同步 | 异步 |
|---|---|
| 原生事件中 | 合成事件中 |
| setTimeout中 | 钩子函数中(生命周期函数) |
有状态和无状态组件
| 有状态组件 | 无状态组件 |
|---|---|
| 1. 在内存中存储有关组件状态变化的信息 | 1. 计算组件的内部的状态 |
| 2. 有权改变状态 | 2. 无权改变状态 |
| 3. 包含过去、现在和未来可能的状态变化情况 | 3. 不包含过去,现在和未来可能发生的状态变化情况 |
| 4. 接受无状态组件状态变化要求的通知,然后将 props 发送给他们。 | 4.从有状态组件接收 props 并将其视为回调函数。 |
React 中三种构建组件的方式
React.createClass()、ES6 class 和无状态函数。
(在构造函数中)调用 super(props) 的目的是什么
在 super() 被调用之前,子类是不能使用 this 的,在 ES2015 中,子类必须在 constructor 中调用 super()。传递 props 给 super() 的原因则是便于(在子类中)能在 constructor 访问 this.props。
何为受控组件(controlled component)
在 HTML 中,类似 <input>, <textarea> 和 <select> 这样的表单元素会维护自身的状态,并基于用户的输入来更新。当用户提交表单时,前面提到的元素的值将随表单一起被发送。但在 React 中会有些不同,包含表单元素的组件将会在 state 中追踪输入的值,并且每次调用回调函数时,如 onChange 会更新 state,重新渲染组件。一个输入表单元素,它的值通过 React 的这种方式来控制,这样的元素就被称为”受控元素”。
何为高阶组件(higher order component)HOC
高阶组件是一个以组件为参数并返回一个新组件的函数。HOC 运行你重用代码、逻辑和引导抽象。最常见的可能是 Redux 的 connect 函数。除了简单分享工具库和简单的组合,HOC 最好的方式是共享 React 组件之间的行为。如果你发现你在不同的地方写了大量代码来做同一件事时,就应该考虑将代码重构为可重用的 HOC。
createElement 和 cloneElement
React.createElement():JSX 语法就是用 React.createElement() 来构建 React 元素的。它接受三个参数,第一个参数可以是一个标签名。如 div、span,或者 React 组件。第二个参数为传入的属性。第三个以及之后的参数,皆作为组件的子组件。
1
2
3
4
5
React.createElement(
type, //标签名
[props], //传入的属性
[...children] //子组件
)
React.cloneElement() 与 React.createElement() 相似,不同的是它传入的第一个参数是一个 React 元素,而不是标签名或组件。新添加的属性会并入原有的属性,传入到返回的新元素中,而就的子元素将被替换。
1
2
3
4
5
React.cloneElement(
element, //React 元素
[props], //传入的属性
[...children] //子组件
)
简述Flux 思想
Flux 的最大特点,就是数据的”单向流动”。
- 用户访问 View
- View 发出用户的 Action
- Dispatcher 收到 Action,要求 Store 进行相应的更新
- Store 更新后,发出一个”change”事件
- View 收到”change”事件后,更新页面
简述Redux、Flux和它们的区别
Redux是一个应用数据流框架,主要是解决了组件间状态共享的问题,原理是集中式管理;- 主要有三个核心方法,
action,store,reducer; - 工作流程:是 view 调用 store 的 dispatch 接收 action 传入 store,reducer 进行 state 操作,view 通过 store 提供的 getState 获取最新的数据;
Flux也是用来进行数据操作的,有四个组成部分action,dispatch,view,store,工作流程是 view 发出一个 action,派发器接收 action,让 store 进行数据更新,更新完成以后 store 发出 change,view 接受 change 更新视图;- Redux 和 Flux 主要**区别**在于 Flux 有多个可以改变应用状态的 store,在 Flux 中 dispatcher 被用来传递数据到注册的回调事件,但是在 redux 中只能定义一个可更新状态的 store,redux 把 store 和 Dispatcher 合并,结构更加简单清晰
- 新增 state,对状态的管理更加明确,通过 redux,流程更加规范了,减少手动编码量,提高了编码效率,同时缺点时当数据更新时有时候组件不需要,但是也要重新绘制,有些影响效率。一般情况下,我们在构建多交互,多数据流的复杂项目应用时才会使用它们
Redux 有什么缺点
- 一个组件所需要的数据,必须由父组件传过来,而不能像 flux 中直接从 store 取。
- 当一个组件相关数据更新时,即使父组件不需要用到这个组件,父组件还是会重新 render,可能会有效率影响,或者需要写复杂的 shouldComponentUpdate 进行判断。
React 生命周期
Mounting(挂载):已插入真实 DOM
constructor():在 React 组件挂载之前,会调用它的构造函数。getDerivedStateFromProps(): 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。render():render() 方法是 class 组件中唯一必须实现的方法。componentDidMount():在组件挂载后(插入 DOM 树中)立即调用。
Updating(更新):每当组件的 state 或 props 发生变化时,组件就会更新。
getDerivedStateFromProps():在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。shouldComponentUpdate():当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。render(): render() 方法是 class 组件中唯一必须实现的方法。getSnapshotBeforeUpdate():在最近一次渲染输出(提交到 DOM 节点)之前调用。componentDidUpdate():在更新后会被立即调用。
Unmounting(卸载):移除 DOM
componentWillUnmount():在组件卸载及销毁之前直接调用。
🤡🤡🤡** 老版本生命周期:**
- 挂载阶段执行顺序
- constructor
- componentWillMount
- render
- componentDidMount
- 更新阶段执行顺序
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
- 销毁阶段
- componentWillUnmount
🤑🤑🤑** 函数组件中:**
useEffect分别用于模拟componentDidMount,componentDidUpdate和componentWillUnmount。
setState执行机制
- 在组件生命周期或React合成事件中,setState是异步
- 在setTimeout或者原生dom事件中,setState是同步
React 性能优化
- 减少不必要的组件更新:当组件的状态或属性发生变化时,并不是所有的组件都需要重新渲染。通过使用 React.memo、useMemo 或 useCallback 等技术,可以避免不必要的渲染,提高性能;
- 避免使用内联函数:每次render渲染时,都会创建一个新的函数实例,应该在组件内部创建一个函数,讲事件绑定到函数,这样每次调用render时,就不会创建单独的函数实例;
- 使用 PureComponent:如果一个 React 组件的 props 和 state 没有发生改变,那么它就可以被视为一个纯组件。对于这类组件,可以使用 React 提供的 PureComponent 基类,它会自动进行浅层比较,如果 props 和 state 没有变化,则不会重新渲染;
- 组件按需加载:对于一些复杂的组件,可以采用按需加载的方式,即只在需要的时候才加载;
- 使用 shouldComponentUpdate:在 React 中,可以通过覆盖 shouldComponentUpdate 方法来控制组件的更新。这个方法会在 props 或 state 发生变化时被调用,开发者可以在这里进行一些优化;
- 使用 React.lazy 和 Suspense 进行****懒加载:对于一些大型的组件或库,可以使用 React.lazy 进行懒加载,而 Suspense 则可以用来处理加载过程中出现的空白;
- 避免在循环中使用 createElement:在循环中创建大量的元素会导致性能下降,可以通过使用 React.Fragment 或其他技巧来避免;
- 使用 debounce 或 throttle 进行节流:对于一些频繁触发的事件;
- 服务端渲染。
添加原生事件不移除为什么会内存泄漏
在React中添加原生事件(addEventListener)不移除会导致内存泄漏的原因是:
当事件监听器被添加到DOM元素上时,它会**持有对组件实例的引用**,而这个引用不会被自动清除。因此,即使组件被卸载或条件改变,由于事件监听器的存在,组件实例不会被垃圾回收,从而导致内存泄漏。
个绍 Function Component(函数组件)
- 是一种纯函数,通过 props 接收外部数据,并通过 JSX 输出组件。
- 是一种无状态组件,没有state和生命周期方法,使用 Hooks。
React 数据流
React数据流是一个单向数据流的过程,即从父节点传递到子节点。
在React中,组件是简单易于把握的,它们只需从父节点获取props渲染即可。如果顶层组件的某个prop改变了,React会递归地向下遍历整棵组件树,重新渲染所有使用这个属性的组件。
React数据流包括props和state两种方式。Props是父组件向子组件传递数据的一种方式,子组件通过props接收数据。State是组件内部的状态,只能在组件内修改。
个绍React context
React Context提供了一种在组件之间共享值的方式,而不必显式地通过组件树的逐层传递props;
react 如何进行代码拆分,拆分原则是什么
React进行代码拆分的原则:
- 目录设计规范:根据作用和职责进行拆分。
- 模块定义规范:根据业务进行拆分。
- 代码设计规范:一般的代码规范。
- 程序设计规范:一般原则(高内聚、低耦合等等)。
在我们的React项目中:
- api层面单独封装,对外暴露http请求的结果。
- 数据层使用react-redux,异步中间件使用redux-thunk。
- 视图层使用redux层面的传递过来的数据,修改逻辑也是重新触发action更改props。
- 静态类型的资源单独放置。
- 公共组件、高阶组件、插件分开放置。
- 工具类文件单独放置。
调用 setState的时候,发生了什么
- 合并状态:React会将新的状态对象与旧的状态对象进行合并,生成一个新的状态对象。
- 调度更新:为了优化性能,React 可能会批量处理多个 setState 调用,从而减少不必要的重新渲染。
- 开始React更新程:基于状态变化和 props 的变化确定哪些组件需要重新渲染。
- 调用生命周期方法(类组件):
对于类组件,React 会在更新过程中调用相应的生命周期方法。
1
2
- 在 React 16.3 之前,这些方法可能包括 componentWillUpdate(已被弃用)和 componentDidUpdate。
- 在 React 16.3 及更高版本中,componentWillUpdate 已被弃用,并引入了新的生命周期方法,如 getDerivedStateFromProps 和 getSnapshotBeforeUpdate。
5. 重新渲染:调用组件的 render 方法来生成新的 React 元素树 6. DOM 更新:通过Diffing 算法来比较新旧的 DOM 树,进行局部更新,而不是重新渲染整个页面。