这是个人剖析 React 源码的第六篇文章。这篇文章链接上篇,将会带着你们学习组件更新过程相关的内容,而且尽量的脱离源码来了解原理,下降你们的学习难度。html
文章分为三部分,在这部分的文章中你能够学习到以下内容:react
三篇文章并无强相关性,固然仍是推荐阅读下 前一篇文章。git
组件更新归结到底仍是 DOM 的更新。对于 React 来讲,这部分的内容会分为两个阶段:github
这一小节的内容将会集中在调和阶段,提交阶段这部分的内容将会在下一篇文章中写到。另外你们所熟知的虚拟 DOM 的 diff 算法在新版本中其实已经彻底被重写了。算法
这一小节的内容会有点难度,若是你以为难以读懂个人文章或者是别的问题,欢迎在下方评论区与我互动! 数据结构
有个例子能更好地帮助理解,咱们就经过如下组件的更新来了解整个调和的过程。函数
class Test extends React.Component {
state = {
data: [{ key: 1, value: 1 }, { key: 2, value: 2 }]
};
componentDidMount() {
setTimeout(() => {
const data = [{ key: 0, value: 0 }, { key: 2, value: 2 }]
this.setState({
data
})
}, 3000);
}
render() {
const { data } = this.state;
return (
<> { data.map(item => <p key={item.key}>{item.value}</p>) } </> ) } } 复制代码
在前一篇文章中咱们了解到了整个更新过程(不包括渲染)就是在反复寻找工做单元并运行它们,那么具体体现到代码中是怎么样的呢?学习
while (nextUnitOfWork !== null && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
复制代码
上述代码的 while
循环只有当找不到工做单元或者应该打断的时候才会终止。找不到工做单元的状况只有当循环完全部工做单元才会触发,打断的状况是调度器触发的。动画
当更新任务开始时,root
永远是第一个工做单元,不管以前有没有被打断过工做。this
循环寻找工做单元的这个流程其实很简单,就是自顶向下再向上的一个循环。这个循环的规则以下:
root
永远是第一个工做单元,无论以前有没有被打断过任务如下动画是例子代码的工做循环过程的一个示例:
了解了工做循环流程之后,咱们就来深刻学习一下工做单元是如何工做的。为了精简流程,咱们就直接认为当前的工做单元为 Test
组件实例。
在工做单元工做的这一阶段中实际上是分为不少分支的,由于涉及到不一样类型组件及 DOM 的处理。Test
是 class
组件,另外这也是最经常使用的组件类型,所以接下来的内容会着重介绍 class
组件的调和过程。
class
组件的调和过程大体分为两个部分:
class
组件生命周期函数最早被处理的生命周期函数是 componentWillReceiveProps
。
可是触发这个函数的条件有两个:
props
先后有差异getDerivedStateFromProps
或者 getSnapshotBeforeUpdate
这两个新的生命周期函数。使用其一则 componentWillReceiveProps
不会被触发知足以上条件该函数就会被调用。所以该函数在 React 16 中已经不被建议使用。由于调和阶段是有可能会打断的,所以该函数会重复调用。
凡是在调和阶段被调用的函数基本是不被建议使用的。
接下来须要处理 getDerivedStateFromProps
函数来获取最新的 state
。
而后就是判断是否须要更新组件了,这一块的判断逻辑分为两块:
shouldComponentUpdate
函数,存在就调用PureComponent
。若是是的话,就浅比较先后的 props
及 state
得出结果若是得出结论须要更新组件的话,那么就会先调用 componentWillUpdate
函数,而后处理 componentDidUpdate
及 getSnapshotBeforeUpdate
函数。
这里须要注意的是:调和阶段并不会调用以上两个函数,而是打上 tag 以便未来使用位运算知晓是否须要使用它们。effectTag
这个属性在整个更新的流程中都是相当重要的一员,凡是涉及到函数的延迟调用、devTool 的处理、DOM 的更新均可能会使用到它。
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
}
if (typeof instance.getSnapshotBeforeUpdate === 'function') {
workInProgress.effectTag |= Snapshot;
}
复制代码
处理完生命周期后,就会调用 render
函数获取新的 child
,用于在以后与老的 child
进行对比。
在继续学习以前咱们先来熟悉三个对象,由于它们在后续的内容中会反复出现:
child
。若是你还记得 fiber 的数据结构的话,应该知道每一个 fiber 都有一个 sibling
属性指向它的兄弟节点。所以知道第一个子节点就能知道全部的同级节点。render
出来的内容。首先咱们会判断 newChild
的类型,知道类型就能够进行相应的 diff 策略了。它可能会是一个 Fragment 类型,也多是 object
、number
或者 string
类型。这几个类型都会有相应的处理,但这不是咱们的重点,而且它们的处理也至关简单。
咱们的重点会放在可迭代类型上,也就是 Array
或者 Iterator
类型。这二者的核心逻辑是一致的,所以咱们就只讲对 Array
类型的处理了。
如下内容是对于 diff 算法的详解,虽然有三次 for
循环,可是本质上只是遍历了一次整个 newChild
。
第一轮遍历的核心逻辑是复用和当前节点索引一致的老节点,一旦出现不能复用的状况就跳出遍历。
那么如何复用以前的节点呢?规则以下:
如下是我简化后的第一轮遍历代码:
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 找到下一个老的子节点
nextOldFiber = oldFiber.sibling;
// 经过 oldFiber 和 newChildren[newIdx] 判断是否能够复用
// 并给复用出来的节点的 return 属性赋值 returnFiber
const newFiber = reuse(
returnFiber,
oldFiber,
newChildren[newIdx]
);
// 不能复用,跳出
if (newFiber === null) {
break;
}
}
复制代码
那么回到上文中的例子中,咱们老的第一个节点的 key 为 1,新的节点的 key 为 0。key 不相同不能复用,所以直接跳出循环,此时 newIdx
仍 为 0。
当第一轮遍历结束后,会出现两种状况:
newChild
已经遍历完当出现 newChild
已经遍历完的状况时只须要把全部剩余的老节点都删除便可。删除的逻辑也就是设置 effectTag
为 Deletion
,另外还有几个 fiber 节点属性须要说起下。
当出现须要在渲染阶段进行处理的节点时,会把这些节点放入父节点的 effect
链表中,好比须要被删除的节点就会把加入进链表。这个链表的做用是能够帮助咱们在渲染阶段迅速找到须要更新的节点。
当出现老的节点已经遍历完了的状况时,就会开始第二轮遍历。这轮遍历的逻辑很简单,只须要把剩余新的节点所有建立完毕便可。
这轮遍历在咱们的例子中是不会执行的,由于咱们以上两种状况都不符合。
第三轮遍历的核心逻辑是找出能够复用的老节点并移动位置,不能复用的话就只能建立一个新的了。
那么问题又再次回到了如何复用节点并移动位置上。首先咱们会把全部剩余的老节点都丢到一个 map
中。
咱们例子中的代码剩余的老节点为:
<p key={1}>1</p>
<p key={2}>2</p>
复制代码
那么这个 map
的结构就会是这样的:
// 节点的 key 做为 map 的 key
// 若是节点不存在 key,那么 index 为 key
const map = {
1: {},
2: {}
}
复制代码
在遍历的过程当中会寻找新的节点的 key 是否存在于这个 map
中,存在便可复用,不存在就只能建立一个新的了。其实这部分的复用及建立的逻辑和第一轮中的是如出一辙的,因此也就再也不赘述了。
那么若是复用成功,就应该把复用的 key
从 map
中删掉,而且给复用的节点移动位置。这里的移动依旧不涉及 DOM 操做,而是给 effectTag
赋值为 Placement
。
此轮遍历结束后,就把还存在于 map
中的全部老节点删除。
以上就是 diff 子节点的所有逻辑,对比 React 15 的 diff 策略而言我的认为代码好懂了许多。
阅读源码是一个很枯燥的过程,可是收益也是巨大的。若是你在阅读的过程当中有任何的问题,都欢迎你在评论区与我交流。
另外写这系列是个很耗时的工程,须要维护代码注释,还得把文章写得尽可能让读者看懂,最后还得配上画图,若是你以为文章看着还行,就请不要吝啬你的点赞。
最后,以为内容有帮助能够加群一同交流与学习。