1

之前写了一篇react相关的文章,但是那篇文章大都在介绍代码,对思想架构基本没什么涉及。

这篇文章一直没有想好怎么写,react内部属实有够复杂的。看了好多人对react的架构理解,这里动笔写一下,有不同意见欢迎diss.

这里首推参考的文章:

卡颂大佬的react源码解析

DavidWong的一眼看穿react4步曲

React 为什么使用 Lane 技术方案

这一篇文章可能有不对的地方,后续有新的理解了也会改。
这一篇不会有很多代码,看看用图或者大白话能不能说明白它。

首先react为啥用fiber架构这就不多说了,看以前这个:https://segmentfault.com/a/11...

我们简单按react架构分下层

  1. Scheduler调度负责找出高优先级的任务 进行协调
  2. Reconciler协调构建 Fiber 数据结构,比对 Fiber 对象找出差异, 记录 Fiber 对象要进行的 DOM 操作(初始加载的时候,负责组装html片段)
  3. Renderer渲染 负责将发生变化的部分渲染到页面上(后续我们称它为commit)

第一段

fiber结构

说到这我们先不考虑优先级,先说正常同步优先级渲染出react树是啥样的,参考了这个知乎的图
https://www.zhihu.com/questio...

我们通过ReactDom.render创建出来的一个react应用的结构如,下图

image.png

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协调阶段

我们知道我们上面说的形成链表时它的行为基本是两种,如下

  1. 递阶段:首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法,根据传入Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。
  2. 归阶段:在“归”阶段会调用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)处理副作用了,这里讲下副作用:。

  1. 首先是mount的情况这会它只会在rootFiber存在Placement effectTag,,也就是说mount最后只有一个effectTag,也就是插入html片段,我们能看到上面mount归的时候会依次收集dom节点,也就是说最后就执行一个副作用effectTag,就是挂载dom
  2. 第二种就是update的情况了,递阶段的时候会给有更新的fiber标记effectTag,而归阶段完成后,如果当前fiber有更新,会在归的上层函数中把它放到effectList的单向链表里,最后到commit阶段只要依次执行effectList链表里的fiber更新就可以了。

“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。
如果还是不清楚的话,来这里 render阶段的递归

commit阶段

最后是进入commit,我们也找到了对应的effectList操作列表,它一共是分为三小阶段

before mutation阶段(执行DOM操作前)

  1. 处理DOM节点渲染/删除后的 autoFocus、blur逻辑
  2. 调用getSnapshotBeforeUpdate生命周期钩子
  3. 调度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,会查看有没有空余时间,如果有的话就继续构建。

前面两个我们知道后,下面来讲我们的重点,优先级.

第二段

待续。。。


Charon
57 声望16 粉丝

世界核平