前端基础知识整理汇总(下)
react 生命周期
React v16.0前的生命周期
初始化(initialization)阶段
此阶段只有一个生命周期方法:constructor。
constructor()
用来完成组件的初始化操作,例如设置this.state的初始值。如果未对state进行赋值或未建立与方法之间的绑定关系,则无需为React组件实现自动生成函数。
为什么必须先调用super(props)?
由于子类自身的this实例,在继承过程中必须先通过调用其父类的构造函数来初始化自身,并获得与父类相同的实例属性和方法随后对其进行进一步处理或附加;若未调用super()方法,则该子类将无法获得完整的this引用
class Checkbox extends React.Component {
constructor(props) {
// ? 这时候还不能使用this
super(props);
// ✅ 现在开始可以使用this
console.log(props); // ✅ {}
console.log(this.props); // ✅ {}
this.state = {};
}
}
为什么super要传 props?
在 React 组件中必要地将 props 传递给 super 构造函数以确保正确继承组件属性与样式信息
但是,在调用 super() 时如果没有传递 props 参数的情况下,在 render 方法和其他相关方法中仍然能够获取 this.props属性的值。
其实是 React 在调用你的构造函数之后,马上又给实例设置了一遍 props。
// React 内部
class Component {
constructor(props) {
this.props = props; // 初始化 this.props
// ...
}
}
// React 内部
const instance = new Button(props);
instance.props = props; // 给实例设置 props
// Button类组件
class Button extends React.Component {
constructor(props) {
super(); // ? 我们忘了传入 props
console.log(props); // ✅ {}
console.log(this.props); // ? undefined
}
}
挂载(Mounting)阶段
此阶段生命周期方法:componentWillMount => render => componentDidMount
1. componentWillMount():
该动作将在组件注入DOM之前执行,并且只会执行一次。 在每个子组件准备 render 时立即触发该操作; 通过此方法,在此操作不会导致组件重新渲染的前提下,相关代码也可预先整合至构造函数中。
2. render(): class 组件唯一必须实现的方法
在 render 被触发时,该组件会检测 props 和 state 的更新情况并返回其中之一.
- React元素通常通过JSX语法构造。例如,在React中使用时会将其解析为对应的DOM结构。
- 数组和fragments使得 render 方法能够返回多个 DOM 节点,并且 Fragments允许将子列表分组而无需增加额外的 DOM 结构。
- Portals提供了一种高效的方式将子组件渲染到不同的 DOM 子树中,并且这一机制特别适合于实现跨组件的数据路由功能。
- 字符串和数值在 DOM 中被解析为文本内容。
- 布尔值和 null 类型不会有任何 DOM 结构生成。
该函数应被视为纯函数。其特点在于,在不修改组件State的情况下完成操作,并保证每次调用时返回的结果一致。此外,该功能不应直接与浏览器进行交互。为了防止side effects,在其内部不应执行this.setState方法。
3. componentDidMount
在组件插入DOM树之后立即执行,并且该操作仅会进行一次。为了实现这一功能,在创建DOM节点时就应该完成相关设置。而完成所有子组件的render操作后才会触发后续步骤
更新(update)阶段
当前阶段生命周期流程:componentWillAcquire → shouldComponentUpdate → renderUpdate → render → onUpdate.
react组件更新机制
setState触发的状态更新或父组件的重绘导致的 props 的更新 ,使得状态和 props 更新后的结果无论是否发生变化 ,都会导致子组件重新渲染。
1. 父组件重新render
- 立即触发新渲染。每当父组件因 parent render 导致触发重传 prop 的事件时,在子组件中将立即执行新的渲染操作(即 new render),无论这些 prop 是否发生变化都可以自动完成这一过程。可以通过 shouldComponentUpdate 方法实现这一功能。
- 在 componentWillReceive 方法内部进行处理时(即在 componentWillReceive 方法内部进行操作),将 props 转换为其 own state,并调用 this.setState() 以避免后续再次触发由于 prop 改变而产生的 render 过程(即 new render)。因为在 componentWillReceive 方法内部会检查 props 是否发生更改:如果这些 props 发生了变化,则会引发 state 的更新(update state),从而导致一次新的 render 过程(new render);为了避免重复执行相同的 render 过程(process),因此无需再次执行由于 prop 改变而引发的 render操作。
2. 自身setState
组件自身触发了 setState 事件, 无论状态是否发生变化. 可以通过 shouldComponentUpdate 方法进行优化.
生命周期分析
1. componentWillReceiveProps(nextProps)
该方法仅应用于因Props引发的组件更新过程中,在接收到Props变化后执行更新操作的方式。其中nextProps表示父组件传递给当前组件的新Props集合。通过比较currentProps与newProps值的变化情况来判断是否需要重传,并采取相应的措施。
2. shouldComponentUpdate(nextProps, nextState)
依据 shouldComponentUpdate() 返回的结果, 来确定 React 组件的输出是否会受到当前 state 或 props 发生更改的影响?默认情况下, 当 state 发生变化时, 组件会重新渲染。
当 props 或 state 发生变化的时候,在组件渲染前会调用 shouldComponentUpdate() 函数。其返回值默认为true。
首次渲染或使用 forceUpdate() 时不会调用该方法。
该方法用于对比this.props与contactab以及this.state与nextState的状态变化情况,在返回true时会触发组件更新流程,在返回false的情况下则会跳过此次更新操作。通过这种方式可以有效减少不必要的渲染操作并提升组件性能水平。
请注意,返回 false 并不会阻止子组件在 state 更改时重新渲染。
- 当在
componentWillRenders方法中调用this.setState()以更新state时,在渲染之前(例如在`shouldComponentUpdate12345678910111213141516或componentWillUpdate方法中),由于未及时更新this.state``引用指向旧值。 - 建议优先采用内置的
PureComponent组件而非手动实现shouldComponentUpdate()方法。采用该组件可简化状态管理逻辑。
3. componentWillUpdate(nextProps, nextState)
该方法在调用render方法之前进行操作,在此期间处理一些组件更新的相关事务。通常被较少地使用。
4. render
render同上
5. componentDidUpdate(prevProps, prevState)
该方法在组件完成更新后的那一刻即刻被调用,并且能够执行对组件更新过程中所涉及的DOM操作。其中prevProps与prevState分别代表在组件处于未更新状态时所拥有的props与state。
卸载阶段
此阶段只有一个生命周期方法:componentWillUnmount
componentWillUnmount
该方法仅在组件即将卸载时被调用,在此阶段可以执行一些清理操作以释放资源。具体来说 ,可以在该阶段清除所有用于组件的定时器事件 ,以及清除所有在componentDidMount生命周期中手动创建的DOM元素引用 。这有助于避免因遗留对象导致的内存泄漏问题 。此外,在terminate() 方法期间不应调用 setState() 方法 ,因为该组件将不再进行刷新操作 。一旦完成卸载过程 ,以后再也不会挂载该组件 。
React v16.0 后的生命周期
React v16.0作为新版本发布时,在其生命周期管理功能中新增了componentDidCatch这一组件方法;该改动属于功能性改进范畴,并不会对原有的组件生命周期管理逻辑产生任何影响
React v16.3版本中新增了两个生命周期管理功能:一个是getDerivedStateFromProps生命周期函数(DeriveStateFromProps),另一个是getSnapshotBeforeUpdate(GetSnapshotBeforeUpdate)。同时终止并移除了三个旧的生命周期函数:一个是componentWillMount(ComponentWillMount)、一个是componentWill.onChange(ComponentWillChange)以及willUpdate(WillUpdate)周期函数,在React 17版本之前仍可使用这些功能,并将根据实际操作情况触发相应的警告信息。
为什么要更改生命周期?
注
- render phase:该阶段负责处理哪些组件会被显示。
- commit phase:该阶段会触发执行(如插入、移动或删除组件)。
在 commit phase 的阶段运行得非常迅速。然而,在真实 DOM 更新方面表现相对较差。因此,在 React 进行组件更新时会为了防止长时间占用浏览器资源而中断并重新启动组件的更新过程。这也就意味着,在某些情况下 render phase 可能需要重复运行以确保组件及时更新。
- 组件构建
- 中间段落
- 接收请求
- 处理更新事件
- 根据属性计算状态
- 判断是否需要更新组件
- 渲染组件
所有这些生命周期均属于 render phase。然而,在这种情况下,由于 render phase 可能会被反复执行,我们仍需特别注意其潜在风险,即可能在其中的操作导致不可预见的影响或错误结果。值得注意的是,相较于当前版本所采用的技术方案,早期版本往往难以有效分离和管理各种业务流程之间的关系,因此我们建议自当前版本起采用新的业务流程设计模式以确保系统运行的安全性和稳定性。
getDerivedStateFromProps(nextProps, prevState)
在React v16.3版本中,在组件创建以及由于父组件更新所触发的情况下才会调用静态方法getDerivedStateFromProps。只有当发生来自父组件的更新时才进行此操作;例如通过自身调用setState或触发forceUpdate事件。
为解决这一问题,在React v16.4版本中做出了相应改进。组件的 derived state 会先于 render 调用触发,并不仅在组件首次加载时会被触发,在随后的状态更新过程中也会被连续执行。
特点:
-
无副作用的影响 。由于Fiber在渲染阶段可能会被多次调用 ,因此API被规划为静态函数 ,无法访问实例的方法 ,也无法通过DOM元素进行操作 。尽管如此 ,但由于可以从props中获取到相关的方法来触发潜在的副作用 ,所以在执行涉及状态变更的函数之前必须谨慎考虑 。
-
仅用于状态更新 。其这个生命周期唯一的作用就是从_COMBO-PROSTATE和prevState中衍生出一个新的state 。它应该返回一个新对象来表示新的状态 ,并将旧的状态替换掉;或者返回null以避免对状态进行不必要的修改 。
-
在React中定义
getDerivedStateFromProps时前需在函数体内显式声明static保留字,并将此函数声明为静态方法;否则会被JavaScript引擎默认忽略。 -
该函数体内
this字段值未初始化;由于静止方法只能通过其所属类对象进行调用而不能通过实例进行调用;因此在React Class组件中此类静止方法将无法访问其所属组件实例的this字段;即此字段值将未初始化。
getSnapshotBeforeUpdate()
getSnapshotBeforeUpdate() 仅仅会在最近的一次渲染输出(即将其提交至DOM节点)之前被调用一次;因此,在此生命周期内可以获取到本次更新之前的DOM状态。此生命周期内的任何返回值会被传递给componentDidUpdate中的第三个参数‘snapshot’字段;如果无返回值,则该字段将设为undefined。
应返回 snapshot 的值(或 null)。
错误处理
当在显示流程、生存周期或子组件初始化阶段出现错误时,该方法会被调用。
- static getDerivedStateFromError(): 此生命周期将在其子组件出现错误时被激活,并通过计算来更新状态变化。
- MainActivity Catch(): 当其子组件遇到错误时会被触发,并且主要用于记录相关的错误信息。
它接收两个参数:
- error —— 发生时的错误实例
- info —— 包含 componentStack键的对象信息
生命周期比较
16.0 前生命周期

16.0 后生命周期:

参考: 浅析 React v16.3 新生命周期函数
react 16做了哪些更新
- React主要提供了一种声明式的UI构建工具,在前端开发中实现了从传统的脚本式编程向声明式编程的转变(即视图驱动模式)。如果直接修改真实的DOM结构(DOM是Document Object Model的缩写),比如将生成的内容直接赋值给innerHTML属性会导致重绘重排等问题出现。为了尽可能地提高性能,在React团队中引入了虚拟DOM概念(Virtual DOM),即通过JS对象来描述DOM树结构,在比较前后两次虚拟对象(vdom diff)时找到最小的操作量(vdom diff),从而优化性能。
- 上述提到的React DOM diff过程在React 16之前被称为stack reconciler算法。这是一个递归处理过程,在DOM树深度较大的情况下进行单次diff操作会占用大量的JS线程资源导致用户交互响应迟缓、页面渲染出现明显卡顿等问题。这些问题在现代前端开发中被视为一个关键挑战。因此为了解决这一问题,React团队对整个架构进行了重构引入了Fiber架构取代了stack reconciler算法采用增量式的渲染策略。具体而言首先将diff过程拆分为两个阶段:reconciliation和commit阶段;其中reconciliation阶段是可以被打断的小任务并行阶段每个阶段都分配给Fiber来进行处理;而commit阶段则是不可分割的一次性操作负责将所有diff节点的影响一次性应用到页面上。
- 由于Reconciliation阶段是可以被打断的并且存在任务优先级的问题因此会导致componentWillMount、componentWillReceive和componentWillUpdate等生命周期函数被多次调用但在React官方声明于React 17版本开始移除了这三个生命周期函数。
- 由于每次更新操作都是从根节点(RootFiber)开始为了更好地实现节点复用和性能优化系统始终维护着workInprogressTree(未来vdom)和oldTree(当前vdom)两个链表它们之间相互引用以解决工作中的潜在问题当workInprogressTree生成错误时系统能够优雅地处理而不至于导致页面渲染崩溃从而保证了系统的稳定性和可靠性。
React hooks原理
在React版本号为16之前的阶段中,默认情况下函数式组件不具备内建的状态管理能力。由于在此之前(即React 16版本之前),仅限于使用类组件时才具备独立的实例特性。而随着Fiber架构于16版本之后推出,在每个节点上都实现了独立且自洽的状态存储能力。从而实现了对状态的有效保存和管理。
Hooks的本质就是闭包和两级链表。
一个
closure即为一个具有权限访问它所嵌入函数作用域中变量或方法的对象,在编程语言中可以通过嵌套形式定义一个closure来实现对该被嵌入函数的作用域进行扩展;通过使用嵌套于某个特定函数之内的closure对象即可实现对该特定函数局部变量及其实现细节的间接引用;借助闭包机制能够突破被调用者内部作用域限制并将其内部的状态和行为与外部环境进行有效交互。
hooks 链表
一个组件包含的所有 hooks 按照链式结构存储在 fiber 节点的 memoizedState 属性中。其中 currentHooks 表示当前正处理中的 fiber 节点。而 nextCurrentHooks 则是即将加入当前 fiber 节点钩子的新列表。
let currentHook: Hook | null = null;
let nextCurrentHook: Hook | null = null;
type Hooks = {
memoizedState: any, // 指向当前渲染节点 Fiber
baseState: any, // 初始化 initialState, 最新的state
baseUpdate: Update<any> | null,
// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
queue: UpdateQueue<any> | null,// 可以让state变化的,即update或dispach产生的update
next: Hook | null, // link 到下一个 hooks
}
复制
state
其实state不是hooks独有的,类操作的setState也存在。
缓存状态和光标存在于哪里?它们是如何与每个函数组件一一对应的?React会生成一棵由自身构建的组件树(或者是一个单链表结构),其中每个节点都代表着一个组件。这些节点上存储了hooks的数据作为组件的信息,并伴随着它们一同诞生和凋零。
因为钩子只能在其外部函数中被调用的原因是什么?它按照钩子定义的执行顺序存储数据,在钩子执行顺序发生变动时,并不会感知到这种变化。
自定义的钩子是如何影响其关联函数组件的行为和交互体验?通过共享同一个 memoizedState 实现统一的状态管理机制,并通过统一的顺序确保组件间的协调运作。
请阐述“Capture Value”特性的生成机制。在每次ReRender过程中都会触发该函数组件的执行;而对于已经运行过该函数组件的部分,则会跳过不做处理。
react setState 异步更新
setState 实现原理
在该实现中,通过一个队列机制来完成 state 的更新过程。每次调用 setState() 方法时,会将当前需要更新的 state 深度复制并添加至状态队列中。并不会立即更新当前状态。该机制能够高效地处理多个状态更新请求。若未使用 setState 方法直接修改 this.state,则不会被加入到状态队列中。在后续任何一次调用 setState 方法处理状态队列时,默认的状态将不再包含之前未被记录的修改内容,默认值也会随之变化。
setState()有的同步有的异步?
在React中,在由React引发的事件处理(例如通过onClick触发的事件处理)的情况下,并不会同步地更新this.state;而其余情况下,则会实现同步更新this.state
特指的是那些通过直接添加事件处理函数的方式绕过React的行为,并不包括使用setimer或setInterval所导致的异步执行机制。
原因:在React在其stateSetUpdate函数的具体实现过程中,默认情况下取决于一个名为isBatchingUpdates的变量来决定是否直接更新this.state或将其放置到队列中进行后续处理。该变量默认设置为false值(即未启用批量更新功能),这意味着通常情况下stateSetUpdate会以同步方式更新this.state。然而,在某些特殊情况下(例如当存在批量更新需求时),另一个名为batchedUpdates的功能会被触发以修改isBatchingUpdates值至true状态。值得注意的是,在React执行事件处理函数之前时会触发该batched_updates功能;这一操作的结果是由于其控制下的事件处理流程将不再进行同步状态更新操作。
实际上内部执行的过程和代码都是同步的
因为合成事件和钩子函数调用顺序在更新前
从而使得无法立即获取到更新后的值
通过将第二个参数设置为 state = partialState, callback 来获取最新的数据。
调用风险
在执行 setState 方法时,在线程安全机制下对事件队列进行操作。该方法实际上会触发 enqueueSetState 的操作,并对其所属的 partialState 和 _pendingStateQueue 进行整合。随后系统将通过 enqueueUpdate 实施状态更新流程。
而 performUpdateIfNecessary 会接收 _pendingElement、_pendingStateQueue、_pendingForceUpdate,并依次调用 reactiveComponent 和 updateComponent 来完成组件更新。
然而,在shouldComponentUpdate或WillUpdate方法内部调用this.setState可能会导致应用程序崩溃。
当在shouldComponentUpdate或 WillUpdate方法中使用 this.setState时(注意:此处应保持原意但避免重复使用"when"),如果this._pendingStateQueue不为空,则performUpda teIfNecessary方法会触发并调用updateComponent方法以执行组件更新;然而,在这种情况下(注意:此处应避免重复使用"while"), updateComponen tmethod又会依次调用should Component Update和 component Will Update方法(注意:此处应避免重复使用"while"),从而导致了无限循环,并最终导致浏览器内存耗尽而崩溃。
React Fiber
当页面中的元素数量较多,尤其是需要频繁进行刷新的时候,"React 15"会导致页面出现卡顿现象.其主要原因在于:由于大量同步计算操作会占用大量CPU资源,从而使得UI渲染受到阻塞.
通常情况下,在浏览器环境中,默认状态下会执行JavaScript运算、布局管理以及图形渲染任务,并且这些操作彼此之间没有重叠。
如果 JS 运算持续占用主线程,页面就没法得到及时的更新。
在我们触发 setState 以更新页面时,在 React 的应用中所有组件树节点会被逐一访问以确定哪些部分需要重新渲染,在此之后系统将重新呈现用户界面。为了确保流畅体验,整个流程必须保持连续性。
当大量页面元素存在时,在所用时间上可能会突破16毫秒限制,并可能导致帧率下降
如何避免主线程长时间承受JavaScript运算的负担?可以通过将JavaScript运算分解为多个阶段逐步执行的方式实现。在完成一部分工作之后释放对浏览器的控制权,并确保浏览器有足够的机会进行页面渲染。等到浏览器处理完毕后接着处理剩余的工作任务。
React 15及以下版本采用递归机制进行渲染,在JS引擎内部使用自身函数调用栈实现;该机制会持续执行直至栈为空时才会终止。
而Fiber开发出了自己的组件调用栈系统。该系统通过链表结构遍历组件树,并具备良好的灵活性特点:能够暂停、继续执行任务以及丢弃未完成的任务。具体实现方法是利用了浏览器提供的requestIdleCallback机制。
requestIdleCallback()会在浏览器空闲时依次被调用,在这种机制下可以实现开发者在主线程处理非阻塞任务的功能,并且这种设计也不会影响到像动画和用户交互这样的延迟触发但至关重要的事件。一般来说,在没有超时时序的情况下,默认情况下请求将按照先进先被处理的方式展开。
React 框架内部的运作可以分为 3 层:
Virtual DOM层是用来呈现页面结构的。
Reconciler层负责执行组件生命周期操作,并进行差异计算。
Renderer层根据平台的不同渲染相应的内容。
该技术将Fiber视为在重叠恢复阶段划分的基本单位;实际上就是一种链表树结构,并且可以用一个纯JS对象进行表示:
const fiber = {
stateNode: {}, // 节点实例
child: {}, // 子节点
sibling: {}, // 兄弟节点
return: {}, // 表示处理完成后返回结果所要合并的目标,通常指向父节点
};
Reconciler区别
- 初始Reconciler曾以名为"Stack Reconciler"的形式存在。该系统性方法运行流程具有高度的连贯性,在操作过程中必须一气呵成;
- Fiber Reconciler在完成一段时间的工作后会将管理权限转移给浏览器,在此期间可实现按段运行;
从Stack Reconciler到Fiber Reconciler,在源码层面实际上是实现了将递归逻辑转换为循环结构的过程。
scheduling(调度)
调度安排是纤维谐调的一个流程, 主要负责将各项任务分配下去, 并实现分段执行. 优先级分为六个等级:
- 同步的,在与之前Stack Reconciler操作同步时执行
- 任务,在next tick事件发生前立即执行
- 动画,在下一帧渲染前完成
- 高优先级,在不久之后立即执行
- 低优先级,则稍微延迟一点也没关系
- 不在屏幕范围内,则等待下次render或scroll操作才进行
高重要性的操作(如键盘输入)能够打断低重要性的操作(如Diff)的处理,并使整个流程能够更快地完成。
Fiber Reconciler 在执行过程中,会分为 2 个阶段:
- 阶段一中构建Fiber树以识别需要更新的节点信息。这一步是一个渐进的过程,并非一次性完成。
- 在阶段二中对所有待更新节点进行批量更新这一操作不可中断。
阶段一具备被打断的可能性,并在处理过程中确保优先级更高的任务能够优先处理。通过框架层面的优化显著降低了页面出现掉帧的概率。
参考: React Fiber 原理介绍 React Fiber
HOC 与render props区别
RenderProps: 将待包裹的组件以props属性形式传递给容器组件后, 该容器随后调用该属性, 并将其注入相关参数。
实现方式:
使用 props.children(props) 来获取 UI 元素。在 复制 2.通过props中的任何函数, 自行定义传入内容 优点: HOC: 接受一个组件作为参数,返回一个新的组件的函数。 由于每次都会生成新的组件对象,在React中这使得diff和状态复用变得不可行。因此,在render方法中无法对高阶组件进行包装处理;然而这也不适合实现动态传参的需求 优点: 总体而言,在Puru组件上添加状态与高级组件类似,并且与React生命周期对应。 React中的数据流呈现单向传输的特点,在实际应用中最为常见的方式是通过props从父组件传递到子组件。 1、被一个相同的父组件所承载的信息可以通过props或context这两种方式传递。 React 合成事件(SyntheticEvent)是一种模拟原生 DOM 事件能力的特殊对象,在该框架中实现了对所有原生DOM事件功能的支持。其作用相当于将各种浏览器原生事件进行跨域统一处理。 为什么要使用合成事件? 实现机制 在 React 中,“合成事件”会通过委托事件的方式绑定到 document 对象上,并在组件卸载环节自动销毁绑定的事件。 当真实 DOM 元素发生 event 时,该 event 会被向上级对象发送,从而触发 React 的 event 处理流程;因此,该 event 将优先执行原生 event,随后才会处理 React 所做的后续操作;最终,真正的 event 将会在 document 对象上进行挂载并执行。为了保证良好的 event 处理效果,建议避免同时使用合成和原生 event;在执行 stopPropagation 方法时,则会导致其他 React event 无法被注册。因为所有 element 的 event 将无法向上级对象发送,导致这些 React events 被跳过而不再响应。 由合成的系列事件组成的集合 合成型的行为集是一种用于提升 React 应用程序性能的技术方案。该系统通过整合多种类型的合成性行为来提升效率。所有这些行为被组织在一个集中化的处理框架中,并且可以根据其性质和应用场景被分类分配到相应的区域。 在 React 应用程序中,默认情况下 render 方法生成的是轻量级的数据结构而非实际存在的 DOM 结构。为了实现高效的DOM操作,在React内部引入了一种称为 virtual DOM 的概念。virtual DOM 机制是基于 JS 的 Object 对象来模拟实际存在的 DOM 节点的一种高效方式。这种机制允许开发者仅管理轻量级的对象即可完成复杂的DOM交互逻辑。 虚拟DOM被认为是React的一大创新,并不仅支持批处理操作(batching),还具备高效的差分算法(diff)。其差分算法的时间复杂度较传统方法显著降低至O(n),提升了性能。 batching(批量处理) 的核心概念是:不管您在 React 的事件处理程序或同步生命周期方法中执行多少次 setState 操作(即 state setter),它都会整合这些操作成为一个更新过程,并最终只触发一次渲染事件。 如果不使用Virtual DOM,则必须直接操作原生DOM。当处理一个长数据表且所有内容发生改变时,默认重置innerHTML是可以接受的。然而,在单行数据变化的情况下,并非如此。这将导致资源浪费。 在内联样式(innerHTML)与虚拟DOM的渲染效率对比中,在内联样式(innerHTML)的渲染流程中包含解析html字符串并生成相应的DOM节点;而虚拟DOM(Virtual DOM)的渲染流程则涉及生成虚拟DOM结构、计算差异并应用必要的DOM更新。 相比而言,在纯 JavaScript 层面上实现 Virtual DOM 的渲染操作及其差分运算确实相对缓慢;尽管如此,在纯 JavaScript 层面上实现 Virtual DOM 的渲染操作及其差分运算确实相对缓慢;相较于后续的DOM操作而言,在成本上仍然低得多;无论是基于 JavaScript 的直接计算还是基于DOM的操作方式,“内HTML”的总计算量始终与整个界面的大小密切相关;而 Virtual DOM 的具体实现则仅受 JavaScript 逻辑以及界面尺寸的影响 与 React 比较而言,在实现功能时, MVVM 的性能也因变动检测的实现原理而有所不同:Angular 基于脏检查;Knockout/Vue/Avalon 采用依赖收集。 Angular的效率问题主要体现在任何一次的小规模变更都会带来与依赖项数量相关的性能开销;然而,在数据发生大幅变化时(例如所有字段同步修改),Angular的表现反而更加突出。具体而言,在初始化阶段以及数据发生变化的过程中(即发生rebuild事件),都需要重新收集相关依赖项以维护应用的状态一致性;然而,在实际开发中这一额外开销通常在数据更新量较小的情况下可忽略不计,在处理大规模数据时则会变得明显。 性能比较 当比较系统性能时, 需要能够区分出初始渲染阶段, 小批量数据更新阶段以及大数据量更新阶段的不同特点. 虚拟DOM技术, 以及 dirty check MVVM模式与 data collection MVVM模式在各自适用的场景下展现出各自的特点, 并对相应的优化策略提出独特的需求. Virtual DOM旨在提升用于轻量级更新操作时的性能表现,并对这些场景进行了专门针对轻量级更新操作进行的性能优化措施。例如SCU和immutable datasets。 传统diff算法采用逐层遍历的方式对节点逐一比对其功能进行分析。该算法的时间复杂度为O(n³),其中n代表树中节点的数量。当处理包含1000个节点的数据时(即n=1000),该算法需要进行约十亿次比较操作(即约1×10⁹次运算)。这样的计算量对于现代前端开发来说无疑是一个巨大的挑战。 diff 算法主要包括几个步骤: React 通过制定大胆的diff策略,将diff算法复杂度从 O(n^3) 转换成 O(n) 。 React 在处理树结构时采用层次化对比方式。在每一轮对比过程中仅会考察当前层级的所有节点。一旦识别出某一层级的某个节点不存在,则其所在的整个子树结构将被彻底移除而不参与后续检查。无需继续深入对比后续层次。 这样仅需对树进行单次遍历即可完成整个DOM树结构的比较分析。 在节点跨层级移动的情况下,并未执行任何移动操作而是生成以该节点为新根节点的新子树结构 这种行为会显著影响React应用性能 因此React官方强烈建议避免在DOM层次上进行跨层级操作。 首先将同一父节点下的所有子节点进行逐一比对; 在开发组件时,维持稳定的DOM架构有助于提高运行效率。具体而言,在无需实际增减DOM节点的情况下,可以通过CSS隐现或显式相关节点来实现功能变更。 对于同一类型的组件来说,在某些情况下其Virtual DOM并不会发生任何变化;如果能够明确这一点的话,则可以节省大量diff运算的时间。因此,在React中提供了一个函数shouldComponentUpdate()用于判断该组件是否需要进行更新和diff运算。 考虑两个类型虽有差异但结构上具有一致性的组件时,则不会对两者进行结构对比,并且会对整个组件的所有内容进行替代操作。一般情况下,在不同类型的 component 中发现具有相同DOM树的机会极为罕见;因此,在实际开发过程中受到这类极端因素显著影响的可能性微乎其微。 对于所有处于同一层级的节点而言,在React diff中包含了以下三种节点动作:INSERT (插入)、MOVE (迁移) 和 REMOVE (去除)。 React 并不负责保有 Duke 和 Villanova 的列表项, 而会重构每个子元素, 也不会执行任何 DOM 移动操作. key 优化 为了解决该问题, React 通过在同级组内分配唯一的 key 值来实现了对同组子节点的区分。 当子元素包含 key 属性时,在 React 中会根据该 key 值匹配原有树结构中的相应子元素以及最新的树结构中的对应子元素。如果发现有完全相同的节点存在,则无需进行任何新建或删除操作;只需将旧集合中的所有节点位置更新至新集合对应的相对位置即可完成数据迁移操作。 1. 监听数据变化的实现原理不同 Vue利用获取器/修改器以及一些函数的劫持来精确感知数据的变化。 React主要依靠引用比较的方法(称为diff)来实现数据同步。 如果未进行优化处理,则会导致频繁不必要的VDOM重构。 2. 数据流不同 在Vue1.0版本中可实现两种双向往绑定关系:父级组件与其子组件间可实现代际属性的双向往;而组件与DOM元素间也可通过v-model机制实现双向往。但在Vue 2.x版本中发现父级组件与其子组件间的双向往已经被断开了(但提供了便捷的解决方案以利用事件机制来间接完成)。React框架并不支持双向往接合关系,默认情况下采用了单向的数据流动模式,并将其称为onChange或setState模式。 3. HoC和mixins Vue整合不同功能的方式是以mixin的形式实现,在Vue中,默认组件被视为一个封装好的函数实体,并不等同于我们在定义组件时传递的对象或调用的函数。 React整合不同功能的方式是以High Orders Components(High Order Components, HoC)的形式实现。 4. 模板渲染方式的不同 在功能机制上存在差异,在开发体验方面也有所不同。 React框架采用JavaScript扩展语言JSX来进行模版渲染, 而Vue则采用了基于扩展型HTML语法实现模版编译的技术。 在功能机制上存在差异, React框架利用内置JavaScript API来处理常见的语法元素, 包括插值运算、条件判断以及循环操作等基础功能模块; 而Vue则是将组件脚本与数据逻辑分离, 在独立于组件代码的功能模块中进行管理, 这种做法使得其模版系统的控制权更加集中化。 举个例子来说明 React 的优点:React 的库函数具有闭合特性,在 render 调用时,默认情况下即可直接调用这些导入的组件。然而,在 Vue 中则不同,在使用模板时由于模板中的数据必须通过 this 进行一次额外的传递,在导入一个组件完成之后(即完成 import),还需在 components 中进行相应的声明。 5. 渲染过程不同 该技术能够更快速地计算出Virtual DOM的差异, 其原因是这种技术会追踪每个组件的所有依赖关系, 因此无需完整重构整个层级结构. 当React的状态发生变化时, 默认情况下所有子组件都会被重新渲染. 可通过shouldComponentUpdate生命周期方法来实现此功能, 并认为这一机制属于一种默认优化. 6. 框架本质不同 Vue的核心是MVVM框架,并源自MVC架构的发展;React属于前端组件化框架,并源自于后端组件化的演变。 CDN是一种部署于全球不同地理区域的 Web 服务集合体,在这个系统中每个节点都是一个被双引号引用的"服务器"。随着单个服务器与目标用户的物理距离越拉越大,在该系统中的延迟会随之增加 头部内联的样式和脚本可能会对页面渲染产生影响,在实际开发中发现将样式代码置于页面头部区域时采用链接的方式引入会导致性能下降;同时建议将前端代码逻辑放置于页面底部,并采用异步加载策略进行导入。 压缩文件可以减少文件下载时间。 将index.html配置为no-cache以便在每次请求时都会比较index.html文件是否有变化。如果没有变化则会使用缓存而如果有变化则会采用新的index.html文件。其余所有文件均采用长时间缓存策略前端代码采用webpack进行打包以确保只有内容发生变动时才会生成新的文件名。对于index.html的设置具体操作如下:设置no-cache选项后系统会在每次请求时执行差异校验如果检测到无差异则直接调用已有的缓存内容否则就会下载并处理最新的index.html文件以确保数据的一致性和最新性。 1. DNS域名解析:拿到服务器ip 当客户端接收到输入的域名地址时 2. 建立TCP链接:客户端链接服务器 TCP提供了稳定的服务机制,在数据传输中采用分组方式,并基于连接建立。 客户端发送带有SYN标志的第一阶段数据包至服务端。 SYN被定义为设置连接初始序号的一种方法;ACK的作用是确认并确保确认号码的有效性;RST命令用于重置当前的数据连接状态;FIN标志表示发送方已停止传输本报文段的信息 客户端:“你好,在家不。” -- SYN 服务端:“在的,你来吧。” -- SYN + ACK 客户端:“好嘞。” -- ACK 3. 发送HTTP请求 4. 服务器处理请求 5. 返回响应结果 6. 关闭TCP连接(需要4次握手) 为防止两端的服务器和客户端在没有请求或相应数据传输的情况下发生资源消耗,在任何一方均可主动发送关闭指令的情形下触发。 当连接关闭时(即客户端向服务器发出FIN信号)时 客户端:"伙伴们"没有需要传输的数据了"咱们就先把连接关闭了吧" -- FIN+seq 服务端:"收到回应"接收到您的信息后"检查一下我们这边是否有数据" -- ACK+seq+ack 服务端:"伙伴们"同样没有需要传输的数据了"咱们就可以放心地关闭连接了" - FIN+ACK+seq+ack 客户端:"明白了" -- ACK+seq+ack 7. 浏览器解析HTML 在解析网页内容时 以解析的方式处理HTML内容而创建DOM数据结构;对CSS文件进行解析以构建CSS对象模型(CSSOM)数据结构;随后利用DOM与CSSOM数据结构构建出一系列渲染 tree实例;值得注意的是,在此过程中构建出的渲染 tree与传统的DOM tree存在显著差异;特别地,在这种情况下构建出的渲染 tree并未包含那些通常位于head标记或设置为不可见(如display属性设为none)而无需在页面上显示的内容 在处理过程中,并非所有步骤都是连续完成的;例如,在同时处理CSS样式时,并行加载HTML文件是可行的;然而,在执行JavaScript脚本时,则会暂停后续HTML文件的解析工作而导致资源被阻塞。 8. 浏览器渲染页面 基于渲染树结构计算CSS样式方案,则包括每个节点在页面中的尺寸、位置以及其他几何属性等细节信息。而HTML默认采用流式布局方式,在此情况下通过JavaScript会影响DOM的外观样式进而包括调整尺寸与位置等参数。最终浏览器按照各节点的位置与样式进行绘制并呈现给用户。 replaint: 定义为在屏幕上更新绘制区域的操作,在此操作中不会修改布局结构。例如当某个CSS属性值发生变化时(如背景色),其他元素的位置与几何尺寸将保持不变。 reflow: 表示由于元素的几何尺寸发生了变化而导致需要重新计算渲染树。 参考: 细说浏览器输入URL后发生了什么 浏览器输入 URL 后发生了什么? 路由是用于连接后端服务器的一种方式,在其作用范围内通过不同路径发起请求获取不同资源。 路由的概念最初出现在后端系统中,在前后未整合阶段通常由后端负责管理路由逻辑;当客户端发送请求时,服务器会解析请求中的URL路径,并根据该路径返回相应的网页或资源信息。 Ajax是一种支持非阻塞式数据传输的技术框架;其全称为Asynchronous JavaScript And XML;它被浏览器采用以实现异步加载。 当时,在Ajax尚未出现的时候,传统的网页大多直接返回HTML内容。这种情况下,在用户每次进行页面更新时都需要手动刷新页面以查看最新内容,并因此影响了用户体验。针对这一问题,在20世纪90年代末期提出了 Ajax(异步加载方案)。有了Ajax之后,在不中断正常操作的情况下让用户无需每次都手动刷新页面即可获取最新内容。随后逐渐演变成单页应用(SPAs)技术。 SPA 中用户的操作由 JavaScript 控制以修改 HTML 内容,并未改变其 URL 未发生更改的情况。这导致了以下两个相关问题: 前端路由就是为了解决上述问题而出现的。 前端路由的具体实现过程通常包括判断 url 是否发生变动,并在此基础上捕获访问的 url 地址进行解析以匹配相应的路由规则。具体实现时通常会采用以下两种策略: hash 包括 url 后面的 # 符号及其后续的所有字符。当 hash 值发生变化时,并不会引发浏览器向服务器发送请求。由于浏览器无需发送请求,因此也不会重新加载页面。 ...的更改会导致对应的...事件被触发,并且可以通过该事件来关注和监控...的变化情况。 在早期阶段(即在HTML5出现之前),浏览器已经具备了 history 对象的功能。然而,在早期版本的历史功能中主要功能就是实现多页面的跳转: 在 HTML5 的规范中,history 新增了几个 API: 复制 因为JavaScript中的history对象提供了pushState()和replaceState()方法来管理URL,并且这些方法不会触发浏览器的历史重放机制,在HTML5标准下的历史对象(history)具备实现前端路由能力的条件。window对象通过onpopstate事件来监控历史栈的变化状态,在历史信息发生变更时会自动触发这一事件。 调用 请注意:调用pushState()方法时不会触发hashchange事件, 因为即使新旧URL仅在锚的数据上存在差异 请注意:调用pushState()方法时不会触发hashchange事件, 因为即使新旧URL仅在锚的数据上存在差异 Vue Router 和 React Router 均基于前端路由技术开发而成,并采用了相似的核心逻辑架构~在 React Router 中常用的 History 类型主要包括以下几种形式: Babel是一种代码转换工具,在具体应用中例如将ES6代码转换为相应的ES5版本,并非直接复制粘贴原生语法;同时也能处理诸如将JSX语法转化为JavaScript语言的实际场景下使用;通过这种方式开发者能够提前体验到新的JavaScript特性 Plugin 核心在于通过Babel插件(plugin)来实现代码转换功能。该工具通常会以最小的力量进行拆分以便开发者按需引入这些小模块 这不仅提升了性能还增强了系统的可扩展性。例如 在将JavaScript中的Class转义为 bow-tie语法时 Babel官方提供了超过18个专门的小模块供开发者选择使用。如果开发者只想体验Arrow Function的特点只需引入transform-es2015-arrow-functions这个插件就足够了 而无需加载整个 ES6 生态系统 Preset 本质上,可以把Babel Preset看作是Babel Plugin的一个集合体。如果需要将所有的ES6代码转换为ES5,并且逐个插件导入的方式效率相对较低时,则可以选择使用Babel Preset。例如,babel-preset-es2015这个版本就集成了与ES6转换相关的全部插件。 Plugin与Preset执行顺序 可以同时使用多个Plugin和Preset,此时,它们的执行顺序非常重要。 比如.babelrc配置如下,那么执行的顺序为: 为增强网站整体性能优化策略 采用集群部署方案 将静态资源与动态网页分别实施集群部署策略 将这些静态资源分配至CDN服务器进行存储 相应地 在线访问的网页内容将直接指向本地存储路径 在维护系统时 将会同步更新所有相关的在线数据 同时 相应地 更新HTML文件中的引用信息 在同时修改页面结构与样式时,并更新了相应的静态资源URL地址,请问现在需要考虑的是如何安排代码发布顺序?究竟是先上传页面文件还是优先上传静态资源文件? 这一类怪异的问题主要源于资源的 覆盖性质,在待发布资源被用来覆盖已发布资源的情况下,则会出现这一类问题。 轻而易举地解决这个问题吗?答案是可以做到的就是完成一种非重叠式的更新策略。具体来说就是通过提取所有文档摘要的信息并将这些信息应用到相应的资源文件重命名过程中。随后将这些摘要信息整合到相应的资源文件发布路径中这样就可以确保当有内容发生修改时它们会被独立地以新的版本在云端线上平台重新发布而不影响原有数据的安全性和完整性 在上线的过程中,首先进行全量静态资源的部署;随后进行灰度化的页面部署;最终实现了问题的有效解决. 大公司的静态资源优化方案,基本上要实现这么几个东西:
// 定义
const RenderProps = props => <div>
{props.children(props)}
</div>
// 调用
<RenderProps>
{() => <>Hello RenderProps</>}
</RenderProps>
// 定义
const LoginForm = props => {
const flag = false;
const allProps = { flag, ...props };
if (flag) {
return <>{props.login(allProps)}</>
} else {
return <>{props.notLogin(allProps)}</>
}
}
// 调用
<LoginForm
login={() => <h1>LOGIN</h1>}
noLogin={() => <h1>NOT LOGIN</h1>}
/>
2.无需担心Props命名冲突,在 render 函数中仅提取必要的 state
3.避免不必要的组件加深层级
4.以动态方式构建 render props 模式,并确保所有更改均在 render 阶段触发
class Home extends React.Component {
// UI
}
export default Connect()(Home);
1、兼容新标准 2、便于复用,在这种情况下HOC采用纯函数式结构并返回组件对象 3、能够处理多参数输入从而扩大了适用范围 缺点:
1、当多个HOC协同工作时很难追踪子组件所使用的props具体由哪个HOC传递 2、过多嵌套可能导致相同名称的props出现从而引发冲突 3、这种做法可能导致大量不必要的组件生成进而增加了层级复杂度React 通信
2、通过引入特定的全局机制来实现信息传递。
3、采用发布与订阅相结合的模式react合成事件
react 虚拟dom
什么是虚拟dom?
虚拟 DOM 与 原生 DOM
Real DOM
Virtual DOM
1. 更新缓慢。
1. 更新更快。
2. 可以直接更新 HTML。
2. 无法直接更新 HTML。
3. 如果元素更新,则创建新DOM。
3. 如果元素更新,则更新 JSX 。
4. DOM操作代价很高。
4. DOM 操作非常简单。
5. 消耗的内存较多。
5. 很少的内存消耗。
虚拟 DOM 与 MVVM
其他基于 MVVM 的框架(如 Angular、Knockout、Vue 和 Avalon)普遍采用了相似的设计理念。
这些框架主要通过 Directive/Binding 对象来实现对数据动态绑定,
实时跟踪数据变化,并保留对相应 DOM 元素的实际引用。
其变化检测机制侧重于数据层面,
而 React 则侧重于 DOM 结构的变化检测。
diff 算法
随后基于此对象生成真实完整的 DOM 结构体.
该结构体会被 .NET 框架整合并插入到当前文档中。
系统将重构一棵新的对象树.
(NET系统会对比新旧两棵树以获取差异信息)
(将差异信息应用)后
系统会更新视图界面。diff 策略
tree diff(层级比较)
随后识别节点的类型;
当遇到组件时,请执行 Component Diff操作;
而如果涉及标签或元素,则执行 Element Diff操作。component diff(组件比较)
element diff (元素比较)
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
react与vue区别
性能优化
1. 静态资源使用 CDN
2. 无阻塞
3. 压缩文件
4. 图片优化
5. 减少重绘重排
6. 使用 requestAnimationFrame 来实现视觉变化
window.requestAnimationFrame()向浏览器发出指令——指示你需要执行一个动画,并且要求在下次重绘事件发生前更新动画内容。此方法需接收一个特定事件作为触发条件,并将在下一个绘制过程中被触发以完成更新操作。7. webpack 打包, 添加文件缓存
在访问任何从本地加载的资源时必须先验证其有效性和完整性。
一旦达到此阈值(以秒计),相应的缓存将被视为已失效。
在达到该时长之前(以秒计),浏览器将不再提交新的请求输入url后发生了什么
在客户端与服务器之间的TCP通信中不可忽视的是『三次握手』的过程。
通过这一过程(即即『3次握手』),双方能够清晰地确认收发操作均正常进行
服务端接收到该数据包后立即返回带有SYN/ACK标志的确认应答数据包。
最终客户端发送带有ACK标志的第三阶段应答数据包完成双方通信握手流程,
实现连接建立。
前端路由
什么是路由
前端路由
前端路由的实现方式
1. Hash模式
// 监听hash变化,点击浏览器的前进后退会触发
window.onhashchange = function() { ... }
window.addEventListener('hashchange', function(event) { ...}, false);2.History 模式
history.go(-1); // 后退一页
history.go(2); // 前进两页
history.forward(); // 前进一页
history.back(); // 后退一页
history.pushState(); // 向当前浏览器会话的历史堆栈中添加一个状态
history.replaceState();// 修改了当前的历史记录项(不是新建一个)
history.state // 返回一个表示历史堆栈顶部的状态的值
history.pushState()或history.replaceState()的行为不会导致popstate事件的发生。仅在执行浏览器相关操作时才会触发该事件。例如,在执行history.back()或history.forward()操作后会触发window.onpopstate事件。
// 历史栈改变
window.onpopstate = function() { ... }两种模式对比
对比
Hash
History
路径
带#, 路径丑
正常路径
兼容性
>=ie8
>=ie10
实用性
直接使用,无需服务端配合处理。
需服务端配合处理
命名空间
同一document
同源
锚点
导致锚点功能失效
锚点功能正常
前端路由实践
Babel Plugin与preset区别
原始代码 --> [Babel Plugin] --> 转换后的代码
{
"presets": [
"es2015",
"es2016"
],
"plugins": [
"transform-react-jsx",
"transform-async-to-generator"
]
}怎样开发和部署前端代码
大数相加
function add(a, b){
const maxLength = Math.max(a.length, b.length);
a = a.padStart(maxLength, 0);
b = b.padStart(maxLength, 0);
let t = 0;
let f = 0;
let sum = "";
for (let i = maxLength - 1; i >= 0; i--) {
t = parseInt(a[i]) + parseInt(b[i]) + f;
f = Math.floor(t / 10);
sum = `${t % 10}${sum}`;
}
if (f === 1){
sum = "1" + sum;
}
return sum;
}斐波那契数列求和
function fib(n) {
if (n <= 0) {
return 0;
}
let n1 = 1;
let n2 = 1;
let sum = 1;
for(let i = 3; i <= n; i++) {
[n1, n2] = [n2, sum];
sum = n1 + n2;
}
return sum;
};
