react
在自己的v16
版本中将底层的reconciliation
过程进行了重大更改,之前v15
中使用同步递归算法
进行组件的diff
,我们知道,浏览器需要一定的刷新帧率(大概16ms渲染一次,FPS为60)才能确保页面不会可见
的卡顿,而react
一旦同步遍历的组件过多,将会引起页面掉帧,应用就会显得卡顿。因此,react
花了大概两年时间优化,提出了Fiber
结构,以链表
的形式实现了操作树的新算法,提供了包括非阻塞渲染,按优先级更新,预渲染等强大的新特性。
最近正在研究学习这个新的fiber
结构,今天不深入讲(毕竟自己也是刚刚开始深入学习),只单单聊一下fiber
进行节点遍历的简单实现,这个抽象方法十分的精巧,浅显易懂,感觉可以用在很多地方。
抽象链表遍历
如何使用单链表遍历一个组件树?react
内的fiber
数据结构包含了三个特殊字段:
- child 指向第一个子节点
- sibling 指向第一个兄弟节点
- return 指向父节点
如下图:
对于这样一个树形结构,每个节点都是一个node
,构造函数简写如下:
1 | class Node { |
要遍历这样一个树,同时对每个节点都进行一些操作,我们需要先想一下需要如何处理。首先,我们需要用while()
循环这个结构,对于循环到的每一个node
调用一个函数doWork()
做操作;其次,操作的同时需要把这个node
跟他的子节点进行联系;最后,我们依然(同v15
)采用深度优先搜索(DFS)
算法,在遍历完后退出while
循环。
我们先用来实现一个link
函数来链接父节点和子节点:
1 | function link(parent, elements) { |
接下来实现doWork()
函数,用来操作节点,同时调用link
函数链接节点和子节点:
1 | function doWork(node) { |
准备工作完成,接下来就重头戏,深度优先遍历:
1 | function walk(o) { |
不断遍历当前节点的第一个子节点,直到到达分支的末尾,然后遍历sibling
,然后return
回父节点。
如果我们用这种方法遍历上方的树形结构,函数调用顺序将会是:work(a1) => doWork(a1) => doWork(b1) => doWork(b2) => doWork(c1) => doWork(d1) => doWork(d2) => doWork(b3) => doWork(c2) => return
。
一撇React源码
React
把一次渲染分成了两个阶段:render
和commit
。render
阶段,如果是第一次渲染,react
为render
返回的每一个react元素
创建一个fiber
节点,随后的更新中会重用和更新这些现有的fiber
节点,生成一个部分节点标记了side effects的fiber节点树
。v16
的重点在于,这个render
过程可以是异步执行的,所以某些生命周期方法可能被重复调用(不细展开)。而commit
阶段则必须是同步的执行,react
将变化一次性更新到页面中。
在render
阶段中就用到了fiber
树的深度优先遍历。我们来简单看一下react
源码是如何做的。
首先,fiber
从workLoop()
函数进入:
1 | function workLoop(isYieldy) { |
nextUnitOfWork
从workInProgress
树中保存了对fiber
节点的引用,可以认为是当前正在处理的节点?处理完当前fiber
后,变量将包含对树中下一个fiber
节点的引用或null
。
这里有4个函数用于遍历树并启动或完成工作:
- performUnitOfWork
- beginWork
- completeUnitOfWork
- completeWork
这里有一个动画,注意观察算法的调用和分支的跳转,它首先完成child
节点的工作,然后转移到parent
身边。
先来看下performUnitOfWork
和beginWork
函数:
1 | function performUnitOfWork(workInProgress: Fiber): Fiber { |
如果有下一个子节点,它将被赋值给workLoop
函数中的nextUnitOfWork
变量,继续往下遍历;如果没有子节点,则代表这个分支的遍历到达了末尾,因此就完成了当前节点,之后,就需要找到这个完成节点的兄弟节点开始下一个分支的遍历,最后再回溯到父节点。这些在completeUnitOfWork
函数中完成:
1 | function completeUnitOfWork(workInProgress: Fiber): Fiber { |
当workInProgress
没有子节点的时候,进入此函数,走completeWork
完成当前节点的工作。然后寻找它是否有兄弟节点,如果找到,react
退出completeUnitOfWork
并返回这个兄弟节点,交给nextUnitOfWork
去进行下一个分支的工作。这里,react
只是完成了第一个子分支的工作,父节点的工作还没有完成,只有在完成所有子分支节点的工作后,才会向上去完成父节点的工作。
上面的4个函数,performUnitOfWork
和completeUnitOfWork
主要用于迭代工作,主要的活动则在beginWork
和completeWork
函数中进行(react源码为这两个方法各自分配了单个文件来写)。
这篇博客只是简单总结了一点点fiber
构建的知识,源码的学习实在太过复杂太难以理解,只能一个点一个点的去突破和理解,正如乔布斯在斯坦福大学中的演讲所说:
Again, you can’t connect the dots looking forward; you can only connect them looking backwards. So you have to trust that the dots will somehow connect in your future.
点点碎片知识的堆积,都是我们日后豁然开朗的根源。