0%

React-interview

React Fiber架构

React的渲染可以分为两个主要阶段:Reconciler(协调阶段),和Committer(提交阶段)。

Reconciler(协调阶段)

工作内容: 构建Fiber树,比较新旧虚拟DOM的不同之处,生成一个变更记录,即一系列需要对真实DOM进行的操作。此阶段的特点:异步,并发,可中断。如果执行过程中有更高优先级的任务来了,那么会中断当前Reconciler的工作,转而处理更重要的任务。

Committer阶段

工作内容:将Reconciler阶段生成的变更记录应用到真实的DOM上。此阶段是同步的,不可中断的。
工作阶段:

  1. Dom更新前 - useEffect在这个阶段执行,通过微任务队列异步执行(页面渲染后执行)
  2. Dom更新 - 执行真实DOM的更新
  3. Dom更新后 - useLayoutEffect在这个阶段执行,同步(页面渲染前执行)

注意页面的渲染和JS的执行是互斥的,只有JS代码执行完,页面才能渲染,这就是useLayoutEffect的作用,可以在页面渲染前执行一些操作,比如调整布局。

数据结构

Fiber架构中采用FiberNode和FiberTree来描述虚拟DOM树。FiberNode是一个双向链表,每个节点都有一个指向父节点的指针,一个指向子节点的指针,一个指向兄弟节点的指针。FiberTree是一个树形结构,由FiberNode组成。

React diff算法

树比较

只做同层级结点比较,如果结点不存在了,则直接删除。不会继续比较其子树。这避免了夸层级移动操作,对于跨层级移动操作,相当于删除再重建。同层级结点移动呢?可以处理。

组件比较

只做同类型的组件比较,比如div和div比较,p和p比较,只有组件的类型相同,才进入子树进行深层次比较。如果类型不一致,则删除重新创建。

元素比较

对于同层级的元素结点。

  1. 元素在新集合中,但是不在原来的集合中,属于全新的结点,对集合进行插入操作。
  2. 元素在原来的集合中,但是不在新的集合中,则删除该元素。
  3. 元素在新集合中,也在原来的集合中,且元素并未更新,只是位置发生了变化,则进行移动操作。

双缓冲策略:

  1. current树负责呈现当前页面,而所有的更新都由workInProgress树来承接,当变更完成需要渲染时,将workInProgress树变成current树。

setState是同步还是异步?

这个问题需要区分开来看,在React18之前,如果executionContext被赋值了,代表该任务已经进入React调度流程中,此时React会对该任务进行异步处理(批量处理),如果executionContext没有被赋值,代表该任务还没有进入React调度流程,此时React会对该任务进行同步处理。想setTimeout, setInterval等函数都是不会进入React调度流程的,所以是同步处理。而合成事件都会进入到React调度流程中,所以会被异步处理。在React18后,如果使用createRoot创建根节点,那么setState会变成同步的。但是如果还是使用传统的render方式,那么和React18之前的处理逻辑一样。

虚拟DOM

虚拟DOM是React的核心概念之一,它是一个轻量级的JavaScript对象,用来描述真实DOM的层次结构。React通过比较新旧虚拟DOM的差异,然后只更新需要更新的部分,从而提高页面的渲染性能。

虚拟DOM的量大作用:

  1. 跨平台,因为虚拟DOM只是一个数据结构,所以可以在不同平台上使用,比如浏览器,移动端App等。这是虚拟DOM的重要特性,很多人没有意识到。
  2. 高性能,通过比较新旧虚拟DOM的差异,只更新需要更新的部分,减少了对真实DOM的操作,提高了页面的渲染性能。

React中嵌套组件的渲染顺序,生命周期,销毁顺序。

渲染顺序

当React开始渲染过程时,它首先从上到下、从父到子地渲染组件树。这意味着父组件会先于其子组件进行渲染。一旦父组件完成它的渲染(包括执行相关的生命周期方法),React会递归地进入该父组件的子组件,并按照同样的方式渲染它们。

生命周期顺序

  • 首次挂载时:
    • 父组件:constructor -> static getDerivedStateFromProps -> render -> 子组件重复相同流程 -> 子组件componentDidMount -> 父组件componentDidMount
  • 更新时(由于props变化或state变化):
    • 父组件:static getDerivedStateFromProps -> shouldComponentUpdate -> render -> 子组件重复相同流程 -> 子组件componentDidUpdate -> 父组件componentDidUpdate

销毁顺序

当组件被卸载时,React会以相反的顺序卸载组件树中的组件。也就是说,React首先卸载最深层的子组件,然后逐步向上,直到根组件。

受控组件和非受控组件

受控组件是指组件的值是由React的state来控制的,比如下面的input,其中关键一句就是value={this.state.value} 这样就把input的value和组件的state关联起来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};

this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}

handleChange(event) {
this.setState({value: event.target.value});
}

handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}

自定义hook

模拟componentDidMount

当我们为useEffect的依赖传递空数组时,它只会在组件初始化时执行一次,这就相当于componentDidMount。注意,这个只是粗略的模拟,因为useEffect是异步执行,且时机是在浏览器渲染完成后,而componentDidMount是在浏览器渲染前同步执行。

1
2
3
4
5
6
7
import {useEffect} from "react";

export const useMount = ((callBack) => {
useEffect(() => {
callBack();
}, []);
})

模拟componentDidUpdate

componentDidUpdate是在每次组件的state或props更新是调用的,但是组件mount的时候不调用,所以我们需要一个变量来标志组件是否是第一次加载,这里我们使用useRef来标志组件是否是第一次加载。因为useRef包裹的值在整个组件声明周期内引用不变(有点像静态变量),并且这个值改变后不会触发组件的重新渲染。

1
2
3
4
5
6
7
8
9
10
11
export const useUpdate = (callback, deps) => {
const isMounted = useRef(false); // 🚩 关键点

useEffect(() => {
if (isMounted.current) {
callback();
} else {
isMounted.current = true; // 挂载后切换标识
}
}, deps);
};

需要注意的是,如果在React18+严格模式下运行该函数,还是会执行一次,因为React18+的严格模式会执行两次。

1
2
3
4
5
6
7
createRoot(document.getElementById('root')).render(
<StrictMode> // 严格模式,去掉即可得到正确结果
<BrowserRouter>
<App/>
</BrowserRouter>
</StrictMode>,
)

模拟componentWillUnmount

这个就比较简单了,componentWillUnmount是在组件卸载时调用的,我们可以使用useEffect的返回值来模拟这个生命周期。

1
2
3
4
5
6
7
export const useUnmount = ((callback) => {
useEffect(() => {
return () => {
callback();
}
}, []);
})