点击这里进入react原理专栏
看react
源码有一段时间了,写篇文章总结一下,也希望能帮到大家。这是react原理系列的第一篇,主要讲解一下react
的基本架构,让大家有一个整体的认识。
- 什么是
fiber
,为什么要使用fiber
react
的三层架构模型:scheduler - reconciler - renderer
react
合成事件,当我们点击一个按钮触发click
事件时发生了什么react
是如何触发更新的(类组件的setState
,函数组件的useState
)react
更新的render
阶段react
更新的commit
阶段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
属性指向父级fiber
,child
属性指向第一个子fiber
,sibling
执行自己的下一个兄弟fiber
节点,stateNode
执行这个fiber
对象对应的组件或者真实的dom
节点。
为什么要用fiber
在采用fiber
架构之前,react
采用递归的方式处理虚拟dom,导致react
占用主线程的时间过长,可能造成页面假死。从前面的图可以看出,fiber
架构下的应用fiber
树是一个类似链表结构的多叉树,每个fiber
是一个独立的工作单元,这就为可中断的更新提供了便利。考虑以下两个情况:
react
更新过程中,用户点击了按钮- 页面动画
针对情况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
属性。
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原理系列文章