前言
react升级到16之后,架构发生了比较大的变化,现在不看,以后怕是看不懂了,react源码看起来也很麻烦,也有很多不理解的地方。
大体看了一下渲染过程。
react16架构的变化
react api的变化就不说了。react架构从stack变到了“fiber”。
最大的变化就是支持了任务帧,把各个任务都增加了优先级,同步和异步。比如用户输入input是优先级比较高的,它可以打断低优先级的任务。
比如再处理dom diff的时候耗时严重,fiber任务处理大概会有50ms的帧时长,超过这个时间就会先去看看有没高优任务去做。然后回来做低优先级任务。
- 优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任务。
- 还增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行。(不过用户操作默认是同步的,暂时还没开放这个特性)
- dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行而设计的。
渲染流程
代码太多,就不贴出来了,整理了一个图。这里就对图说下流程。(以下是个人理解,欢迎指正)
这篇源码分析还是挺不错的(http://zxc0328.github.io/2017/09/28/react-16-source/)
调用setState后,其实是调用了this.updater.enqueueSetState,这个函数首先从全局拿到React组件实例对应的fiber,然后拿到了fiber的优先级。最后调用了addUpdate向队列中推入需要更新的fiber,并调用scheduleUpdate触发调度器调度一次新的更新。
setState其实是把任务推到队列里,然后调用调度器处理更新
addUpdate
然后看下addUpdate,其实是把任务封装成update,然后把update推入updateQueue,
type Update = {
priorityLevel: PriorityLevel,
partialState: PartialState<any, any>,
callback: Callback | null,
isReplace: boolean,
isForced: boolean,
isTopLevelUnmount: boolean,
next: Update | null,
};
type UpdateQueue = {
first: Update | null,
last: Update | null,
hasForceUpdate: boolean,
callbackList: null | Array<Callback>,
// Dev only
isProcessing?: boolean,
};
每个react 结点都有2个fiber链表,一个叫current fiber,一个叫alternate fiber,而每个链表又对应两个updateQueue。
而currentFiber.alternate = alternateFiber; alternateFiber.alternate = currentFiber。通过alternate属性连接起来。初始化的时候,alternate fiber是current fiber 的clone。
处理diff的时候,alternate queue的diff被处理而不断pop,处理完diff,让current queue = alternate queue;这样一个处理就完成了。
如果被高优插入,current queue的存在就知道此任务还没完成,需要继续处理。
insertUpdate函数,将update按优先级插入这两个queue。
到此,一般工作做完了。另一半就是react的核心,调度算法了。
scheduleUpdate
fiber的调度是一个链表,从当前的结点,一直遍历到根结点(每个fiber都有fiber.return,它的值是它的父元素)。fiber的调度是从根结点进行的。遍历过程中会更新父结点的pendingWorkPriority(当前fiber的优先级)。标志这个结点上等待更新的事务的优先级。
当遍历到HostRoot(根结点)时,开始真正的调度工作。
会根据任务的优先级来决定是不是执行这次更新,
- 如果优先级为sychronousPriority(比如用户输入), 就执行同步更新,立马执行。
- 如果是TaskPriority,它的执行一般是在batchUpdate里面进行更新。(比如我回调里执行setSate,它的任务优先级就是TaskPriority),它会在dispatch的callback里面等callback执行完再执行更新。这样是对应setstate的batchUpdate。后面再讲这部分。如果是TaskPriority,直接return。
- 如果不是这两个优先级的话,表明这次更新是异步的,那就在浏览器空闲的时间处理它。调用的是scheduleDeferredCallback,会根据平台注入函数,比如浏览器的api是requestIdleCallback,如果浏览器不支持react还有个polyfill。
我们继续跟着同步任务处理走,同步任务调用performWork:
performWork
这里会开始任务的处理,调度完成。
performWork的作用就是“刷新”待更新队列,执行待更新的事务。
处理逻辑主循环是workLoop,也会调用一次scheduleDeferredCallback,未来处理一次更新,来完成workLoop未完成的任务。
workLoop
这里就会处理帧任务,进入函数首要处理就是看下上次处理有没有没被处理的任务,然后根据时间片处理任务。
如果任务在deadline之前没有commit,就会被标记pendingCommit,workLoop首先会处理pendingCommit。
接下来是处理逻辑:
- 如果下一个update是同步的,就调用performUnitOfWork进行reconcilation,最后调用commitAllWork把它渲染到dom。
- 解决完同步的任务,这时候看下时间片,有没有超时,如果没有,调用performUnitOfWork执行异步任务。
- 如果还有时间就commitAllWork,如果没有时间就留到下一帧提交。
react16 将任务的处理分成reconcilation和render,reconcilation包括dom diff,处理react组件的属性和钩子等。单独把render到dom抽出来,以便跨平台。
一次更新是同步还是异步是由优先级决定的,异步渲染默认是关闭的。用户代码的优先级是同步的。
performUnitOfWork
performUnitOfWork是执行的reconcilation阶段,它又拆分成beginWork和complete,beginWork做的是产出effect list,complete做的是处理react实例的生命周期和处理属性等操作。
effect list是dom diff的产出,其实是被打过tag的diff数组。render阶段会根据effect list render到dom
beginWork
beginWork就会根据不同的fiber tag做不同的处理,比如ClassComponent(React组件实例)调用updateClassComponent,hostComponent(真实dom)调用updateHostComponent。
updateClassComponent
如果为空,初始化一个react组件,调用组件的componentWillMount,如果新老props不一样,调用componentWillReceiveProps。这时候会检测shouldComponentUpdate,如果为true,就调用实例的render渲染出children,然后调用reconcileChildren对dom进行diff。
reconcileChildren其实是调用的reconcileChildrenArray。进行大名鼎鼎的dom diff操作。
updateHostComponent
真实dom直接进行reconcileChildrenArray dom diff
reconcileChildrenArray(dom diff)
React的reconcile算法采用的是层次遍历,然后在层次上面进行简化的两端比较法,因为fiber树是单链表结构,没有子节点数组这样的数据结构。也就没有可以供两端同时比较的尾部游标。所以React的这个算法是一个简化的两端比较法,只从头部开始比较。
从头部遍历。第一次遍历新数组,对上了,新老index都++,比较新老数组哪些元素是一样的,(通过updateSlot,比较key),如果是同样的就update。第一次遍历玩了,如果新数组遍历完了,那就可以把老数组中剩余的fiber删除了。
如果老数组完了新数组还没完,那就把新数组剩下的都插入。
如果这些情况都不是,就把所有老数组元素按key放map里,然后遍历新数组,插入老数组的元素,这是移动的情况。
最后再删除没有被上述情况涉及的元素
completeUnitOfWork
completeUnitOfWork是reconcile的complete阶段,主要是更新props和调用生命周期方法等等。
主要的逻辑是调用completeWork完成收尾,然后将当前子树的effect list插入到HostRoot的effect list中。
completeWork
这里也是根据不同的fiber tag进行对应的处理。
主要是HostComponent的处理,检查结点是不是需要更新,如果需要就打个tag,标记为update的side-effect。更新新老ref。
commitAllWork
这块就是render到dom的操作了。根据dom diff产生的effect list进行dom的增删改。还会调用一些生命周期,比如删除组件调用componentWillUnmount。
如果effect发生在ClassComponent上,调用组件的componentDidMount
componentDidUpdate。
在render完暴露钩子以便调用包括
componentDidMount、componentDidUpdate和componentWillUnmount
到这全部流程就结束了。
QA
如何进行批量的setstate处理?异步setstate?
比如
handleClick = (e) => {
e.stopPropagation();
this.setState({
title: 'click2'
})
this.setState({
title: 'click3'
})
this.setState({
title: 'click4'
})
}
触发 dispatchEvent 回调函数的处理过程中,会执行到 batchedUpdates。
function batchedUpdates(fn, a) {
var previousIsBatchingUpdates = isBatchingUpdates; // 批量处理
isBatchingUpdates = true;
try {
return fn(a); // // 此过程中可能改变state所以需要再performWork
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performWork(Sync, null);
}
}
}
isBatchingUpdates被置为true,这三个 setState 都不能立即执行performWork。
if (isBatchingUpdates) {
if (isUnbatchingUpdates) {
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
}
return;
}
if (expirationTime === Sync) {
performWork(Sync, null);
} else {
scheduleCallbackWithExpiration(expirationTime);
}
而会在这个callback执行之后调用performWork,这样就完成了batch 处理。
为什么settimeout可以跳过batchUpdate呢?
setTimeout 的回调函数须等 dispatchEvent 函数执行完,也就是要等到 performWork 执行,然而在 performWork 中, nextFlushedRoot 为 null , while 循环无法进行。
function performWork () {
while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && (minExpirationTime === NoWork || nextFlushedExpirationTime <= minExpirationTime) && !deadlineDidExpire) {
performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
// Find the next highest priority work.
findHighestPriorityRoot();
}
}
执行 setTimeout 的回调函数时, isBatchingUpdates 已经变为 false,所以每次 setState 都会触发 performWork 。