react源码阅读:整体认识

912 阅读5分钟

点击这里进入react原理专栏

react源码有一段时间了,写篇文章总结一下,也希望能帮到大家。这是react原理系列的第一篇,主要讲解一下react的基本架构,让大家有一个整体的认识。

  1. 什么是fiber,为什么要使用fiber
  2. react的三层架构模型:scheduler - reconciler - renderer
  3. react合成事件,当我们点击一个按钮触发click事件时发生了什么
  4. react是如何触发更新的(类组件的setState,函数组件的useState)
  5. react更新的render阶段
  6. react更新的commit阶段
  7. scheduler调度任务的流程

系列文章的react版本都为17.0.2

什么是fiber

为了降低react源码新手的困惑,我们直接从数据结构上来理解什么是fiber,每个fiber就是一个对象,每个组件(比如App组件),每个真实的dom节点都会对应一个fiber对象,fiber对象有很多属性,这里先介绍如下几个:

Fiber: {
  return: Fiber | null,
  child: Fiber | null,
  sibling: Fiber | null,
  stateNode: null
}

return属性指向父级fiberchild属性指向第一个子fibersibling执行自己的下一个兄弟fiber节点,stateNode执行这个fiber对象对应的组件或者真实的dom节点。

fiber.jpg

为什么要用fiber

在采用fiber架构之前,react采用递归的方式处理虚拟dom,导致react占用主线程的时间过长,可能造成页面假死。从前面的图可以看出,fiber架构下的应用fiber树是一个类似链表结构的多叉树,每个fiber是一个独立的工作单元,这就为可中断的更新提供了便利。考虑以下两个情况:

  1. react更新过程中,用户点击了按钮
  2. 页面动画

针对情况1,react希望能够在触发点击事件时,用户能够尽快得到响应,页面动画不会卡顿。在采用fiber架构时,每个fiber都是一个独立的单元,react能够在事件触发时中断fiber的更新,转而处理用户点击事件,处理完毕后继续进行fiber的更新。

针对情况2,react会以fiber为基本单位进行更新,并为每个fiber的处理分配一个时间片,每次处理完一个fiber后,会检查时间片是否到期,如果时间片到期,react就会将线程让给浏览器,让浏览器执行相应的页面更新。

要想实现上面提到的两种效果,react使用了fiber架构,并引入了scheduler模块和优先级的概念。下面介绍一下scheduler

上面提到的两种情况,之后在concurrent mode下才会产生,使用ReactDOM.render创建的应用是没有以上特性的,所以react17也被成为一个过渡版本。

fiber的工作流程

react采用了双缓存机制来保存fiber树,即内存中存在两棵fiber树,称为current树和workInProgress树。current树表示当前正在页面中显示的fiber节点,每次触发更新时,react会根据current树,并结合触发的更新,采用深度优先遍历的方式创建workInProgress树。当workInProgress创建完毕后,react会切换两棵树,从而完成应用的更新。

在具体实现上,react中有一个fiberRoot节点,两个rootFiber节点。fiberRoot表示整个应用的根节点,每次触发更新时,都会从这个节点开始向下遍历。fiberRoot有一个current属性,指向current树。每次应用更新结束后,切换current指针的指向,就完成了页面的更新。

两个rootFiber节点就是current树和workInProgress树的根节点,并且每个current树节点和对应的workInProgress树节点都会有一个指针指向对方:alternate属性。

双缓存.jpg

react对于workInProgress的创建过程是很复杂的,这涉及到render阶段的diff流程,之后会单独讲解。

scheduler

这里先简单介绍一下scheduler这个模块,这个模块提供的功能就是提供任务调度功能,并且为任务提供优先级,实现高优先级任务打断低优先级任务。这样,react就能在更新时及时响应用户触发的事件。

react合成事件

看下面的代码,你能说出输出结果吗?

class App extends React.Component {
  componentDidMount() {
    outer.addEventListener('click', function() {
      console.log('native event click outer')
    })
    
    inner.addEventListener('click', function() {
      console.log('native event click inner')
    })
  }
  
  handleClickInner = () => {
    console.log('react event click inner')
  }
  
  handleClickOuter = () => {
    console.log('react event click outer')
  }
  
  render() {
    return (
    	<div className='outer' onClick={this.handleClickInner}>
      	<div className='inner' onClick={this.handleClickOuter}></div>
      </div>
    )
  }
}

这里就涉及到了react的合成事件机制,之后会有专门的文章进行介绍。

setState是同步还是异步

一个经典面试题了:setState到底是同步还是异步的,比如下面这段代码的输出结果

// 异步
handleClick = () => {
  this.setState({
    count: this.state.count + 1
  })
  this.setState({
    count: this.state.count + 1
  })
  this.setState({
    count: this.state.count + 1
  })
  console.log(this.state.count)
}

// 同步
handleClick = () => {
	setTimeout(() => {
    this.setState({
      count: this.state.count + 1
    })
    this.setState({
      count: this.state.count + 1
    })
    this.setState({
      count: this.state.count + 1
    })
    console.log(this.state.count)
  })
}

之后也会有专门的文章讲解react是如何创建更新,如何处理更新的。

hooks原理

hooks是react16一个重要的更新,hooks让我们能够在函数组件中使用state,之后也会有一篇文章讲解hooks是如何实现的。

react更新的两大阶段

react的一次更新包括两个阶段:render阶段和commit阶段

render阶段:包括了更新的计算,fiber树的diff算法,effectList的处理,dom节点的创建,类组件的生命周期等等

commit阶段:包括类组件的生命周期,useEffect的调度,setState回调函数的执行,dom节点的插入等等

之后也会有专门的文章讲解这两个阶段的流程。

由于自己水平有限,而且react源码内容繁多,结构复杂,再加上react17作为一个过渡版本,有很多为concurrent mode做铺垫的代码,所以阅读react源码是比较困难的,针对很多内容,笔者的理解也并不深刻,因此暂时先不做介绍,未来有机会的话会再写文章进行介绍。

最后希望大家在看完这个系列文章之后能够对react的整体运行流程有一个比较全面的认识。

推荐@Axizs大佬的react原理系列文章