2024年前端最全深入 React Fiber 架构和协调算法(1),高级前端面试答案

css

1,盒模型
2,如何实现一个最大的正方形
3,一行水平居中,多行居左
4,水平垂直居中
5,两栏布局,左边固定,右边自适应,左右不重叠
6,如何实现左右等高布局
7,画三角形
8,link @import导入css
9,BFC理解

js

1,判断 js 类型的方式
2,ES5 和 ES6 分别几种方式声明变量
3,闭包的概念?优缺点?
4,浅拷贝和深拷贝
5,数组去重的方法
6,DOM 事件有哪些阶段?谈谈对事件代理的理解
7,js 执行机制、事件循环
8,介绍下 promise.all
9,async 和 await,
10,ES6 的 class 和构造函数的区别
11,transform、translate、transition 分别是什么属性?CSS 中常用的实现动画方式,
12,介绍一下rAF(requestAnimationFrame)
13,javascript 的垃圾回收机制讲一下,
14,对前端性能优化有什么了解?一般都通过那几个方面去优化的?

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

}

可以在这里[8]把玩这段代码,如你所见,这是一个简单组件,从 render() 方法中返回两个子元素 button 和 span 。

当你单击按钮时,组件的状态将被内部的 handler 更新,顺带的,这会导致 span 元素的文本更新。

React 在 协调(reconciliation) 期间执行各种活动。

例如,下面是 React 在简单组件的首次渲染中,以及 state 更新之后,执行的高级操作:

•更新 ClickCounter 组件中 state 的 count 属性。•检索并比较 ClickCounter 的子组件和 props 。•更新 span 的 props 。

协调(reconciliation) 期间也进行了其他活动,包括调用生命周期方法[9]或更新 refs[10]。这些活动在 Fiber 架构中被统称为 work。 work 的 type 通常取决于 React 元素的类型。

例如,对一个类组件而言,React 需要创建一个实例,而函数组件则无需执行此操作。

React 的有许多类型的 elements,例如类组件和函数组件, host 组件(DOM节点)等。

React 元素的类型由传入到 createElement 的第一个参数决定,通常在 render 方法中使用此函数来创建元素。

在我们开始探索活动细节和主要的 fiber 算法之前,让我们先熟悉 React 内部使用的数据结构。

从 React 元素到 Fiber 节点


React 中的每个组件都有一个 UI 表示,我们可以称之为从 render 方法返回的一个视图或模板。

这是 ClickCounter 组件的模板:

Update counter

{this.state.count}

React 元素

模板通过JSX编译器后,将得到一堆React元素。下面是真正从 React 组件的 render 方法返回的结果(并不是 HTML)。

由于我们不需要使用JSX,因此可以将 ClickCounter 组件的 render 方法重写:

class ClickCounter {

render() {

return [

React.createElement(

‘button’, {

key: ‘1’,

onClick: this.onClick

},

‘Update counter’

),

React.createElement(

‘span’, {

key: ‘2’

},

this.state.count

)

]

}

}

render 方法调用的 React.createElement 会产生两个数据结构。

像这样:

[{

$$typeof: Symbol(react.element),

type: ‘button’,

key: “1”,

props: {

children: ‘Update counter’,

onClick: () => {

}

}

},

{

$$typeof: Symbol(react.element),

type: ‘span’,

key: “2”,

props: {

children: 0

}

}]

可以看到 React 将属性 $$typeof 添加到这些对象中,作为 React 元素的唯一标识。

而且有 type 、 key 、和 props 这些属性对 React 元素进行描述。

它们的值来源于传递给 react.createElement 函数的参数。

请关注 React 是如何将文本内容表示为 span 和 button 的子节点的。

以及如何把 click 的 handler 描述为 button 里 props 的一部分。

React 元素还有其他的很多字段,比如 ref ,但是超出本文范围不作展开。

ClickCounter 对应的 React 元素不存在任何 props 或 key :

{

$$typeof: Symbol(react.element),

key: null,

props: {},

ref: null,

type: ClickCounter

}

Fiber nodes

协调(reconciliation) 过程中,从 render 方法返回的每个 React element 的数据将被合并到 Fiber 节点树中,每个 React element 都有一个对应的 Fiber 节点。

与 React 元素不同, Fiber 并不是每次渲染都会重新创建,它们是用来保存组件 state 和 DOM 的可变数据结构。

之前聊到过,框架执行的活动,取决于 React 元素的类型。

在我们的示例中,对于类组件 ClickCounter 而言,它调用生命周期方法和 render 方法。

而对于 span host 组件(dom节点),执行 DOM 更新。

因此,每个 React 元素都被转换成 相应类型[11]的 Fiber 节点,描述需要完成的工作。

[译者注:这里的类型是 WorkTag 每个类型是一个固定的数字,例如函数式组件对应的是 0 而类组件对应的是 1]

你可以将 Fiber 想象成一种数据结构,用来表示一些要做的工作,或者换句话说,一个工作单元。 Fiber 的架构还提供了一种方便的方式来跟踪、调度、暂停和中止工作。

当 React 元素第一次转换为 fiber 节点时,React在 createFiberFromTypeAndProps 函数中使用元素的数据来创建一个 Fiber 。

在后续更新中,React 复用了fiber节点,这意味着它只会根据数据发生改变的部分来更新对应的 fiber 节点中的属性。

如果从 render 方法不再返回相应的 React 元素,React 可能还需要根据 key 属性来移动或删除层级结构中的 fiber 节点。

深入 ChildReconciler[12] 函数,了解所有活动的列表以及 React 为现有 fiber 节点执行的相应函数。

因为 React 为每个 React 元素创建一个 fiber 节点,并且我们已经有一个这些元素组成的树,所以我们将会得到一个fiber 节点树。这样的情况下,我们简单的示例看起来就像这样:

所有 fiber 节点使用这些属性: child 、 sibling 和 return 通过链接列表的形式连接在一起。

如果你想知道更多关于为什么要这样的更多信息,可以阅读这篇文章 The how and why on React’s usage of linked list in Fiber[13] (如果你还没读过的话)

Current 树和 workInProgress 树

在第一次渲染之后,React 最终得到了一棵反映渲染出 UI 的应用程序 state 的 fiber 树。

这棵树通常被称为 current 树。当 React 开始处理更新时,它会构建一棵所谓的 workInProgress 树,反映将来要刷新到屏幕的 state。

所有的 work 都是在 workInProgress 树的 fibler 上进行的。当 React 遍历 current 树时,它为每个现有的光纤节点创建一个替代节点。

这些节点构成了 workInProgress 树。它们用 render 方法返回的 React 元素的数据创建。

一旦处理完所有 update 并完成所有相关 work,React 将一棵准备好的备用树刷新到屏幕。

一旦在屏幕上渲染 workInProgress 树,它就成为了 current 树。

React 的核心原则之一 consistency (一致性)。

React总是一次性更新 DOM (它不会显示部分结果)。 workInProgress 树作为用户看不到的 “草稿”,以便 React 可以在处理所有组件之后,再将它们的更新刷新到屏幕上。

在源代码中,你会看到许多函数从 current 树和 workInProgress 树中获取 fiber 节点。下面是一个这样的函数的示例:

function updateHostComponent(current, workInProgress, renderExpirationTime) {

}

每个 fiber 节点都在 alternate 字段中保留了在另一棵树上其对应节点的引用。 current 树中的一个节点指向 workInProgress 树中的节点,反之亦然。

Side-effects 副作用

我们可以将 React 中的组件视为使用 state 和 props 来计算 UI如何呈现的函数。

除此之外的所有活动,例如,改变DOM 或调用生命周期方法,都应被视为 Side-effects ,或者简单地视为一种 effect。

在 文档[14]里也有提及。

你之前可能已经在 React 组件中执行过获取数据、订阅或者 手动修改 DOM。我们统一把这些操作称为 “Side-effects”,或者简称为 “effect”。(因为它们会影响其他组件,并且在渲染期间无法完成。) ”

你可以看到大多数 state 和 props 的更新将如何导致 side-effects 。

而且,由于执行 effect 是一种 work,fiber 节点是一种跟踪更新效果的便捷机制。

每个fiber 节点都可以包含与其相关的 effect,在 effectTag 字段中。

因此,Fiber中的 effect 基本上定义了实例在处理更新后需要完成的 work[15]:

•对于 host 组件(dom元素),包括添加、更新或删除元素。•对于类组件,React 可能需要更新 refs 并调用 componentDidMount 和 componentDiddUpdate 生命周期方法。•还有其他 effect 对应于其他类型的 fiber。

Effects 链表

React 执行 update 非常快,它采用了一些有趣的技术来达到这种性能水平:

建立具有 effect 的 fiber 节点的线性链表以实现快速迭代是其中之一

迭代线性链表比树快得多,不需要花时间在没有 side-effects 的节点上。

该链表的目的是,标记具有 DOM 更新或与其他 effect 关联的的节点。

它是 finishedWork 树的子集,并且使用 nextEffect 属性而不是 current 树和 workInProgress 树中的 child 属性进行链接。

Dan Abramov[16] 提出了一个 effect 链表的类比,把它想象成一棵圣诞树,"圣诞灯"把所有有效的节点绑在一起。

为了将其可视化,让我们想象下面的 fiber 节点树:

其中,高亮的节点有一些 work 要做,例如,我们的更新导致 c2 插入到 DOM 中。

d2 和 c1 更改属性, B2 触发生命周期方法。

effect 链表将它们链接在一起,以便 React 可以稍后跳过其他节点:

可以看到,具有 effect 的节点是如何链接在一起的。

当遍历节点时,React 使用 firstEffect 指针来确定列表的起始位置。所以上面的图表可以表示为这样的线性链表:

Fiber 树的根节点

每个 React 应用程序都有一个或多个充当容器 DOM 元素。在我们的例子中它是 ID 为 container 的 div 。

const domContainer = document.querySelector(‘#container’);

ReactDOM.render(React.createElement(ClickCounter), domContainer);

React为每个容器创建一个 fiber root [17] 对象。你可以使用 DOM 元素的引用来访问它:

const fiberRoot = query(‘#container’)._reactRootContainer._internalRoot

这个 fiber root 是 React 保存对 fiber 树的引用的地方。存储在 fiber root 的 currrent 属性中:

const hostRootFiberNode = fiberRoot.current

Fiber 树以 特殊类型[18] 的 fiber 节点 HostRoot 开始。

它是在内部创建的,并充当最顶层组件的父级。

HostRoot fiber 节点通过 stateNode 属性链接到 FiberRoot :

fiberRoot.current.stateNode === fiberRoot; // true

你可以通过 fiber root 访问最顶层的 HostRoot fiber 节点来探索 fiber tree。

或者,你可以从组件实例中获取单个 fiber 节点,像这样:

compInstance._reactInternalFiber

Fiber 节点结构

来看看为 ClickCounter 组件创建的 fiber 节点的结构:

{

stateNode: new ClickCounter,

type: ClickCounter,

alternate: null,

key: null,

updateQueue: null,

memoizedState: {

count: 0

},

pendingProps: {},

memoizedProps: {},

tag: 1,

effectTag: 0,

nextEffect: null

}

以及 span DOM 元素:

{

stateNode: new HTMLSpanElement,

type: “span”,

alternate: null,

key: “2”,

updateQueue: null,

memoizedState: null,

pendingProps: {

children: 0

},

memoizedProps: {

children: 0

},

tag: 5,

effectTag: 0,

nextEffect: null

}

fiber 节点上有很多字段。在前面已经描述过字段 alternate 、 effectTag 和 nextEfect 的用途。现在看看其他的字段的用途。

stateNode

保存对类组件实例,DOM 节点或与 fiber 节点关联的其他 React 元素类型的引用。一般来说,此属性用于保存与 fiber 关联的 local state。

type

定义与此 fiber 关联的函数或类。对于类组件,它指向构造函数,对于 DOM 元素,它指定 HTML 标记。

我把这个字段理解为 fiber 节点与哪些元素相关。

tag

定义 fiber类型[19],在 reconciliation 算法中使用它来确定需要完成的工作。

就像前面提到的,work 取决于 React 元素的类型。 createFiberFromTypeAndProps函数 [20]将 React 元素映射到相应的 fiber 节点类型。

在我们的应用中, ClickCounter 组件的 tag 属性是 1 ,它表示 类组件 ;

而 span 元素的 tag 属性是 5 表示 HostComponent(宿主组件) 。

updateQueue

state 更新,回调以及 DOM 更新的队列。

memoizedState

用于创建输出的 fiber 的状态。处理更新时,它反映了当前渲染在屏幕上的内容的 state。

memoizedProps

在上一次渲染期间用于创建输出的 fiber 的 props 。

pendingProps

在 React element 的新数据中更新并且需要应用于子组件或 DOM 元素的 props。(子组件或者 DOM 中将要改变的 props)

key

唯一标识符,当具有一组 children 的时候,用来帮助 React 找出哪些项已更改,已添加或已从列表中删除。与这里[21]所说的React的 “列表和key” 功能有关

你可以在这里[22]找到 fiber节点的完整结构。在上面的说明中,省略了很多字段。

尤其是跳过了构成树结构的 child 指针, sibling 指针和 return 指针。

[这些结构我在 上一篇文章 [23]中有所描述。]

以及专门针对 Scheduler 的 expirationTime , childExpirationTime 和 mode 等字段类别。

通用算法


React 执行工作分为两个主要阶段:render 和 commit

在 render 阶段,React 通过 setState 或 React.render 有计划的将更新应用于组件,并找出需要在 UI 中更新的内容。

如果是初始渲染,React 会为 render 方法返回的每个元素创建一个新的 fiber 节点。在后续更新中,现有 React 元素的 fiber 节点将被复用和更新。

**该阶段的执行的结果是带有 effect 的 fiber 节点树。**effect 描述了在下一个 commit 阶段需要完成的工作。

当前阶段,React 会绘制一棵标记有 effect 的 fiber 树,并将其应用于实例。它遍历 effect 链表并执行 DOM 更新和用户可见的其他更改。

理解 render 阶段的工作可以异步执行,对我们而言非常重要。React 可以根据可用时间来处理一个或多个 fiber 节点,然后中断并且暂存已完成的工作,转去处理某些事件,接着它再从它停止的地方继续执行。

但有时候,它可能需要丢弃完成的工作然后重新从头开始。

由于在此阶段执行的工作不会导致任何用户可见的更改(例如DOM更新),所以才可以实现这些暂停。

(译者注:因为 React 的一致性,所以不可能给用户呈现渲染到一半的组件,这样意味着这个阶段执行的所有 work 都是用户不可见的。)

与之相反的是,后续的 commit 阶段始终是同步的。这是因为在此阶段执行的工作会导致用户可见的更改,例如 DOM 更新。这就是为什么 React 需要一次性完成这些操作。

调用生命周期方法是 React 的工作之一。一些方法是在 render 阶段调用的,而另一些方法则是在 commit 阶段调用。

这是在第一个 render 阶段工作时,调用的生命周期列表:

•[UNSAFE_]componentWillMount (deprecated)•[UNSAFE_]componentWillReceiveProps (deprecated)•getDerivedStateFromProps•shouldComponentUpdate•[UNSAFE_]componentWillUpdate (deprecated)•render

如你所见,在16.3版本中,在 render 阶段执行的一些遗留的生命周期方法被标记为 UNSAFE 。

(译者注:这里的 “unsafe” 不是指安全性,而是表示使用这些生命周期的代码在 React 的未来版本中更有可能出现 bug,尤其是在启用异步渲染之后。参考官方文档[24])

现在在文档中它们被称为遗留 (legacy lifecycles) 生命周期。将在以后的 16.x 发行版中弃用,而没有 UNSAFE 前缀的对应版本将在 17.0版本中移除。

你可以在这里[25]详细的了解这些更改,以及建议的迁移路径。

你是否对此感到好奇?

好吧,我们刚刚了解到,由于 render 阶段不会产生诸如 DOM 更新之类的 effect,因此 React 可以异步处理组件的异步更新(甚至可能在多个线程中进行)。

但是,标有 UNSAFE 的生命周期经常被误解和巧妙地滥用。开发人员倾向于将带有 effect 的代码放在这些方法中,这可能会导致新的异步渲染方法出现问题。

Although only their counterparts without the UNSAFE prefix will be removed, they are still likely to cause issues in the upcoming Concurrent Mode (which you can opt out of).

(译者注:这一段并不是忘记翻译,我对作者描述的内容还有疑问,已经在沟通中,后续会同步到 frontendwingman 的章节中)

接下来罗列的生命周期方法是在第二个 commit 阶段执行的:

•getSnapshotBeforeUpdate•componentDidMount•componentDidUpdate•componentWillUnmount

因为这些方法都在同步的 commit 阶段执行,他们可能会包含 side-effects ,并且操作DOM。

好的,现在我们拥有了背景之后,让我们继续深入研究,用于遍历树和执行 work 的通用算法 。

Render 阶段

协调算法始终使 renderRoot[26] 函数从最顶层的 HostRoot fiber 节点开始。但是,React 会退出(跳过)已经处理的 fiber 节点,直到找到工作未完成的节点。

例如,如果你在组件树的深处调用 setState ,React将从顶部开始,但会快速跳过父级,直到它到达调用了 setState 方法的组件。

WorkLoop 的主要步骤

所有的 fiber 节点都会在 work loop[27]. 中进行处理。如下是该循环的同步部分的实现:

function workLoop(isYieldy) {

if (!isYieldy) {

while (nextUnitOfWork !== null) {

nextUnitOfWork = performUnitOfWork(nextUnitOfWork);

}

} else {

}

}

在上面的代码中, nextUnitOfWork 持有对 workInProgress 树中 fiber 节点的引用。

该节点需要完成一些工作。当 React 遍历 Fibers 树时,它通过此变量来判断是否还有其他未完成的 fiber 节点。

处理完当前光纤后,该变量将包含对树中下一个光纤节点的引用或为“ null”。在这种情况下,React退出工作循环并准备提交更改。

处理过当前 fiber 后,变量将持有树中下一个 fiber 节点的引用或 null 。在为 null 的情况下,React 退出工作循环并准备好提交更改。

有四个主要函数用于遍历树并初始化或完成工作:

•performUnitOfWork[28]•beginWork[29]•completeUnitOfWork[30]•completeWork[31]

为了演示他们的使用方法,请看以下遍历 fiber 树的动画。我演示中使用了这些函数的简化实现。

每个函数都需要对一个 fiber 节点进行处理,当 React 顺着树往下遍历时,当前活动的 fiber 节点发生了变化。

图中可以清楚地看到,算法是如何从一个分支转到另一个分支。

在回溯到父节点之前,它首先完成子节点的 work,。

注意,垂直方向连线表示同级(sibling 兄弟节点),而弯曲的连接表示子级,例如 b1 没有孩子,而 b2 有一个孩子 c1 。

(译者注,图中的树结构,按照正常的从顶到根的顺序排列的话,应该是从左往右看)

这里 [32]是视频的链接,你可以暂停播放并检查当前节点和函数状态。

从概念上讲,你可以将 “开始 (begin)” 视为 “进入 (stepping into)” 组件,而将“完成 (complete)” 视为 “离开 (stepping out)” 组件。

当我解释这些功能时,你也可以在 这里[33] 体验示例和实现。

我们从 performUnitOfWork 和 beginWork 开始:

function performUnitOfWork(workInProgress) {

最后

基础知识是前端一面必问的,如果你在基础知识这一块翻车了,就算你框架玩的再6,webpack、git、node学习的再好也无济于事,因为对方就不会再给你展示的机会,千万不要因为基础错过了自己心怡的公司。前端的基础知识杂且多,并不是理解就ok了,有些是真的要去记。当然了我们是牛x的前端工程师,每天像背英语单词一样去背知识点就没必要了,只要平时工作中多注意总结,面试前端刷下题目就可以了。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

转到另一个分支。

在回溯到父节点之前,它首先完成子节点的 work,。

注意,垂直方向连线表示同级(sibling 兄弟节点),而弯曲的连接表示子级,例如 b1 没有孩子,而 b2 有一个孩子 c1 。

(译者注,图中的树结构,按照正常的从顶到根的顺序排列的话,应该是从左往右看)

这里 [32]是视频的链接,你可以暂停播放并检查当前节点和函数状态。

从概念上讲,你可以将 “开始 (begin)” 视为 “进入 (stepping into)” 组件,而将“完成 (complete)” 视为 “离开 (stepping out)” 组件。

当我解释这些功能时,你也可以在 这里[33] 体验示例和实现。

我们从 performUnitOfWork 和 beginWork 开始:

function performUnitOfWork(workInProgress) {

最后

基础知识是前端一面必问的,如果你在基础知识这一块翻车了,就算你框架玩的再6,webpack、git、node学习的再好也无济于事,因为对方就不会再给你展示的机会,千万不要因为基础错过了自己心怡的公司。前端的基础知识杂且多,并不是理解就ok了,有些是真的要去记。当然了我们是牛x的前端工程师,每天像背英语单词一样去背知识点就没必要了,只要平时工作中多注意总结,面试前端刷下题目就可以了。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

  • 26
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值