最近在准备面试。复习了一些react的知识点,特此总结。vue
react 16之前的生命周期是这样的react
组件在首次渲染时会被实例化,而后调用实例上面的componentWillMount,render和componentDidMount函数。组件在更新渲染时能够调用componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate,render和componentDidUpdate函数。组件在卸载时能够调用componentWillUnmount函数。面试
借图:算法
从 React v16.3 开始,React 建议使用getDerivedStateFromProps
和getSnapshotBeforeUpdate
两个生命周期函数替代 componentWillMount
,componentWillReceiveProps
和componentWillUpdate
三个生命周期函数。这里须要注意的是 新增的两个生命周期 函数和原有的三个生命周期函数必须分开使用,不能混合使用api
目前的生命周期(借图):数组
componentWillMount存在的问题浏览器
有人认为在componentWillMount中能够提早进行异步请求,避免白屏。可是react在调用render渲染页面的时候,render并不会等待异步请求结束,再获取数据渲染
。这么写是有潜在隐患的。缓存
而在react fiber以后 可能在一次渲染中屡次调用。缘由是:react fiber技术使用增量渲染来解决掉帧的问题,经过requestIdleCallback调度执行每一个任务单元,能够中断和恢复,生命周期一旦中断,恢复以后会从新跑一次以前的生命周期
markdown
新的生命周期并发
static getDerivedStateFromProps
getSnapshotBeforeUpdate
因为React渲染/更新过程一旦开始没法中断,持续占用主线程,主线程忙于执行JS,无暇他顾(布局、动画),形成掉帧、延迟响应(甚至无响应)等不佳体验。fiber应运而生。
Fiber 是对react reconciler(调和) 核心算法的重构。关键特性以下:
增量渲染用来解决掉帧的问题,渲染任务拆分以后,每次只作一小段,作完一段就把时间控制权交还给主线程,而不像以前长时间占用。
Fiber tree
Fiber以前的reconciler(被称为Stack reconciler)自顶向下的递归mount/update,没法中断(持续占用主线程),这样主线程上的布局、动画等周期性任务以及交互响应就没法当即获得处理,影响体验。
Fiber解决这个问题的思路是把渲染/更新过程(递归diff)拆分红一系列小任务,每次检查树上的一小部分,作完看是否还有时间继续下一个任务,有的话继续,没有的话把本身挂起,主线程不忙的时候再继续。
fiber树实际上是一个单链表结构
,child指向第一个子节点,return指向父节点,sibling指向下个兄弟节点。结构以下:
// fiber tree节点结构
{
stateNode,
child,
return,
sibling,
...
}
复制代码
Fiber reconciler
reconcile过程分为2个阶段:
1.(可中断)render/reconciliation 经过构造workInProgress tree得出change
2.(不可中断)commit 应用这些DOM change(更新DOM树、调用组件生命周期函数以及更新ref等内部状态)
构建workInProgress tree的过程就是diff的过程,经过requestIdleCallback来调度执行一组任务,每完成一个任务后回来看看有没有插队的(更紧急的),每完成一组任务,把时间控制权交还给主线程,直到下一次requestIdleCallback回调再继续构建workInProgress tree
生命周期也被分红了两个阶段:
// 第1阶段 render/reconciliation
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
// 第2阶段 commit
componentDidMount
componentDidUpdate
componentWillUnmount
复制代码
第1阶段的生命周期函数可能会被屡次调用,默认以low优先级执行,被高优先级任务打断的话,稍后从新执行。
fiber tree与workInProgress tree
双缓冲技术:指的是workInProgress tree构造完毕,获得的就是新的fiber tree,而后把current指针指向workInProgress tree,因为fiber与workInProgress互相持有引用,旧fiber就做为新fiber更新的预留空间,达到复用fiber实例的目的。
每一个fiber上都有个alternate属性,也指向一个fiber,建立workInProgress节点时优先取alternate,没有的话就建立一个
let workInProgress = current.alternate;
if (workInProgress === null) {
//...
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// We already have an alternate.
// Reset the effect tag.
workInProgress.effectTag = NoEffect;
// The effect list is no longer valid.
workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
}
复制代码
这么作的好处:
fiber 中断 恢复
中断:检查当前正在处理的工做单元,保存当前成果(firstEffect, lastEffect),修改tag标记一下,迅速收尾并再开一个requestIdleCallback,下次有机会再作
断点恢复:下次再处理到该工做单元时,看tag是被打断的任务,接着作未完成的部分或者重作
P.S.不管是时间用尽“天然”中断,仍是被高优任务粗暴打断,对中断机制来讲都同样。
在代码中调用setState函数以后,React 会将传入的参数对象与组件当前的状态合并,而后触发所谓的调和过程(Reconciliation)。通过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树而且着手从新渲染整个UI界面。在 React 获得元素树以后,React 会自动计算出新的树与老树的节点差别,而后根据差别对界面进行最小化重渲染。在差别计算算法中,React 可以相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是所有从新渲染。
setState调用时有时是同步的(settimeout,自定义dom事件),有时是异步的(普通调用)
React事件是经过事件代理,在最外层的 document上对事件进行统一分发,并无绑定在真实的 Dom节点上。 并且react内部对原生的Event对象进行了包裹处理。具备与浏览器原生事件相同的接口,包括 stopPropagation()
和 preventDefault()
。
若是有多个同步setState(...)
操做,React 会将它们的更新(update)前后依次加入到更新队列(updateQueue),在应用程序的 render 阶段处理更新队列时会将队列中的全部更新合并成一个,合并原则是相同属性的更新取最后一次的值。若是有异步setState(...)操做,则先进行同步更新,异步更新则遵循 EventLoop 原理后续处理。
React 更新
// 源码位置:packages/react-reconciler/src/ReactUpdateQueue.js
function createUpdate(expirationTime, suspenseConfig) {
var update = {
// 过时时间与任务优先级相关联
expirationTime: expirationTime,
suspenseConfig: suspenseConfig,
// tag用于标识更新的类型如UpdateState,ReplaceState,ForceUpdate等
tag: UpdateState,
// 更新内容
payload: null,
// 更新完成后的回调
callback: null,
// 下一个更新(任务)
next: null,
// 下一个反作用
nextEffect: null
};
{
// 优先级会根据任务体系中当前任务队列的执行状况而定
update.priority = getCurrentPriorityLevel();
}
return update;
}
复制代码
每个更新对象都有本身的过时时间(expirationTime)、更新内容(payload),优先级(priority)以及指向下一个更新的引用(next)。其中当前更新的优先级由任务体系统一指定。
React 更新队列
// 源码位置:packages/react-reconciler/src/ReactUpdateQueue.js
function createUpdateQueue(baseState) {
var queue = {
// 当前的state
baseState: baseState,
// 队列中第一个更新
firstUpdate: null,
// 队列中的最后一个更新
lastUpdate: null,
// 队列中第一个捕获类型的update
firstCapturedUpdate: null,
// 队列中第一个捕获类型的update
lastCapturedUpdate: null,
// 第一个反作用
firstEffect: null,
// 最后一个反作用
lastEffect: null,
firstCapturedEffect: null,
lastCapturedEffect: null
};
return queue;
}
复制代码
这是一个单向链表结构。
当咱们使用setState()时, React 会建立一个更新(update)对象,而后经过调用enqueueUpdate
函数将其加入到更新队列(updateQueue)
// 源码位置:packages/react-reconciler/src/ReactUpdateQueue.js
// 每次setState都会建立update并入updateQueue
function enqueueUpdate(fiber, update) {
// 每一个Fiber结点都有本身的updateQueue,其初始值为null,通常只有ClassComponent类型的结点updateQueue才会被赋值
// fiber.alternate指向的是该结点在workInProgress树上面对应的结点
var alternate = fiber.alternate;
var queue1 = void 0;
var queue2 = void 0;
if (alternate === null) {
// 若是fiber.alternate不存在
queue1 = fiber.updateQueue;
queue2 = null;
if (queue1 === null) {
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
}
} else {
// 若是fiber.alternate存在,也就是说存在current树上的结点和workInProgress树上的结点都存在
queue1 = fiber.updateQueue;
queue2 = alternate.updateQueue;
if (queue1 === null) {
if (queue2 === null) {
// 若是两个结点上面均没有updateQueue,则为它们分别建立queue
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
queue2 = alternate.updateQueue = createUpdateQueue(alternate.memoizedState);
} else {
// 若是只有其中一个存在updateQueue,则将另外一个结点的updateQueue克隆到该结点
queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
}
} else {
if (queue2 === null) {
// 若是只有其中一个存在updateQueue,则将另外一个结点的updateQueue克隆到该结点
queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
} else {
// 若是两个结点均有updateQueue,则不须要处理
}
}
}
if (queue2 === null || queue1 === queue2) {
// 通过上面的处理后,只有一个queue1或者queue1 == queue2的话,就将更新对象update加入到queue1
appendUpdateToQueue(queue1, update);
} else {
// 通过上面的处理后,若是两个queue均存在
if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
// 只要有一个queue不为null,就须要将将update加入到queue中
appendUpdateToQueue(queue1, update);
appendUpdateToQueue(queue2, update);
} else {
// 若是两个都不是空队列,因为两个结构共享,因此只在queue1加入update
appendUpdateToQueue(queue1, update);
// 仍然须要在queue2中,将lastUpdate指向update
queue2.lastUpdate = update;
}
}
...
}
function appendUpdateToQueue(queue, update) {
if (queue.lastUpdate === null) {
// 若是队列为空,则第一个更新和最后一个更新都赋值当前更新
queue.firstUpdate = queue.lastUpdate = update;
} else {
// 若是队列不为空,将update加入到队列的末尾
queue.lastUpdate.next = update;
queue.lastUpdate = update;
}
}
复制代码
在enqueueUpdate
函数中,React 将更新加入到更新队列时会同时维护两个队列对象 queue1 和 queue2,其中 queue1 是应用程序运行过程当中 current 树上当前 Fiber 结点最新队列,queue2 是应用程序上一次更新时(workInProgress 树
)Fiber 结点的更新队列,它们之间的相互逻辑是下面这样的。
fiber.updateQueue
,queue2 取的是fiber.alternate.updateQueue
;null
,则调用createUpdateQueue(...)
获取初始队列;null
,则调用cloneUpdateQueue(...)
从对方中获取队列;null
,则将update
做为lastUpdate
加入 queue1 中。React 处理更新队列
React 应用程序运行到 render 阶段时会处理更新队列,处理更新队列的函数是processUpdateQueue
// 源码位置:packages/react-reconciler/src/ReactUpdateQueue.js
function processUpdateQueue(workInProgress, queue, props, instance, renderExpirationTime) {
...
// 从队列中取出第一个更新
var update = queue.firstUpdate;
var resultState = newBaseState;
// 遍历更新队列,处理更新
while (update !== null) {
...
// 若是第一个更新不为空,紧接着要遍历更新队列
// getStateFromUpdate函数用于合并更新,合并方式见下面函数实现
resultState = getStateFromUpdate(workInProgress, queue, update, resultState, props, instance);
...
update = update.next;
}
...
// 设置当前fiber结点的memoizedState
workInProgress.memoizedState = resultState;
...
}
// 获取下一个更新对象并与现有state对象合并
function getStateFromUpdate(workInProgress, queue, update, prevState, nextProps, instance) {
switch (update.tag) {
case UpdateState:
{
var _payload2 = update.payload;
var partialState = void 0;
if (typeof _payload2 === 'function') {
// setState传入的参数_payload2类型是function
...
partialState = _payload2.call(instance, prevState, nextProps);
...
} else {
// setState传入的参数_payload2类型是object
partialState = _payload2;
}
// 合并当前state和上一个state.
return _assign({}, prevState, partialState);
}
}
}
复制代码
processUpdateQueue函数用于处理更新队列,在该函数内部使用循环的方式来遍历队列,经过update.next依次取出更新(对象)进行合并,合并更新对象的方式是:
React 应用程序首次渲染时在 prerender
阶段会初始化 current 树
。最开始的 current 树只有一个根结点— HostRoot类型的 Fiber 结点。在后面的 render 阶段会根据此时的 current 树建立 workInProgress 树
。在 workInProgress 树
上面进行一系列运算(计算更新等),最后将反作用列表(Effect List)传入到 commit
阶段。当 commit 阶段运行完成后将当前的 current 树
替换为 workInProgress 树
,至此一个更新流程就完成了。简述:
current 树是未更新前应用程序对应的 Fiber 树,workInProgress 树是须要更新屏幕的 Fiber 树。
FiberRootNode
构造函数只有一个实例就是 fiberRoot
对象。而每一个 Fiber 节点都是 FiberNode
构造函数的实例,它们经过return,child和sibling三个属性链接起来,造成了一个巨大链表。React 对每一个节点的更新计算都是在这个链表上完成的。React 在对 Fiber 节点标记更新标识的时候的作法就是为节点的effectTag
属性赋不一样的值。
借图:
这两个api,其实概念上仍是很好理解的,一个是「缓存函数」, 一个是缓存「函数的返回值」。
一旦在条件语句中声明hooks,在下一次函数组件更新,hooks链表结构,将会被破坏,current树的memoizedState缓存hooks信息,和当前workInProgress不一致,若是涉及到读取state等操做,就会发生异常。
总结:能够认为维护了一个state数组 和一个cursor
指针 。第一次渲染时把当前的值push进states
数组里,把绑定了指针的setter推动 setters
数组中。每次的后续渲染都会重置指针cursor
的位置,并会从每一个数组中读取对应的值。每一个 setter 都会有一个对应的指针位置的引用,所以当触发任何 setter 调用的时候都会触发去改变状态数组中的对应的值。若是放在条件语句中。那么setstate的顺序就会发生错乱。
let first = true;
const [num1, setNum1] = usestate(1);
if (first) {
const [num2, setNum2] = usestate(2);
first = false
}
const [num3, setNum3] = usestate(3);
复制代码
第一次渲染时,维护的states数组时[1,2,3] ,setters数组指针指向[state[0],state[1],state[2]]。可是当咱们的组件更新时,这时候的states数组是[1,2,3] setters数组是[state[0],state[1]]。显然咱们的setNum3被设置成了2。
vue react 共同点:
1. 监听数据变化的实现原理不一样
2. 数据流的不一样
3. 组件通讯的区别
4. 模板渲染方式的不一样