在 React V16 将调度算法进行了重构, 将之前的 stack reconciler 重构成新版的 fiber reconciler,变成了具有链表和指针的 单链表树遍历算法。通过指针映射,每个单元都记录着遍历当下的上一步与下一步,从而使遍历变得可以被暂停和重启。
这里我理解为是一种 任务分割调度算法,主要是 将原先同步更新渲染的任务分割成一个个独立的 小任务单位,根据不同的优先级,将小任务分散到浏览器的空闲时间执行,充分利用主进程的事件循环机制。
官方的一句话解释是“React Fiber是对核心算法的一次重新实现”
理解为是一种 任务分割调度算法,主要是 将原先同步更新渲染的任务分割成一个个独立的 小任务单位,根据不同的优先级,将小任务分散到浏览器的空闲时间执行,充分利用主进程的事件循环机制
在react中,主要做了以下的操作:
1.为每个任务增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新执行优先级低的任务
增加了异步任务,调用requestIdleCallback api
,浏览器空闲的时候执行
dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行
————————————————
链表树遍历算法: 通过 节点保存与映射,便能够随时地进行 停止和重启,这样便能达到实现任务分割的基本前提
任务分割,React 中的渲染更新可以分成两个阶段
reconciliation
阶段: vdom 的数据对比,是个适合拆分的阶段,比如对比一部分树后,先暂停执行个动画调用,待完成后再回来继续比对
Commit
阶段: 将 change list 更新到 dom 上,并不适合拆分,才能保持数据与 UI 的同步。否则可能由于阻塞 UI 更新,而导致数据更新和 UI 不一致的情况。
分散执行: 任务分割后,就可以把小任务单元分散到浏览器的空闲期间去排队执行,而实现的关键是两个新API: requestIdleCallback 与 requestAnimationFrame
低优先级的任务交给requestIdleCallback处理,这是个浏览器提供的事件循环空闲期的回调函数,需要 pollyfill,而且拥有 deadline 参数,限制执行事件,以继续切分任务;
高优先级的任务交给requestAnimationFrame处理。
reconciliation
(调度算法,也可称为 render)
更新 state 与 props;
调用生命周期钩子;
生成 virtual dom
这里应该称为 Fiber Tree
更为符合;
通过新旧 vdom 进行 diff 算法,获取 vdom change
确定是否需要重新渲染
commit
:
如需要,则操作 dom 节点更新
1.什么是类数组?
解释:不是数组, 长的像数组;
类数组本质是一个对象, 只不过是具有如下两个特性的对象:
1)拥有length
属性 和数字索引
2)不具有普通数组的操作方法
2.常见的类数组有哪些
arguments
函数参数
HTMLCollection
通过getElementsByTagName和getElementsByClassName选择器筛选出的DOM元素
NodeList
通过querySelectorAll筛选出的NodeList
先给出答案: 有时表现出异步,有时表现出同步
1.setState只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的。
2.setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数setState(partialState, callback)中的callback拿到更新后的结果。
3. setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
来实现跨层级的组件数据传递
Flux基本概念:
View: 视图层
Action(动作):视图层发出的消息(比如mouseClick)
Dispatcher(派发器):用来接收Actions、执行回调函数
Store(数据层):用来存放应用的状态,一旦发生变动,就提醒Views要更新页面
Flux 的最大特点,就是数据的"单向流动"。
用户访问 View
View 发出用户的 Action
Dispatcher 收到 Action,要求 Store 进行相应的更新
Store 更新后,发出一个"change"事件
View 收到"change"事件后,更新页面
在React函数式组件中,可以使用 useEffect Hook 来处理组件的生命周期方法,包括模拟 componentDidMount、componentDidUpdate 和 componentWillUnmount 方法。
模拟 componentDidMount
方法
在函数式组件中,当组件首次渲染后,可以使用 useEffect 来模拟 componentDidMount 方法。具体实现如下:
jsx
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// 组件首次渲染后执行的代码
// 可以在这里进行一些数据的初始化操作
}, []);
return (
// 组件的JSX代码
);
}
模拟 componentDidUpdate 方法
在函数式组件中,当组件更新后,可以使用 useEffect 来模拟 componentDidUpdate 方法。具体实现如下:
jsx
import { useEffect, useState } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// 组件更新后执行的代码
// 可以在这里进行一些数据的更新操作
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
在 useEffect 的第二个参数中传入一个数组 [count],表示该 useEffect 仅在 count 变量发生变化时执行。因此,在 useEffect 的回调函数中可以执行一些数据的更新操作。
模拟 componentWillUnmount 方法
在函数式组件中,当组件卸载后,可以使用 useEffect 来模拟 componentWillUnmount 方法。具体实现如下:
jsx
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
return () => {
// 组件卸载时执行的代码
// 可以在这里进行一些资源的释放操作
};
}, []);
return (
// 组件的JSX代码
);
}
在 useEffect 的回调函数中返回一个函数,该函数将在组件卸载时执行。因此,在该函数中可以进行一些资源的释放操作,例如取消订阅、清除定时器等。注意,该函数需要返回一个清理函数,以确保在组件卸载时执行。
判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染
允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
何时使用refs:
管理焦点,文本选择或媒体播放。
触发强制动画。
集成第三方 DOM 库。
使用方法:
1.字符串
2、回调函数
3、React.createRef() 创建,并通过 ref 属性附加到 React 元素,当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef(); }
render() {
return <div ref={this.myRef} />; }
}
旧的生命周期
初始化阶段: 由ReactDOM.render()触发—初次渲染
1. constructor() ---构造器
2. componentWillMount() ---将要挂载
3. render() ---render
4. componentDidMount() ---挂载时
更新阶段: 由组件内部this.setSate()或父组件重新render触发
6. shouldComponentUpdate() ---是否要进行更改数据
7. componentWillUpdate() ---将要更新数据
8. render()
9. componentDidUpdate() ---更新数据
卸载组件: 由ReactDOM.unmountComponentAtNode()触发
componentWillUnmount() ---卸载
初始化阶段: 由ReactDOM.render()触发—初次渲染
1. constructor()
2. getDerivedStateFromProps()
3. render()
4. componentDidMount()
getDerivedStateFromProps()
简单翻译过来就是从Props中获得State,所以该函数的功能就是从更新后的props中获取State,它让组件在 props 发生改变时更新它自身的内部 state。
更新阶段: 由组件内部this.setSate()或父组件重新render触发
1. getDerivedStateFromProps()
2. shouldComponentUpdate()
3. render()
4. getSnapshotBeforeUpdate()
5. componentDidUpdate()
卸载组件: 由ReactDOM.unmountComponentAtNode()触发
1. componentWillUnmount()
合成事件 处理了浏览器之间的差异,在上层面向开发者暴露统一的、稳定的、与 DOM 原生事件相同的事件接口。
React 能够从很大程度上干预事件的表现,使其符合自身的需求。
一方面,
将事件绑定在document统一管理,防止很多事件直接绑定在原生的dom元素上。造成一些不可控的情况
另一方面,
React 想实现一个全浏览器的框架, 为了实现这种目标就需要提供全浏览器一致性的事件系统,以此抹平不同浏览器的差异。
声明式渲染:React 使创建交互式 UI 变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。
组件化:创建拥有各自状态的组件,再由这些组件构成更加复杂的 UI。
JSX是js的语法扩展,更好的描述UI应该呈现应有交互的本质形式。
JSX是React.CreacteElement 的语法糖。
为什么使用虚拟DOM?
因为操作真实DOM的耗费的性能代价太高,所以react内部使用js实现了一套dom结构,在每次操作在和真实dom之前,使用实现好的diff算法,对虚拟dom进行比较,递归找出有变化的dom节点,然后对其进行更新操作
React.CreacteElement
函数返回的一个虚拟DOM,就是一个描述真实DOM的纯JS对象
优点
:处理了浏览器兼容性问题,避免用户操作真实DOM
内部经过了xss处理,可以防范xss攻击
容易实现跨平台。
更新的时候实现差异化更新。减少更新dom操作
在初次渲染的时候,其实并不快。在更新的元素内容比较少,可以实现精确更新。不需要把全部的DOM元素删除重添加
缺点
:虚拟DOM需要消耗额外的内存
首次渲染其实不快。
$typeof是啥?标识符,表示一个React元素,也就是所谓的虚拟DOM
相同点:都可以接收属性并返回React元素
不同点:编程思想不同,类组件为面向对象方式编程,函数组件为函数式编程的思想
内存占用:类组件需要创建并保存实例,占用一定的内存,而函数组件不需要创建实例,可以节省内存。
值捕获:函数组件具有值捕获特性。也就是使用一个计时器去获取最初的值得时候,函数组件中进行读取值不会变成状态更新之后的。
可测试性:函数式组件更方便编写单元测试。
状态:类组件有自己的实例,可以自己定义状态,修改状态更新组件。函数式组件以前没有状态,现在可以使用UseState使用状态。
生命周期:类组件有自己完整的生命周期,函数可以使用UseEffect实现生命周期的功能。
逻辑复用:类组件可以使用继承实现逻辑的复用。函数组件使用HOOKs实现。
跳过更新:类组件可以使用shouldComponentUpdate()和PureComponent组件来跳过更新,函数式组件可以使用React.memo来跳过更新。
react16+的渲染流程:
scheduler选择高优先级的任务进行reconciler
reconciler计算变更的内容。
react-dom把变更的内容渲染到页面上。
设计理念:
跨平台渲染=>虚拟dom
快速响应=异步可中断
+增量更新
diff算法的作用
计算出Virtual DOM中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面。
React的diff算法
什么是调和?
将Virtual DOM树转换成actual DOM树的最少操作的过程 称为 调和 。
什么是React diff算法?
diff算法是调和的具体实现。
diff策略
策略一(tree diff):
tree diff
element diff
插入:组件 C 不在集合(A,B)中,需要插入
删除:
组件 D 在集合(A,B,D)中,但 D的节点已经更改,**不能复用和更新,**所以需要删除 旧的 D ,再创建新的。
组件 D 之前在 集合(A,B,D)中,但集合变成新的集合(A,B)了,D 就需要被删除。
移动:组件D已经在集合(A,B,C,D)里了,且集合更新时,D没有发生更新,只是位置改变,如新集合(A,D,B,C),D在第二个,无须像传统diff,让旧集合的第二个B和新集合的第二个D 比较,并且删除第二个位置的B,再在第二个位置插入D,而是 (对同一层级的同组子节点) 添加唯一key进行区分,移动即可。
diff的不足与待优化的地方
尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,会影响React的渲染性能