一些react知识点

JSX

  • JSX的本质是什么,它和JS之间到底是什么关系?

    JSX是JavaScript的一种语法扩展,它和模板语言很接近,但是它充分具备JavaScript的能力

  • 为什么要用JSX?不用会有什么后果?

    1.提升开发效率;
    2.组件化开发;
    3.更好的抽象层次;

  • JSX背后的功能模块是什么,这个功能模块都做了哪些事情?

    JSX会被编译为React.createElement()React.createElement()将返回一个叫作“React Element”的JS对象【虚拟DOM】,由babel来完成。

    组件初始化->render方法->生成虚拟DOM->ReactDOM.render方法->真实DOM。

    组件更新->render方法->生成新的虚拟DOM->diff->定位出两次虚拟DOM的差异。

React生命周期

React 15的声明周期:

  • 初始化渲染

    constructor()
    componentWillMount()
    render()
    componentDidMount() 渲染结束后被触发,DOM已经挂载,可以处理DOM操作
    componentWillUnmount() 组件卸载

  • 组件更新

    componentWillReceiveProps() [父组件触发] 由父组件的更新触发
    如果父组件导致组件重新渲染,即使props没有更改也会调用此方法(componentReceiveProps)如果只想处理更改,请确保进行当前值与变更值的比较。
    shouldComponentUpdate() [子组件触发] 根据返回值决定是否要进行该方法后面的生命周期
    componentWillMount()
    componentWillUpdate()
    render()
    componentDidUpdate()
    可以处理DOM操作
    componentWillUnmount() 组件卸载

React 16的生命周期:

废弃了componentWillMount(),新增static getDerivedStateFromProps()在初始挂载及后续更新时都会被调用。

static getDerivedStateFromProps():使用props来派生/更新state。getDerivedStateFromProps 的存在只有一个目的:让组件在props变化时更新state。
该方法返回一个对象用于更新 state,如果返回 null 则不更新任何内容。

废弃了componentWillUpdate(),新增getSnapshotBeforeUpdate()在render之后执行。

废弃了componentWillMount()

在fiber架构下,componentWillXXX()可能会多次执行。

Hook的设计

  • 为什么需要HOOK?

    • 告别难以理解的class;

      • this指向问题。
      • 生命周期复杂,逻辑杂糅。
    • 解决业务逻辑难以拆分的问题;

      • 相同的逻辑分散在不同的生命周期中。
    • 使得状态逻辑复用变得简单可行;

      • 自定义Hook
    • 函数组件更加符和React框架的设计理念。ui = f(data);

  • 工作机制

    • 只在 React 函数中调用 Hook
    • 不要在循环、条件或嵌套函数中调用 Hook
    • 要确保 Hooks 在每次渲染时都保持同样的执行顺序

Hooks的正常运作,在底层依赖于顺序链表。

虚拟DOM

虚拟DOM的价值不在性能。

  • 研发体验/研发效率的问题,虚拟DOM是数据驱动视图的载体。
  • 跨平台的问题,虚拟DOM是DOM结构的抽象,用以表述真实DOM。
  • 批量更新,实现集中化的DOM批量更新。

setState到底是同步的还是异步的?

这个问题的讨论要结合具体的版本:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/17。

下面的内容应该在react18之前。

结论:只要是在React管控下的setState,一定是异步的。

在React钩子函数及合成事件中,它表现为异步。
setState异步的动机:避免频繁的re-render。每来一个setState,就将其放进队列里面,时间成熟后对state结果做合并,走一次更新流程。
在setTimeout、setlnterval等函数中包括在 DOM 原生事件中,它都表现为同步。

setState工作流:

1
2
3
4
5
6
7
8
9
isBatchingUpdates = true

setTimeout(() => {
this.setState({
count: this.state.count + 1
})
}, 0)

isBatchingUpdates = false

react 会在执行前加一个“锁”来标记是否需要批处理, 如上,react加了锁之后立刻就释放了,然后才会执行setTimeout里的setState, 也就是说setTimeout和原生事件会脱离react的控制。 只有在react控制下才会存在批处理,setState才会有“异步”效果。

https://github.com/reactwg/react-18/discussions/21

为什么需要fiber架构?

在Stack Reconciler的架构下,组件通过树的结构进行维护,当某个组件的状态发生变化的时候,React会递归其所有的子组件,然后寻找该变化产生的影响。递归遍历一旦启动就不能停止,当组件的结构过于复杂的时候,递归的时间会很长,进而导致主线程的阻塞。现代浏览器的刷新率一般为60HZ,这意味着,长时间的递归将导致掉帧的发生,影响体验。

为什么没有Vue fiber?

Vue没有fiber的原因在于,在Vue中数据变化会被拦截到,然后通知相应的watcher进行对应视图的重新渲染。数据发生变化后,Vue能够快速的捕获这种变化,并通知对应的视图进行响应,而在React中,组件的数据发生变化后,React并不能确定这种变化将会对哪些子组件产生影响,因此只能通过深度遍历进行比对来确定。

事件系统

React的事件体系大都通过事件委托来实现,少部分直接绑定在对应的DOM元素上。

原生事件中,一个事件的传播过程要经过2个阶段:

  • 事件捕获阶段
  • 目标阶段
  • 事件冒泡阶段

事件委托:假如<ul>标签下有若干个<li>标签,要为每个<li>标签都添加一个点击事件,点击事件最终都会冒泡到他们的共同的父节点<ul>上面,因此,可以将要绑定的事件直接绑定在<ul>标签上。

React合成事件:

  • 在底层抹平了不同浏览器的差异
  • 在上层面向开发者暴露统一的、稳定的、与 DOM 原生事件相同的事件接口
  • 帮助React实现了对所有事件的中心化管控

运行机制

  • render阶段:
    递归的创建fiber树
  • commit阶段:
    • before mutation阶段
      • 处理DOM节点渲染/删除后的autoFocus、blur逻辑
      • 调用getSnapshotBeforeUpdate生命周期钩子
      • 调度useEffect,不是调用。异步执行的原因是防止同步执行的时候阻塞浏览器渲染
    • mutation阶段
      • 执行DOM操作
      • 执行useLayoutEffect的销毁函数
    • layout阶段
      • 类组件:componentDidMount/componentDidUpdate生命周期
      • 类组件:调用this.setState的回调
      • 函数组件:调用useLayoutEffect的回调
      • 函数组件:调度useEffect的销毁和回调函数,layout阶段完成后再执行

fiber架构

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
  • Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

Reconciler:

1
2
3
4
5
6
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
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
31
32
33
34
35
36
37
38
39
40
41
42
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
// The main thread has only been blocked for a really short amount of time;
// smaller than a single frame. Don't yield yet.
return false;
}
// The main thread has been blocked for a non-negligible amount of time. We
// may want to yield control of the main thread, so the browser can perform
// high priority tasks. The main ones are painting and user input. If there's
// a pending paint or a pending input, then we should yield. But if there's
// neither, then we can yield less often while remaining responsive. We'll
// eventually yield regardless, since there could be a pending paint that
// wasn't accompanied by a call to `requestPaint`, or other main thread tasks
// like network events.
if (enableIsInputPending) {
if (needsPaint) {
// There's a pending paint (signaled by `requestPaint`). Yield now.
return true;
}
if (timeElapsed < continuousInputInterval) {
// We haven't blocked the thread for that long. Only yield if there's a
// pending discrete input (e.g. click). It's OK if there's pending
// continuous input (e.g. mouseover).
if (isInputPending !== null) {
return isInputPending();
}
} else if (timeElapsed < maxInterval) {
// Yield if there's either a pending discrete or continuous input.
if (isInputPending !== null) {
return isInputPending(continuousOptions);
}
} else {
// We've blocked the thread for a long time. Even if there's no pending
// input, there may be some other scheduled work that we don't know about,
// like a network event. Yield now.
return true;
}
}
// `isInputPending` isn't available. Yield now.
return true;
}

fiber节点之间的关联

1
2
3
4
5
6
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

fiber架构的工作原理

React双缓存fiber树:current Fiber树、workInProgress Fiber树。

  • fiberRootNode:应用的根节点。
  • rootfiber:调用ReactDOM.redner渲染的组件树的根节点。