题目 9 - 题目 16
9.说说 React commit 阶段的执行过程
React 的 commit 阶段包括以下主要步骤:
- 提交 Fiber 树,准备处理更新。
- 递归遍历 Fiber 树,应用副作用操作。
- 插入、更新和删除实际 DOM 元素。
- 调用生命周期方法和执行副作用回调。
- 更新 Fiber 树,清理副作用标记。
- 浏览器进行布局计算和绘制,呈现更新后的页面内容。
这个阶段的核心任务是将 render 阶段生成的虚拟 DOM 更新应用到实际 DOM 上,并执行组件的副作用操作,以确保页面内容与应用状态保持一致。
10.React 中,fiber 是如何实现时间切片的?
- 任务拆分和优先级
- 任务拆分:Fiber 架构允许将长时间运行的任务(如组件更新)拆分成多个小的 Fiber 节点处理单元。这些小任务可以在浏览器的空闲时间中逐步完成,而不是一次性处理所有任务。
- 优先级调度:React 为不同的更新任务分配不同的优先级。例如,用户输入相关的更新(如点击和输入事件)通常会被赋予较高的优先级,而低优先级的更新(如数据预取)则可能在用户操作之后执行。优先级调度可以确保重要的任务优先处理。
- 任务调度
- 调度器(Scheduler):React 使用调度器来控制任务的执行。调度器决定何时执行 Fiber 节点的更新工作,以及在主线程上分配的时间片。调度器会根据任务的优先级和浏览器的空闲时间来安排任务执行。
- 时间片切换:时间切片的核心思想是将长时间运行的任务分割成多个时间片(时间段),并在每个时间片内处理一部分任务。在每个时间片结束时,React 会检查是否有更高优先级的任务需要处理,或者是否需要将当前任务暂停,等待下一次时间片继续处理。
- Fiber 节点的处理
- 工作单元:每个 Fiber 节点代表一个工作单元,处理 Fiber 节点的任务被称为“工作单元”。在时间切片过程中,React 会逐步处理这些工作单元,以便将渲染任务拆分成较小的部分。
- 中断与恢复:如果在处理 Fiber 节点的过程中,浏览器遇到用户交互(如点击或滚动),React 可以中断当前任务,并优先处理这些用户交互相关的高优先级任务。一旦用户交互处理完成,React 会恢复中断的任务,继续处理剩余的 Fiber 节点。
- 流程实现
- 任务开始:当 React 开始执行更新任务时,它会将任务拆分成多个 Fiber 节点的处理单元,并根据优先级安排这些任务。
- 调度执行:调度器会分配时间片来处理这些 Fiber 节点。每个时间片内,React 会处理一定数量的 Fiber 节点,更新虚拟 DOM 和实际 DOM。
- 检查任务状态:在每个时间片结束时,React 会检查是否有高优先级任务需要处理,或者是否需要暂停当前任务。调度器决定是否继续执行当前任务或切换到其他任务。
- 恢复执行:如果任务被中断,React 会在下一次时间片中恢复执行,继续处理未完成的 Fiber 节点。
- 用户交互
- 响应用户操作:时间切片技术确保用户操作(如输入、点击)能够及时响应。React 在处理高优先级任务时,能够快速响应用户交互,提高页面的交互性能。
- 优化渲染:通过将长时间运行的任务分解成多个时间片,React 能够避免阻塞主线程,提高页面的整体性能和响应性。
总结
Fiber 的时间切片实现使得 React 能够高效地处理长时间运行的渲染任务,通过将任务拆分成多个小任务,并在主线程上分配时间片来逐步完成这些任务。这种方法可以显著提高用户界面的响应性和性能,确保高优先级任务能够及时处理,并优化页面渲染体验。
11.说说你对 createPortal 的了解
ReactDOM.createPortal 是 React 的一个 API,用于将子节点渲染到 DOM 的不同部分,而不是当前组件的父级节点中。这对于需要在 DOM 的不同层级中插入内容(如模态框、工具提示、下拉菜单等)非常有用,因为它允许我们将子元素渲染到指定的 DOM 节点中,保持其逻辑上的位置和结构。
ReactDOM.createPortal(child, container)
- DOM 层级结构:
- 通过 createPortal,你可以将子元素插入到 DOM 的任意位置,这对于需要在页面不同层级中展示内容的情况很有帮助。例如,模态框通常需要渲染在页面的顶层。
- 维持 React 组件的生命周期和状态:
- 使用 createPortal 渲染的元素仍然保持在 React 组件树中,因此它们会遵循 React 的生命周期方法,状态和上下文不会受到影响。
- 避免 CSS 影响:
- 在某些情况下,使用 createPortal 可以避免子组件的 CSS 被父组件的样式干扰,因为渲染到的 DOM 节点通常是在 body 或其他顶层容器中。
import React from 'react'
import ReactDOM from 'react-dom'
class Modal extends React.Component {
render() {
return ReactDOM.createPortal(
<div className="modal">
<h1>I'm a modal!</h1>
<button onClick={this.props.onClose}>Close</button>
</div>,
document.body // Modal 将被渲染到 body 元素中
)
}
}
export default Modal
原理
- createPortal 创建的组件会挂载到指定的 DOM 节点 container 中,但它仍然在 React 的组件树中。这意味着 React 可以处理它的生命周期和更新,而不是直接操作 DOM。
12.React 中的 createContext 和 useContext 分别有什么用?
- createContext
- 作用:用于创建一个新的上下文对象。
- 用法:调用 createContext 会返回一个上下文对象,这个对象包含 Provider 和 Consumer 组件。Provider 用于提供上下文的值,而 Consumer 用于消费上下文的值。
// 创建上下文对象
const MyContext = createContext(defaultValue);
// 提供上下文的值
<MyContext.Provider value={someValue}>
{/* 子组件 */}
</MyContext.Provider>
// 消费上下文的值
<MyContext.Consumer>
{value => /* 使用上下文的值 */}
</MyContext.Consumer>
- useContext
- 作用:用于在函数组件中访问上下文的值。
- 用法:useContext 是一个 Hook,用于在函数组件中消费上下文的值,而不需要使用 Consumer 组件。
import { useContext } from 'react'
// 在函数组件中访问上下文
const value = useContext(MyContext)
useContext 适用于:跨组件树共享数据、避免属性钻取、全局状态管理、跨组件通信等场景。
13.使用 react-router 跳转时,如何将参数传递给下一个页面?
- 通过 URL 参数传递
定义路由时:
在定义路由时,可以使用 URL 参数。例如:
<Route path="/user/:id" component={User} />
跳转时:
使用 useHistory 或 useNavigate(在 React Router v6 中)进行跳转时,可以将参数添加到 URL 中:
import { useHistory } from 'react-router-dom' // React Router v5
function RedirectToUser() {
const history = useHistory()
const handleClick = () => {
history.push(`/user/123`) // 跳转到 /user/123
}
return <button onClick={handleClick}>Go to User 123</button>
}
import { useNavigate } from 'react-router-dom' // React Router v6
function RedirectToUser() {
const navigate = useNavigate()
const handleClick = () => {
navigate(`/user/123`) // 跳转到 /user/123
}
return <button onClick={handleClick}>Go to User 123</button>
}
接收参数:
在目标组件中,可以使用 useParams 钩子(在 React Router v6 中也是 useParams)来 接收 URL 参数:
import { useParams } from 'react-router-dom'
function User() {
const { id } = useParams()
return <div>User ID: {id}</div>
}
- 通过查询字符串传递
跳转时:
可以将参数作为查询字符串添加到 URL 中:
import { useHistory } from 'react-router-dom' // React Router v5
function RedirectToUser() {
const history = useHistory()
const handleClick = () => {
history.push(`/user?id=123`) // 跳转到 /user?id=123
}
return <button onClick={handleClick}>Go to User</button>
}
import { useNavigate } from 'react-router-dom' // React Router v6
function RedirectToUser() {
const navigate = useNavigate()
const handleClick = () => {
navigate(`/user?id=123`) // 跳转到 /user?id=123
}
return <button onClick={handleClick}>Go to User</button>
}
接收查询参数:
可以使用 URLSearchParams 来获取查询参数:
import { useLocation } from 'react-router-dom'
function User() {
const { search } = useLocation()
const params = new URLSearchParams(search)
const id = params.get('id')
return <div>User ID: {id}</div>
}
- 通过状态传递
跳转时:
可以将参数作为状态传递:
import { useHistory } from 'react-router-dom' // React Router v5
function RedirectToUser() {
const history = useHistory()
const handleClick = () => {
history.push({
pathname: '/user',
state: { id: 123 }
})
}
return <button onClick={handleClick}>Go to User</button>
}
import { useNavigate } from 'react-router-dom' // React Router v6
function RedirectToUser() {
const navigate = useNavigate()
const handleClick = () => {
navigate('/user', { state: { id: 123 } })
}
return <button onClick={handleClick}>Go to User</button>
}
接收状态:
在目标组件中,可以通过 useLocation 钩子获取传递 的状态:
import { useLocation } from 'react-router-dom'
function User() {
const location = useLocation()
const id = location.state?.id
return <div>User ID: {id}</div>
}
14.react 中,数组用 useState 做状态管理的时候,使用 push,pop,splice 等直接更改数组对象,会引起页面渲染吗?
为什么直接修改数组不触发渲染
React 使用 Object.is(或其变体)来检查状态是否发生了变化。直接对数组进行 push、pop、splice 等操作,修改了原始数组的内容,但数组的引用(内存地址)没有改变。React 仅通过引用变化来判断状态是否更新,因此直接修改原始数组不会触发更新
正确的做法
为了触发渲染,应该遵循不可变数据模式,即通过创建新数组来更新状态。以下是使用 useState 管理数组状态的推荐方法:
import React, { useState } from 'react'
function MyComponent() {
const [items, setItems] = useState([1, 2, 3])
// 添加新项
const addItem = item => {
setItems(prevItems => [...prevItems, item])
}
// 移除最后一项
const removeLastItem = () => {
setItems(prevItems => prevItems.slice(0, -1))
}
// 更新特定项
const updateItem = (index, newItem) => {
setItems(prevItems => prevItems.map((item, i) => (i === index ? newItem : item)))
}
return (
<div>
<button onClick={() => addItem(4)}>Add Item</button>
<button onClick={removeLastItem}>Remove Last Item</button>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
)
}
export default MyComponent
解释
- 添加新项:
- 使用展 开运算符 [...prevItems, item] 创建一个新数组,并将新项添加到末尾。
- 移除最后一项:
- 使用 slice(0, -1) 创建一个新数组,去除最后一项。
- 更新特定项:
- 使用 map 方法创建一个新数组,并根据条件更新特定项。
15.React 中的 hooks 和 memorizedState 是什么关系?
在 React 中,Hooks 和 MemorizedState 都与组件的状态管理和性能优化有关,但它们的作用和实现方式不同
Hooks
Hooks 是 React 16.8 引入的一组 API,使函数组件能够拥有状态和副作用管理的能力,之前这些特性只有在类组件中才可以使用。常用的 Hooks 包括:
- useState:用于在函数组件中添加状态。
- useEffect:用于处理副作用,如数据获取和订阅。
- useContext:用于在组件树中共享状态。
- useReducer:用于处理更复杂的状态逻辑。
- useMemo 和 useCallback:用于性能优化,避免不必要的重新渲染。
MemorizedState
MemorizedState 是 React 内部的一种实现机制,用于优化组件的性能。它是 React 在内部管理组件状态时使用的一种状态存储方式,尤其是与 Hooks 的实现密切相关。
- 在 React 中,useState 和 useReducer 的实现依赖于 MemorizedState:当你调用 useState 或 useReducer 时,React 会为每个组件实例创建一个 MemorizedState 对象来存储状态。这些状态在组件重新渲染时会被保留,确保组件的状态在生命周期中保持一致。
- MemorizedState 的作用是优化性能:它帮助 React 追踪状态的变化,并在组件重新渲染时有效地管理和更新状态。
16.React 中,怎么给 children 添加额外的属性?
方法一:使用 React.cloneElement
React.cloneElement 是 React 提供的一个 API,用于克隆一个元素并添加额外的属性。这种方法适用于在渲染过程中动态地为 children 添加属性。
import React from 'react'
function ParentComponent({ children }) {
// 为每个子元素添加额外的属性
const childrenWithProps = React.Children.map(children, child => React.cloneElement(child, { extraProp: 'value' }))
return <div>{childrenWithProps}</div>
}
function ChildComponent(props) {
return <div>{props.extraProp}</div>
}
// 使用示例
export default function App() {
return (
<ParentComponent>
<ChildComponent />
<ChildComponent />
</ParentComponent>
)
}
说明:
- React.Children.map 遍历 children 并应用 React.cloneElement。
- React.cloneElement 克隆每个子元素,并添加 extraProp 属性。
方法二:使用 Context API
另一种方法是使用 React 的 Context API 传递额外的属性。这种方法适用于全局传递数据或配置,尤其是当有多个嵌套组件时。
import React, { createContext, useContext } from 'react'
// 创建一个 Context
const ExtraPropsContext = createContext({ extraProp: 'default' })
function ParentComponent({ children }) {
const extraProps = { extraProp: 'value' }
return <ExtraPropsContext.Provider value={extraProps}>{children}</ExtraPropsContext.Provider>
}
function ChildComponent() {
const { extraProp } = useContext(ExtraPropsContext)
return <div>{extraProp}</div>
}
// 使用示例
export default function App() {
return (
<ParentComponent>
<ChildComponent />
<ChildComponent />
</ParentComponent>
)
}
说明:
- 使用 createContext 和 Provider 传递额外的属性。
- 使用 useContext 在子组件中获取这些属性。