之前写了一篇react相关的文章,但是那篇文章大都在介绍代码,对思想架构基本没什么涉及。
这篇文章一直没有想好怎么写,react内部属实有够复杂的。看了好多人对react的架构理解,这里动笔写一下,有不同意见欢迎diss.
这里首推参考的文章:
这一篇文章可能有不对的地方,后续有新的理解了也会改。
这一篇不会有很多代码,看看用图或者大白话能不能说明白它。
首先react为啥用fiber架构这就不多说了,看以前这个:https://segmentfault.com/a/11...
我们简单按react架构分下层
- Scheduler调度负责找出高优先级的任务 进行协调
- Reconciler协调构建 Fiber 数据结构,比对 Fiber 对象找出差异, 记录 Fiber 对象要进行的 DOM 操作(初始加载的时候,负责组装html片段)
- Renderer渲染 负责将发生变化的部分渲染到页面上(后续我们称它为commit)
第一段
fiber结构
说到这我们先不考虑优先级,先说正常同步优先级渲染出react树是啥样的,参考了这个知乎的图
https://www.zhihu.com/questio...
我们通过ReactDom.render创建出来的一个react应用的结构如,下图
fiberRoot是这个react应用的根,它有个current指向当前界面上渲染好的fiber树,那rootFiber就是我们的指定挂载到哪个元素上的真实DOM的fiber,例如
ReactDom.render(<App/>, document.getElementById('root'))
那rootFiber就是id为root的真实DOM的fiber,我们可以看到图上有两个rootFiber树中间有属性可以互相访问,我们看图可以看到一个是current树,另一个被称为workInProgress树。
workInProgress树:我们每次通过不同方式发起更新,就是要在内存中基于current树来构建这颗树(内存中构建时,current的是否复用就是react的diff算法了),当workInProgress树构建完成提交(commit)后,fiberRoot的current就会指向它,此时它就变成了current树。(低优先级任务构建的过程中被高优先级打断的话,会重新开始构建workInProgress树)
重点:很多文章里说过react应用中只有一个fiberRoot,这里每ReactDom.render一次就可以认为是一个react应用。(验证了好久)
这个两棵树的方式 在react中被称为双缓存技术。
如果上面文字描述看不是很明白的话,建议看下卡颂大佬的,这一篇
fiber树的update和mount的不同表现
接下来我们看react的虚拟dom是如何变成fiber的,我们知道jsx会被babel的jsx插件转换为react.createElement函数,那这个函数产生的就是我们平时说的虚拟dom的结构,它是个树形结构,我们通过协调层Reconciler首次构建时会把这个虚拟dom的结构深度优先遍历的方式来形成一个链表结构的fiber树(后续我们就可以循环该链表了),我们看一段代码看它转换之后的一个结果,如下:
export default class Index extends React.Component{
state={ number:666 }
handleClick=()=>{
this.setState({
number:this.state.number + 1
})
}
render(){
return
<div>
hello,world
<p > 《React进阶实践指南》 { this.state.number } </p>
<button onClick={ this.handleClick } >点赞</button>
</div>
}
}
我们可以看到每个节点有child和return及sibling属性,通过这些属性我们可以把他们串联成一个链表方便循环。
render协调阶段
我们知道我们上面说的形成链表时它的行为基本是两种,如下
- 递阶段:首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法,根据传入Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。
- 归阶段:在“归”阶段会调用completeWork处理Fiber节点,当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。
递和归两个阶段都对应了update和mount两种情况,每种情况的处理是不一样的,递阶段的mount挂载时会创建子fiber,update时更新的fiber节点会有effectTag标记,而归阶段mount和update两种情况如下:
update
- onClick、onChange等回调函数的注册
- 处理style prop
- 处理DANGEROUSLY_SET_INNER_HTML prop
- 处理children prop
mount
- 为Fiber节点生成对应的DOM节点
- 将子孙DOM节点插入刚生成的DOM节点中
- 与update逻辑中的updateHostComponent类似的处理props的过程
最后所有归的阶段结束后,就要挂载(commit)处理副作用了,这里讲下副作用:。
- 首先是mount的情况这会它只会在rootFiber存在Placement effectTag,,也就是说mount最后只有一个effectTag,也就是插入html片段,我们能看到上面mount归的时候会依次收集dom节点,也就是说最后就执行一个副作用effectTag,就是挂载dom
- 第二种就是update的情况了,递阶段的时候会给有更新的fiber标记effectTag,而归阶段完成后,如果当前fiber有更新,会在归的上层函数中把它放到effectList的单向链表里,最后到commit阶段只要依次执行effectList链表里的fiber更新就可以了。
“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。
如果还是不清楚的话,来这里 render阶段的递归
commit阶段
最后是进入commit,我们也找到了对应的effectList操作列表,它一共是分为三小阶段
before mutation阶段(执行DOM操作前)
- 处理DOM节点渲染/删除后的 autoFocus、blur逻辑
- 调用getSnapshotBeforeUpdate生命周期钩子
- 调度useEffect(异步调用的防止同步执行时阻塞浏览器渲染,你可想象你需要设置订阅和事件处理等情况,这会dom还没有挂载)
mutation阶段(执行DOM操作)
mutation阶段会遍历effectList,依次执行commitMutationEffects。该方法的主要工作为“根据effectTag调用不同的处理函数处理Fiber,也就是说在这一步根据fiber的副作用在这一步进行了对应的更新操作(增删改操作及对应fiber的useLayoutEffect函数,和fiber删除之后的对应useEffect销毁时的回调)。
layout阶段(执行DOM操作后)
在这一步就是调用class组件对应的生命周期函数,例如componentDidMount和componentDidUpdate,触发状态更新的this.setState如果赋值了第二个参数回调函数,最后就是current Fiber树切换。
我们知道componentWillUnmount会在mutation阶段执行。此时current Fiber树还指向前一次更新的Fiber树,在生命周期钩子内获取的DOM还是更新前的。
componentDidMount和componentDidUpdate会在layout阶段执行。此时current Fiber树已经指向更新后的Fiber树,在生命周期钩子内获取的DOM就是更新后的
到这里我们知道了,Reconciler阶段和Renderer阶段都做了什么,也就是说从Scheduler找出高优先级任务后就是到Reconciler开始对比筛选,如果高优先级打断了低优先级就会重新构建workInProgress树,如果没打断的话大概每5ms,会查看有没有空余时间,如果有的话就继续构建。
前面两个我们知道后,下面来讲我们的重点,优先级.
第二段
待续。。。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。