前置知识
使用react也有将近一年了,在使用的过程中,我相信你也会像我一样,存在很多疑惑的点; 举些例子,比如为什么react有些生命钩子会执行多次,而有些只会安全的执行一次?react 16 大版本更新的fiber到底是个什么东西?诸如此类的问题,我也百思不得其解;所以我踏上了探究源码之路;
总所周知,react源码不是一般的多,直接阅读react源码,真的是劝退... 在搜集react源码资料的时候,发现比较全和新的资料也很少,偶然一次机会看到奇舞团大佬按照react源码思路自己debug造了一个;在学习他的源码时候,我也私下和他交流了很多,真的是听君一席话,胜读十年书呀;2333....(还是自己太菜了,非常感谢大佬解答我的问题)
言归正传,这里强烈推荐他电子书,源码系列文章是基于最新的16.13.1解析的; 虽然没有更完,但是写得相当精彩,反正我是看了还想看那种。(有点崔更了,哈哈)
本文是根据最新的16.13.1进行解析,目的是把整体的源码流程看懂个大概,并不会深入到很细节的东西; 也就是说把react的整体更新流程弄明白,可以帮助你更好的去探究最终的源码细节,如果有不正确的地方,还望大佬们指正; 虽说现在更新到了16.13.1版本了,但是整体的架构依然没有变,这里我推荐几个必读的资料,很精彩;
- Lin Clark - A Cartoon Intro to Fiber - React Conf 2017
- 这可能是最通俗的 React Fiber(时间分片) 打开方式
- Deep In React 之浅谈 React Fiber 架构(一)
React16架构
在了解react架构之前,我们还需要了解一下浏览器渲染原理,主流的浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次。我们知道,JS是可以操作DOM的,所以JS脚本执行和浏览器布局、绘制是处于同一线程(渲染线程)。 也就是浏览器在一帧的时间内要完成以下工作
- JS脚本执行
- 样式布局
- 样式绘制 当JS执行时间过长,超出了16.6ms,这次刷新就没有时间执行样式布局和样式绘制了。这就是造成卡顿的原因
在16大版本之前,也就是React15架构只分为两层,Reconciler(协调器,可不中断)+ Renderer(渲染器,不可中断);也就是说协调阶段,同步(递归更新完)更新的;这很容易造成JS执行时间过长,超出了16.6ms,也就是说一旦开始更新,就不可中断,一口气做完。会造成卡顿,这样的用户体验非常差;
react团队发现,让用户操作感觉不到卡顿,操作以外的有延迟,卡顿一下,用户是完全可以接受的;JS执行时间过长,所以react更改了架构;React16架构可以分为三层:
- Scheduler(调度器,可中断)—— 调度任务的优先级,高优任务优先进入Reconciler
- Reconciler(协调器,可中断)—— 负责找出变化的组件
- Renderer(渲染器,不可中断)—— 负责将变化的组件渲染到页面上
- 这样的三层架构,个人觉得有以下几个优点
- 像计算机网路协议一样,每一层专注干一件事情(单一职责),这样架构的应用,生命周期都相对的长;TCP/IP协议不是活了几十年了嘛。QAQ
- 可扩展性和灵活性很强;给开发者保留了很多底层抽象的可能;(antd 就是一个例子)
- 熟悉react框架以后,转其他框架相对轻松,因为react是最早出现的主流框架。(该懂的应该都懂)
- 当然也会有一些非常明显的缺点
- 学习成本的提高,像新出的hook,和未来即将稳定的Concurrent 模式,都存在一定的学习成本;
- react并没有做很多优化工作,比如在编译阶段,像vue这样的框架就做了相应的优化;不过这也是框架和库的区别;因为react的定位始终是库,react核心开发人员dan自己也说过,未来的发展不会把react变成框架;
初始化阶段
要理解react的更新流程,我觉得最好的方式是画流程图,结合一点源码注释;不然在学习源码的过程会非常的混乱;先看react的初始化阶段。
- reactDOM.render,还记得应用挂载的时候么?
- 应用挂载时候的入口
// ReactDOM.render(<App name="Hello"/>, document.querySelector('#app'));
const ReactDOM = {
render(element, container) {
// 创建 FiberRoot
const root = container._reactRootContainer = new ReactRoot(container);
// 首次渲染不需要批量更新
DOMRenderer.unbatchedUpdates(() => {
//调用 FiberRoot 的render方法开始渲染
root.render(element);
})
}
- FiberRoot 数据结构一探究竟
- 这里我就不想贴一整大段代码,只把关键的属性列出来
- FiberNode 里面的数据结构先不管,我们只需要知道它是记录组件(class/fc/element)的状态和信息
- 最关键的是 current 属性,即是 RootFiber,也就是说 FiberNode.current = RootFiber;
export default class ReactRoot {
constructor(container) {
// RootFiber tag === 3
this.current = new FiberNode(3, null, null);
// 初始化rootFiber的updateQueue
initializeUpdateQueue(this.current);
// RootFiber指向FiberRoot
this.current.stateNode = this;
// 应用挂载的根DOM节点
this.containerInfo = container;
// root下已经render完毕的fiber
this.finishedWork = null;
}
}
- unbatchedUpdates,这里涉及到一个react的批量更新问题;
- 在 react 中,如果我在一个 classComponent 组件内的点击事件多次调用 this.setState
- 主动
batchedUpdates
, 会输出1,2,3 - 事件处理函数自带
batchedUpdates
,相当于使用定时器的效果,会输出0,0,0 - 这是因为,react认为,在很短的时间内触发的更新,其实是没有必要的,会自动的加上事件合成
batchedUpdates
- 当然,首次更新是非批量更新的,所以才会调用 unbatchedUpdates 方法;
handleClick = () => {
// 主动`unbatchedUpdates`
// setTimeout(() => {
// this.countNumber()
// }, 0)
// setTimeout中没有`batchedUpdates`
setTimeout(() => {
batchedUpdates(() => this.countNumber())
}, 0)
// 事件处理函数自带`batchedUpdates`,相当于上面的情况
// this.countNumber()
}
countNumber() {
const num = this.state.number
this.setState({
number: num + 1,
})
console.log(this.state.number)
this.setState({
number: num + 2,
})
console.log(this.state.number)
this.setState({
number: num + 3,
})
console.log(this.state.number)
}
- 紧接着调用 FiberRoot.render
- expirationTime 过期时间,代表着本次更新(update)的优先级;
- 这里得注意,React16.13.1 的 expirationTime 和 16.7 的过期时间是相反的,在16.7中,值越小,优先级越大;
- 在创建好更新以后,就进入了react调度阶段;
export default class ReactRoot {
constructor(container) {
// TODO...
}
render(element) {
// RootFiber
const current = this.current;
// 申请当前的创建更新时间
const currentTime = DOMRenderer.requestCurrentTimeForUpdate();
// expirationTime 过期时间,可以代表着本次更新任务的优先级;
// 不同事件触发的update会产生不同priority
// 不同priority使fiber获得不同的expirationTime
const expirationTime = DOMRenderer.computeExpirationForFiber(currentTime, current);
// 创建更新
const update = createUpdate(expirationTime);
// fiber.tag为HostRoot类型,payload为对应要渲染的ReactComponents(APP 组件)
update.payload = {element};
enqueueUpdate(current, update);
// 首次渲染会走这里,再次更新就直接创建更新对象然后开始调度
return DOMRenderer.scheduleUpdateOnFiber(current, expirationTime);
}
}
首次渲染更新流程
老样子,我们还是直接先上流程图,根据流程再来看代码和注释;在阅读react源码的时候,是相当枯燥的,我们需要一点耐心慢慢解刨;
- scheduleUpdateOnFiber
- 我们只处理异步任务,所以不需要通过expirationTime检查是否是异步
// 从当前fiber递归上去到root,再从root开始work更新
export function scheduleUpdateOnFiber(fiber, expirationTime) {
// 注意是值越大,权限越大,和16.7相反了;
// 向上冒泡更新,同时更新的过期时间(expirationTime)和子节点的过期时间 (childExpirationTime)
// 这样做的原因是让整个fiber树上更新的最高优先级冒泡到root节点,进行更新
const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
// root == FiberRoot
if (!root) {
return;
}
// 开始安排调度安排调度
ensureRootIsScheduled(root);
}
- ensureRootIsScheduled 开始安排调度
- 这个阶段相对来说是非常复杂的,但是总的来说它做了以下几件事:
- 将root加入schedule,root上每次只能存在一个scheduled的任务
- 每次创建update后都会调用这个函数,需要考虑如下情况:
- 1.root上有过期任务,需要以ImmediatePriority(同步不中断)立刻调度该任务
- 2.root上已有schedule但还未到时间执行的任务,比较新旧任务expirationTime和优先级处理
- 3.root上还没有已有schedule的任务,则开始该任务的render阶段
function ensureRootIsScheduled(root) {
// 这个变量记录过期未执行的fiber的expirationTime
const lastExpiredTime = root.lastExpiredTime;
if (lastExpiredTime !== NoWork) {
// ....TODO
}
// 寻找root(FiberRoot)本次更新的过期时间
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
const existingCallbackNode = root.callbackNode;
// 本次更新的过期时间其实是没有任务
if (expirationTime === NoWork) {
// 又存在当前正在进行的异步任务,同步执行掉
if (existingCallbackNode) {
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority = Scheduler.NoPriority;
}
return;
}
// 从当前时间和expirationTime推断任务优先级
const currentTime = requestCurrentTimeForUpdate();
const priorityLevel = inferPriorityFromExpirationTime(currentTime, expirationTime);
if (existingCallbackNode) {
// 该root上已存在schedule的root
const existingCallbackNodePriority = root.callbackPriority;
const existingCallbackExpirationTime = root.callbackExpirationTime;
if (existingCallbackExpirationTime === expirationTime && existingCallbackNodePriority >= priorityLevel) {
// 该root已经存在的任务expirationTime和新udpate产生的expirationTime一致
// 这代表他们可能是同一个事件触发产生的update
// 且已经存在的任务优先级更高,则可以取消这次update的render
return;
}
// 否则代表新udpate产生的优先级更高,取消之前的schedule,重新开始一次新的
Scheduler.cancelCallback(existingCallbackNode);
}
root.callbackExpirationTime = expirationTime;
root.callbackPriority = priorityLevel;
// 保存Scheduler保存的当前正在进行的异步任务
let callbackNode;
// 过期任何和同步任务一样,不中断,一口气更新完;
if (expirationTime === Sync) {
callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// 正常的异步任务和Concurrent首次渲染走走这里
callbackNode = Scheduler.scheduleCallback(
priorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
// 根据expirationTime,为任务计算一个timeout
// timeout会影响任务执行优先级
{timeout: expirationTimeToMs(expirationTime) - Scheduler.now()}
)
}
root.callbackNode = callbackNode;
}
- performSyncWorkOnRoot
- 这是不通过scheduler的同步任务render阶段的入口
- 注意render阶段其实就是Reconcile协调阶段, diff算法就是在这个阶段做的;
function performSyncWorkOnRoot(root) {
const lastExpiredTime = root.lastExpiredTime;
const expirationTime = lastExpiredTime !== NoWork ? lastExpiredTime : Sync;
//先暂时忽略这个函数
flushPassiveEffects();
if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
// 创建WIP树进行创建更新,如果WIP树还存在,说明需要打断这个任务
prepareFreshStack(root, expirationTime);
}
//根据WIP树进行更新
if (workInProgress) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
do {
// 进入同步的workLoop渲染大循环
workLoopSync();
break;
} while (true)
// render阶段结束,进入commit阶段,commit阶段不可中断
commitRoot(root);
// 重新安排调度, 以免又执行不到过期了的任务;
ensureRootIsScheduled(root);
}
return null;
}
- workLoopSync
- 同步模式,不需要考虑任务是否需要中断, 这也是为什么渲染阶段可以同步的原因;
function workLoopSync() {
while (workInProgress) {
workInProgress = performUnitOfWork(workInProgress);
}
}
- performUnitOfWork
- 开始执行每个单元的渲染工作,执行到WIP树为空,也就是说没有更新了;
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate;
// beginWork会返回fiber.child,不存在next意味着深度优先遍历已经遍历到某个子树的最深层叶子节点
// beginWork 为render阶段的主要工作之一,主要做了如下事:
// 根据update更新 state
// 根据update更新 props
// 根据update更新 effectTag
let next = beginWork(current, unitOfWork, renderExpirationTime);
// beginWork完成 fiber的diff,可以更新momoizedProps
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (!next) {
// completeUnitOfWork 主要做了如下事:
// 1.为 beginWork阶段生成的fiber生成对应DOM,并产生DOM树
// let next = completeWork(current, workInProgress);
// 2. 将child fiber的expirationTime冒泡到父级
// 这样在父级就能直到子孙中优先级最高到expirationTime
// resetChildExpirationTime(workInProgress);
// 3. 组装圣诞树链条 effect list
next = completeUnitOfWork(unitOfWork);
}
return next;
}
- 对着代码我们再来看个图,你就明白了;work阶段结束了,也就代表着渲染阶段已结束
- commitRoot 提交阶段
- 提交阶段相对简单,因为是同步执行的,不可中断
function commitRoot(root) {
const renderPriorityLevel = Scheduler.getCurrentPriorityLevel();
// 包裹一层commitRoot,commit使用Scheduler调度
Scheduler.runWithPriority(Scheduler.ImmediatePriority, commitRootImp.bind(null, root, renderPriorityLevel));
}
// commit阶段的入口,包括如下子阶段:
// before mutation阶段:遍历effect list,执行 DOM操作前触发的钩子
// mutation阶段:遍历effect list,执行effect
function commitRootImp(root) {
do {
// syncCallback会保存在一个内部数组中,在 flushPassiveEffects 中 同步执行完
// 由于syncCallback的callback是 performSyncWorkOnRoot,可能产生新的 passive effect
// 所以需要遍历直到rootWithPendingPassiveEffects为空
flushPassiveEffects();
} while (ReactFiberCommitWorkGlobalVariables.rootWithPendingPassiveEffects !== null)
if (!finishedWork) {
return null;
}
root.finishedWork = null;
root.finishedExpirationTime = NoWork;
// 重置Scheduler相关
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority = Scheduler.NoPriority;
// 已经在commit阶段,finishedWork对应的expirationTime对应的任务的处理已经接近尾声
// 让我们找找下一个需要处理的任务
// 在 completeUnitOfWork中有childExpirationTime的冒泡逻辑
// fiber树中高优先级的expirationTime会冒泡到顶上
// 所以 childExpirationTime 代表整棵fiber树中下一个最高优先级的任务对应的expirationTime
const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime(finishedWork);
// 更新root的firstPendingTime,这代表下一个要进行的任务的expirationTime
markRootFinishedAtTime(root, expirationTime, remainingExpirationTimeBeforeCommit);
if (root === workInProgressRoot) {
// 重置 workInProgress
workInProgressRoot = null;
workInProgress = null;
renderExpirationTime = NoWork;
}
let firstEffect;
if (root.effectTag) {
// 由于根节点的effect list不含有自身的effect,所以当根节点本身存在effect时需要将其append 入 effect list
if (finishedWork.lastEffect) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// 根节点本身没有effect
firstEffect = finishedWork.firstEffect;
}
let nextEffect;
if (firstEffect) {
// before mutation阶段
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
nextEffect = firstEffect;
do {
try {
nextEffect = commitBeforeMutationEffects(nextEffect);
} catch(e) {
console.warn('commit before error', e);
nextEffect = nextEffect.nextEffect;
}
} while(nextEffect)
// mutation阶段
nextEffect = firstEffect;
do {
try {
nextEffect = commitMutationEffects(root, nextEffect);
} catch(e) {
console.warn('commit mutaion error', e);
nextEffect = nextEffect.nextEffect;
}
} while(nextEffect)
// workInProgress tree 现在完成副作用的渲染变成current tree
// 之所以在 mutation阶段后设置是为了componentWillUnmount触发时 current 仍然指向之前那棵树
root.current = finishedWork;
if (ReactFiberCommitWorkGlobalVariables.rootDoesHavePassiveEffects) {
// 本次commit含有passiveEffect
ReactFiberCommitWorkGlobalVariables.rootDoesHavePassiveEffects = false;
ReactFiberCommitWorkGlobalVariables.rootWithPendingPassiveEffects = root;
ReactFiberCommitWorkGlobalVariables.pendingPassiveEffectsExpirationTime = expirationTime;
ReactFiberCommitWorkGlobalVariables.pendingPassiveEffectsRenderPriority = renderPriorityLevel;
} else {
// effectList已处理完,GC
nextEffect = firstEffect;
while (nextEffect) {
const nextNextEffect = nextEffect.next;
nextEffect.next = null;
nextEffect = nextNextEffect;
}
}
executionContext = prevExecutionContext;
} else {
// 无effect
root.current = finishedWork;
}
}
非首次渲染更新流程
内容未完待续
总结
待更新