❝前言 记录一下自己学习react fiber架构的一个过程
❞
由于js「单线程阻塞式」的特性, 导致浏览器在同一时间, 只能干一件事。 所以对现在的前端框架而言,快速完成计算并且响应用户的操作成为了各大框架需要的解决的首要问题。
直到.....react16采用了全新的fiber架构。
通过本篇文章, 希望达到以下目标
❝何为fiber架构
❞
fiber是一种数据结构, 也可以说是一个任务单元。
React16的fiber架构就是「基于fiber这种数据结构」实现的一种时间切片式的可拆分,可中断的调和器架构(Fiber Reconciler)。
它把之前作为一个整体的dom diff和渲染过程拆分成一个细小的fiber单元,
使整个渲染过程可以被中断, 让位给高优先级的任务, 等到浏览器空闲时间再回复渲染。
发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
快速响应用户操作, 让用户觉得够快, 不阻塞用户操作
那么, fiber到底是一种什么样的数据结构呢
{ tag: string, // 当前节点的名称 type: string, // 当前节点的类型, 标志着当前节点是一种类组件,或者函数组件, 或者是一种原生dom标签等 props: Object, stateNode: Object, // 当前节点的实例/真实dom return: Fiber, // 当前fiber节点的父节点 alternate: Fiber, // 上一次更新的Fiber树 child: Fiber, // 指向第一个子节点 siblings: Fiber, // 指向兄弟节点 effectTag: UPDATE, // 副作用标示 render阶段我们会收集副作用, 增加删除 更新 nextEffect: null, // effect list顺序和完成的顺序是一样的, 但是节点只放改变的fiber, 没有副作用 的节点会绕过去 } 复制代码
在react中, 每个节点都是这样的一个fiber结构单元, 基于这种结构单元, react将整个页面组装成了一个链表结构。 即每一个fiber节点单元中, 都会有一个指针,指向下一个要跟新的单元。 如图所示
流程解析:
接下来通过代码分析这个过程
用户调用「ReactDOM.render」方法
ReactDOM.render(<Component name="越祈" />, document.getElementById('root')) 复制代码
浏览器调用「requestIdleCallback」方法,开启调度
// 循环执行工作 function workLoop(deadline) { let shouldYield = false; // 是否让出时间片或者说控制权 while(nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) shouldYield = deadline.timeRemaining() < 1; // 没有时间了, 让出控制权 } if(!nextUnitOfWork && workInProgressRoot) { console.log('render阶段结束') commitRoot() } // 告诉浏览器, 现在有任务, 在空闲的时候执行当前的任务 // 任务优先级: expirationTime requestIdleCallback(workLoop, { timeout: 500 }); } 复制代码
这里浏览器会以一帧作为参考维度, 优先执行高优任务。
每一帧执行一个任务单元, 执行结束后的剩余时间交给用户。
ReactDom.render方法将挂载节点转化为fiber节点, 同时开始「reconciler」阶段
let rootFiber = { tag: TAG_ROOT, // 每个fiber都会有一个fiber,标志这此元素的类型 stateNode: container, // 一般情况下如果这个元素是一个原生节点的话, stateNode指向真是DOM元素 props: { // 这个fiber的属性对象, children里面放的是要渲染的元素 children: [element] // prop.children是一个数组, // 里面放的是react元素, 虚拟dom。 后面会根据每个react元素创建对应的fiber } } // 开始reconciler(调和阶段) scheduleRoot(rootFiber) 复制代码
react主要有两个阶段
「reconciler阶段」
这里引入三个概念
如果是初次渲染的话, 从根节点(rootFiber)开始调度
export function scheduleRoot(rootFiber) { workInProgressRoot = rootFiber nextUnitOfWork = workInProgressRoot; } 复制代码
处理当前的fiber节点
根据节点执行执行不同的操作:
/** * 开始解析 * completeUnitOfWork * 1. 创建真实DOM元素 * 2. 创建子fiber */ function beginWork(currentFiber) { if(currentFiber.tag === TAG_ROOT) { // 将根节点的孩子 转化成fiber单元 updateHostRoot(currentFiber) // 如果是文本fiber }else if(currentFiber.tag === TAG_TEXT) { updateHostText(currentFiber) } // 如果是原生节点 else if(currentFiber.tag === TAG_HOST) { updateHostTag(currentFiber) } // 如果是类式组件 else if(currentFiber.tag === TAG_CLASS) { updateClassComponent(currentFiber) }else if(currentFiber.tag === TAG_FUNCTION_COMPONENT) { updateFunctionComponent(currentFiber) } } 复制代码
处理孩子「reconcileChildren」
遍历当前节点所有孩子, 生成对应的fiber节点,
同时建立关联关系, 形成一个链表(firstEffect和lastEffect)
function reconcileChildren(currentFiber, newChildren) { let newChildIndex = 0; // 新子节点的索引 // 如果当前的fiber有alternate属性, 拿到它的第一个孩子 let oldFiber = currentFiber.alternate && currentFiber.alternate.child; let prevSibling; // 上一个新的子fiber if(oldFiber) { oldFiber.firstEffect = oldFiber.lastEffect = oldFiber.nextEffect = null; } // begin的时候创建fiber, 在completeUnitOfWork的时候收集effect while(newChildIndex < newChildren.length || oldFiber) { let newChild = newChildren[newChildIndex]; let newFiber; let tag; // .... newFiber = { tag, type: newChild.type, props: newChild.props, stateNode: null, updateQueue: new UpdateQueue(), return: currentFiber, effectTag: PLACEMENT, // 副作用标示 render阶段我们会收集副作用, 增加删除 更新 nextEffect: null, // effect list顺序和完成的顺序是一样的, 但是节点只放改变的fiber, 没有副作用 的节点会绕过去 } if(newFiber) { if(newChildIndex === 0) { // children currentFiber.child = newFiber; }else { // 上一个节点的兄弟节点指向当前的feber prevSibling.sibling = newFiber } prevSibling = newFiber } newChildIndex++; } 复制代码
当前fiber节点解析完成。 执行下一步的操作
// 在完成的时候要收集有副作用的fiber, 然后组成effect list // 每个fiber有两个属性, firstEffect指向第一个有副作用的子fiber, lastEffect指向最后一个有副作用的子节点 // 中间的用nextEffect做成一个单链表 function completeUnitOfWork(currentFiber) { let returnFiber = currentFiber.return if(returnFiber) { if(!returnFiber.firstEffect) { returnFiber.firstEffect = currentFiber.firstEffect; } if(!!currentFiber.lastEffect) { if(returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = currentFiber.firstEffect } returnFiber.lastEffect = currentFiber.lastEffect } // 如果有副作用 const effectTag = currentFiber.effectTag if(effectTag) { if(!!returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = currentFiber }else { returnFiber.firstEffect = currentFiber; } returnFiber.lastEffect = currentFiber; } } } ``` 复制代码
commit阶段, 处理 副作用。
将之前手机到的effect链表依次更新
// 提交effect链 function commitRoot() { // 执行effect之前, 先把该删的删掉 deletions.forEach(commitWork) let currentFiber = workInProgressRoot.firstEffect; while(currentFiber) { commitWork(currentFiber); currentFiber = currentFiber.nextEffect } deletions.length = 0; // 当前渲染成功的根fiber, 赋给currentRoot currentRoot = workInProgressRoot; workInProgressRoot = null; } 复制代码
依次处理新增, 删除, 更新节点等操作
function commitWork(currentFiber) { if(!currentFiber) { return } let returnFiber = currentFiber.return; while(returnFiber.tag !== TAG_HOST && returnFiber.tag !== TAG_ROOT && returnFiber.tag !== TAG_TEXT ) { returnFiber = returnFiber.return } let returnDOM = returnFiber.stateNode; // 新增节点 if(currentFiber.effectTag === PLACEMENT) { let nextFiber = currentFiber; if(nextFiber.tag === TAG_CLASS) { return } // 如果要挂载的节点不是dom节点, 那么去找它的孩子, 一直找, 知道找到真实dom节点为止 while(nextFiber.tag !== TAG_HOST && nextFiber.tag !== TAG_TEXT) { nextFiber = currentFiber.child } returnDOM.appendChild(nextFiber.stateNode) // 删除节点 }else if(currentFiber.effectTag === DELETE) { // returnDOM.removeChild(currentFiber.stateNode); return commitDeletion(currentFiber, returnDOM) // 更新节点 }else if(currentFiber.effectTag === UPDATE) { if(currentFiber.type === ELEMENT_TEXT) { // fiber.alternate指向老的节点 if(currentFiber.alternate.props.text !== currentFiber.props.text) { currentFiber.stateNode.textContent = currentFiber.props.text } }else { updateDOM(currentFiber.stateNode, currentFiber.alternate.props, currentFiber.props) } } currentFiber.effectTag = null } 复制代码
第一次更新, 建立currentRoot(渲染成功之后的根节点)
第二次更新, 建立alternate树, 复用上一次更新创建的节点
第三次更新, workInProgress树指向上一次更新的alternate树, 即第一次更新创建的树
本文使用 mdnice 排版
持续更新中~