React 使用链表遍历组件树
React 为在有限的资源情况下,更好地控制UI的更新,提出了时间分片的概念。以达到三个目标:
performing non-blocking rendering(无阻塞渲染);applying updates based on the priority(根据优先级渲染);pre-rendering content in the background(预渲染)。
React 遍历Dom Tree:
1. updates state and props(更新状态和属性)
2. calls lifecycle hooks(执行生命周期回调函数)
3. retrieves the children from the component(找到子组件)
4. compares them to the previous children (对子组件进行diff)
5. figures out the dom updates the need to be performed(找出变更,执行dom操作)
when dealing with UIs, the problem is that if too much work is executed all at once ,it can cause animations to drap frames。(当在有太多内容要更新时,会出现掉帧的现象,即卡顿,更新跟不上浏览器的刷新速度)
if react is going to walk the entire tree of components synchromously and perform work for each component ,it may run over 16ms available for an application code to execute its logic 。And this will cause frames to drop causing stuttering visual effects.
在理解react如何通过时间分片的来实现对用户而言的无阻塞渲染之前,需要了解两个浏览器的api:
requestIdleCallback会在某一帧结束后的空余时间或者用户处于不活跃的的状态时,处理我们的工作。利用这个API能使我们尽可能高效利用任何空闲的资源。
requestAnimationFrame 是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。设置这个API的目的是为了让各种网页动画(
Dom动画,Canvas动画,SVG动画,WebGL动画)能够有一个统一的刷新机制。显示器固有的刷新频率(60HZ/75HZ),requestAnimationFrame的基本思想就是与这个刷新频率保持一致。
requestIdleCallback((deadline)=>{
// 可用时间 & 是否有可用时间
console.log(deadline.timeRemaining(),deadline.didTimeOut)
})
//timieRemaining can change as soon as browser gets some work to do,so it should be constanly checked。
要知道浏览器渲染一帧后的剩余时间,除了浏览器本身,利用js基本很难准确计算得到,因为当requestAnimationFrame的回调完成后,还要进行样式的计算,布局,渲染以及浏览器内部的工作等,还要确保当前没有用户交互。
当事件很多时,你可能会担心你的回调函数永远不会被执行。requestIdleCallback有一个可选的第二参数:含有timeout属性的对象。如果设置了这个timeout的值,回调函数还没有调用的话,则浏览器必须在设置的这个毫秒数后,去强制调用对应的回调函数。如果回调函数由timeout触发,timeRemaining()会返回0,deadtime对象的didTimeout属性值true。(设置timeout会破坏潜规则)
如果在某一帧的末尾,回调函数被触发,它将被安排在当前帧被Commit之后,这表示相应的样式已经改动,同时更重要的是布局已经重新计算。如果我们在这个回调中进行样式的改动,设计到布局的计算则会被判无效。如果在下一帧中有任何读取布局相关的操作,浏览器不得不执行一次强制同步布局,这将是一个潜在的性能瓶颈。此外不要在回调函数中触发dom改动的原因是DOM改动的时间是不可预期的,正因为如此,DOM操作很容易地超过浏览器给出的限期。
最佳实践是在requestAnimationFrame的回调中去进行dom改动,因为浏览器会优化同类型的改动。这意味可以在下一次的requestAnimationFrame回调中添加一个文档片段。如果用的是虚拟DOM的库,你可在requestIdleCallback中做改动,但要在下一次requestAnimationFrame中将这些改动应用到Dom上,而不是在requestIdleCallback中。
为了使用这些API,需要把整个tree的渲染工作划分为可逐渐递增的单元。同时为了达到这个目标,React需要重新实现遍历树的算法,从之前的依赖内部调用栈的同步递归模型转向通过指针链接链表的异步模型。原因在于如果依赖内部调用栈,遍历更新的工作会一直进行直到调用栈为空。React Filter的目标在于把一次连续的树的遍历操作变为可单步执行的栈的片段,以达到无阻塞渲染和优先级渲染的目的。
(调用堆栈是一种数据结构,用来存储有关计算机程序活跃子程序的信息,调用堆栈存在的主要原因是跟踪每个活跃的子程序在完成执行时应该返回的控制位置。)
递归的算法非常直观,适合进行树的遍历操作,但是它是有局限性的,最大的一点就是无法分解工作为增量单元,这使得React不能暂停特定组件的工作并在稍后恢复。通过递归,React只能不断迭代直到它处理完成所有组件,堆栈为空。
React Fiber采用邻接链表树遍历算法来代替递归遍历。
// 结构体
class Node{
constructor(instance){
this.instance = instance;
this.child = null; // fisrt child
this.sibling = null; // first sibling
this.return = null; // parent
}
}
function link(parent,elements){
if(elements === null) elements =[];
parent.child = elements.reduceRight((previous,current)=>{
const node = new Node(current);
node.return = parent;
node.sibling = previous;
return node;
},null)
return parent.child
}
// the function iterates over the array of nodes starting from the last one and links them //together in a singly linked list. It returns the reference
// to the first sibling in the list
function doWork(node){
const children = node.instance.render()
return link(node,children)
}
//it's parent first,depth-first implementation
function walk(o){
let root = o;
let current = o;
while(true){
let child = doWork(current);
if(child){
current = child
continue;
}
if(current === root){
return ;
}
while(!current.sibling){
if(!current.return || current.return === root){
return;
}
current = current.return;
}
}
}
// Fiber is re-implementation of the stack,specialized for React components.You can think of a //single fiber as a virtual stackframe
// we can stop the traversal at any time and resume to it later.That's exactly the condition we //wanted to achieve to be able to use the new
// requestIdleCallback API
function workLoop(isYieldy){
if(!isYieldy){
while(nextUnitOfWork !== null){
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
}else{
while(nextUnitOfWork !== null && !shouldYiels()){
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
}
}
// nextUnitOfWork 变量作为顶部帧,保留对当前Fiber节点的引用。函数shouldYield返回基于//deadlineDidExpire和deadline变量的结果,这些变量在React为Fiber节点执行
// 工作时不停的更新
react更新UI,要做的所有操作都是Fiber的要做工作。work的类型依据元素的类型。在协调期间,每个从render 函数返回的react节点,会被合并到fiber节点树中。每一个react节点都有对应的fiber节点,不同的是,fiber并不会在每次重新render之后,重新创建。fiber节点其实是可变的数据结构保存了组件的状态的dom结构。每个fiber可以认为代表了不同类型的工作要做。这使得fiber可以根据优先级进行渲染。