- Notifications
You must be signed in to change notification settings - Fork1
License
yuanxiang1990/emini
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
- setSatate支持(done)。
- class类型组件支持(done)。
- 组件生命周期支持(done)。
- fiber任务调度机制(初步实现)。
- react事件支持(初步实现)。
- 当当前组件无更新时需要跳过当前组件。(TODO 中)
- differ算法优化(解决每次differ相同key对象会重复创建的问题)(done)
- reconcile打算优化(目前存在bug,打断后会直接进入commit)(TODO 中)
- function component组件和Hook支持(TODO 低)
- 目前仅实现了rootQueue,针对单fiber的updateQueue没有做优先级处理,调度任务的粒度应该是针对每一次setState!!!(done)
- differ性能(去除链表转数组操作)(done)
react的大体流程主要分为render阶段和commit阶段。render阶段主要differ出虚拟dom的变更,即收集各节点的effect。commit阶段主要将各节点的effcet应用到真实dom上。在react16之前,render阶段采用递归的方式实现,当dom节点较为复杂时,render的执行时间会非常长。由于js代码执行和浏览器dom操作都是执行在主线程上,render阶段可能会持续占用主线程,造成用户的很多操作的得不到马上响应,十分影响用户体验。所以react16提出了fiber架构来解决这个问题。
fiber架构主要对render阶段进行拆分,commit阶段不需要拆分,一方面commit阶段不会需要非常久的执行时间,再加上对commit阶段进行拆分会导致dom操作执行一不分被终止,对用户体验的影响非常大。
首先需要改变以前的递归的differ方式,如果继续采用递归这种形式的话,是没法进行拆分的。react主要是讲之前的递归改成了循环的形式,因此,react引入了一种新的数据结构fiber。fiber的数据结构如下:
export class FiberNode { constructor(tag, type, pendingProps, key) { // Instance this.tag = tag;//节点类型 this.key = key; this.stateNode = null;//对应真实dom或者class类型组件实例 this.type = type;//对应的是以前reaat element的type属性 // Fiber this.return = null;//父节点 this.child = null;//子节点 this.sibling = null;//兄弟节点 this.index = 0; this.props = pendingProps; this.memoizedState = null; // Effects // this.effectTag = NoEffect; this.effects = [];//子节点变更 this.effectTag = null;//当前节点变更 this.expirationTime = NoWork;//节点到期时间,根据这个属性判断任务优先级 this.alternate = null;//对应变更下的fiber节点 this.updateQueue = [];//更新队列 }}
注意:改fiber结构做了很多简化,删除了一些不需要的属性。这个alternate属性需要特别说一下,任务开始时会创建一颗workInProgress Tree。该对象是从alternate属性上创建的,workInProgress对象的alternate属性又指向当前的tree,当前的tree的alertnate对象又指向workInProgress。(貌似非常绕)后续的操作都是针对workInProgress操作,完成之后workInProgress就变成了新的fiber tree。下次执行时不会继创建workInProgress对象,而是把原对象的alternate属性赋值给workInProgress作为新的对象,此时的workInProgress的alternate就变成了原fibertree,然后把当前的child再赋值给新的workInprogre对象。后续的differ都是对当前对象的child和alternate对象的chidl做对比,如果相同则把alternate对象的child拷贝到新创建的fiber child的alertnate属性下。
上面说了一大推估计基本都看晕了。总结一下就是当前tree和alternate是相互引用关系,新的任务到来时,当前对象需要挂载到alternate下,则只需把新的workInProgress指向当前对象的alternate即可。而新的child都需要从最新的element下创建。这种相互持有引用的技术,react称之为双缓冲技术。
代码如下:
export function createWorkInProgress(current) { let workInProgress = current.alternate; if (workInProgress === null) { workInProgress = new FiberNode(current.tag); workInProgress.alternate = current; workInProgress.stateNode = current.stateNode; workInProgress.props = current.props || {}; workInProgress.expirationTime = current.expirationTime; current.alternate = workInProgress; } else { workInProgress.effects = []; workInProgress.child = current.child; workInProgress.props = current.props; } workInProgress.expirationTime = current.expirationTime; workInProgress.updateQueue = current.updateQueue; return workInProgress;}
下面我们来看以下html结构
<div> <div> <div>aaa</div> <div>bbb</div> </div> <div> <div>ccc</div> <div>ddd</div> </div><div>
这些真实dom会转化成虚拟dom。react16之前的节点是按层级遍历对比的,先对比A节点,再对比B、C节点,在分别对比F、G和D、E将节点。react16之后会先遍历左子树,知道没有子节点后再回溯遍历对比父节点的兄弟节点的子树,最后一次遍历玩整棵树。在fiber架构中,遍历顺序会是A->B->C->D->E->aaa文本节点->bbb文本节点->F->G->ccc文本节点->ddd文本节点。该结构是一个链表结构,可随时中断,之后再恢复。
任务执行分片主要是采用浏览器的requestIdleCallback这个api。requestIdleCallback可以在浏览器一帧的空闲时间执行。requestIdleCallback具体可参考http://www.zhangyunling.com/702.html/
在react中每个任务都会对应一次root,每个root会对应一个到期时间。在react中时间是反过来的,先出初始化一个非常大的时间然后依次减少。时间越大,任务优先级越高。(注意:在原生react中当当前时间减去expirationTime差值为0时会自动获得高优先级执行该任务,防止低优先级任务一直被插队)计算到期时间的方法如下
function computeExpirationForFiber(currentTime) { let expirationTime; if (isWorking) { if (isCommitting) {//commit阶段不能被打断 expirationTime = Sync;//同步任务优先级最高 } else { expirationTime = nextRenderExpirationTime } } else { if (isBatchingInteractiveUpdates) {//优先级较高的任务如用户交互等 expirationTime = computeInteractiveExpiration(currentTime);//计算出的值较大,优先级高 } else {//普通异步任务 expirationTime = computeAsyncExpiration(currentTime);//计算的值较小,优先级低 } } return expirationTime}
React.render主要在初始化的时候执行,setState则是触发虚拟dom的更新。react.render核心代码如下
const ReactDom = { render(element, container, callback) { const root = createHostRootFiber(); root.stateNode = container; root.alternate = null; updateContainer(element, root); }};
该方法主要是创建root节点后执行updateContainer方法。
export function updateContainer(children, containerFiberRoot) { let root = containerFiberRoot; let currentTime = requestCurrentTime(); let expirationTime = computeExpirationForFiber(currentTime); root.expirationTime = expirationTime; root.updateQueue.push({ element: children }) return updateContainerAtExpirationTime(root, expirationTime)}function updateContainerAtExpirationTime(currentFiber, expirationTime) { currentFiber.expirationTime = expirationTime; scheduleWork(currentFiber, expirationTime)}
updateContainer方法主要是计算expirationTime并将react element放入更新队列中,最后执行scheduleWork开始进入render阶段。
setState方法主要是调用内部的enqueueSetState方法。
const classComponentUpdater = { enqueueSetState: function (inst, payload) { const fiber = inst._reactInternalFiber; const currentTime = requestCurrentTime(); const expirationTime = computeExpirationForFiber(currentTime); if (expirationTime > nextRenderExpirationTime) {//更高优先级任务到来时终止当前任务 nextUnitOfWork = null; nextRenderExpirationTime = NoWork; } fiber.updateQueue.push({ payload }) const root = getRootFiber(fiber); root.expirationTime = expirationTime; scheduleWork(root, expirationTime); }}
该方法和updateContainer方法类似,同样是拿到执行的root节点后计算expirationTime最后执行scheduleWork方法。注意,当前任务的优先级大于正在执行任务的优先级时,会中断当前任务,执行高优先级任务。
function scheduleWork(root, expirationTime) { addRootToSchedule(root, expirationTime); if (isBatchingUpdates) { return; } requestWork(root, expirationTime);}function requestWork(root, expirationTime) { if (isRendering) { //当前正在渲染时先不执行,最后一次再一起执行 return } if (expirationTime === Sync) { performSyncWork(root); } else { performAsyncWork(root, expirationTime); }}
scheduleWork方法首先会把root节点放入队列,其次需注意isBatchingUpdates和isRendering这两个参数。isBatchingUpdates参数主要是在事件回调方法执行之前会设置为true,主要是处理事件回调里面包含多个setState方法的情况,会把多个setState合成更新。isRendering参数代表当前正在渲染时先不执行,最后一次再一起执行。
export function performSyncWork() { performWork(null)}function performAsyncWork(root, expirationTime) { recomputeCurrentRendererTime(); requestIdleCallback((deadline) => { return performWork(deadline) })}
两者都会执行performWork方法,异步方法会调用requestIdleCallback执行并传入deadline参数
function performWork(deadline, root) { findHighestPriorityRoot(); while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork) { performWorkOnRoot(deadline, nextFlushedRoot); findHighestPriorityRoot(); }}
每次循环都会调用findHighestPriorityRoot方法会找出优先级最高的root出来执行。
function performWorkOnRoot(deadline, root) { isWorking = true; isRendering = true; if (nextUnitOfWork == null) { workInProgress = createWorkInProgress(root); nextUnitOfWork = workInProgress; nextRenderExpirationTime = workInProgress.expirationTime; } try { workLoop(deadline); } catch (e) { nextUnitOfWork = throwException(nextUnitOfWork, e); console.error(e); } recomputeCurrentRendererTime(); let expirationTime = workInProgress.expirationTime; //继续处理回调 if (nextUnitOfWork) { if (currentRendererTime > expirationTime && deadline.didTimeout) {//帧超时退出 requestIdleCallback((deadline) => { performWorkOnRoot(deadline, nextUnitOfWork); }) } else {//错误异常退出等。。。 performWorkOnRoot(deadline, nextUnitOfWork); } } /** * 任务已处理完,直接进入commit阶段 */ else { isCommitting = true; commitAllWork(pendingCommit); isCommitting = false; isRendering = false; isWorking = false; pendingCommit = null; nextRenderExpirationTime = NoWork; root.expirationTime = NoWork; rootQueue.splice(rootQueue.indexOf(root), 1); //workInProgress = null; }}
该方法首先会调用createWorkInProgress方法构建一颗workInProgress树,workLoop方法则是循环处理每一个fiber节点。workLoop方法执行之后,如果fiber节点已经处理完,则会进入commit阶段。最后workInProgress树会变成当前树。
function workLoop(deadline) { if (deadline) { while (nextUnitOfWork && !deadline.didTimeout) { nextUnitOfWork = performUnitWork(nextUnitOfWork); } } else { while (nextUnitOfWork) { nextUnitOfWork = performUnitWork(nextUnitOfWork); } }}function performUnitWork(nextUnitOfWork) { const currentFiber = nextUnitOfWork; const nextChild = beginWork(currentFiber); finalizeInitialFiber(currentFiber, getRootFiber(currentFiber)) if (nextChild) return nextChild; let topFiber = currentFiber; while (topFiber) { completeWork(topFiber); if (topFiber.sibling) { return topFiber.sibling } else { topFiber = topFiber.return; } } return null;}function beginWork(workInProgress) { workInProgress.effects.length = 0; switch (workInProgress.tag) { case tag.ClassComponent: {//处理class类型组件 let update = {}; workInProgress.updateQueue.forEach(item => { update = Object.assign(update, item.payload); }) console.log(update, 'up') if (!isEmptyObject(update)) { workInProgress.stateNode._partialState = update; workInProgress.effectTag = Effect.UPDATE; } workInProgress.stateNode.updater = classComponentUpdater; return updateClassComponent(workInProgress); } case tag.HostRoot: { const update = workInProgress.updateQueue.shift(); if (update) { workInProgress.props.children = update.element; } return updateHostComponent(workInProgress); } default: { return updateHostComponent(workInProgress); } }}
workLoop会分同步和异步两种方式调用performUnitWork方法。performUnitWork方法则是单个节点的处理过程。beginWork方法相当于是节点分分类,根据节点的类型调用不同的方法differ节点。处理完之后completeWork依次收集节点变更到root节点。到此render阶段的任务执行完成。
/** * 进入commit阶段 */export function commitAllWork(topFiber) { console.log(topFiber.effects.slice(0)) commitPreLifeCycle(topFiber); topFiber.effects.forEach(fiber => { let domParent = fiber.return; while (domParent.tag === tag.ClassComponent) {//class类型组件 domParent = domParent.return; } if (fiber.effectTag === Effect.PLACEMENT) { commitPlacement(fiber, domParent); } if (fiber.effectTag === Effect.DELETION) { commitDeletion(fiber, domParent); } fiber.effectTag = null; }) commitAfterLifeCycle(topFiber) topFiber.effects = [];}
commit阶段比较简单,主要就是将收集到的变更应用到真实dom当中。