做为一个构建用户界面的库,React的核心始终围绕着更新这一个重要的目标,将更新和极致的用户体验结合起来是React团队一直在努力的事情。为何React能够将用户体验作到这么好?我想这是基于如下两点缘由:javascript
本文是对React原理解读系列的第一篇文章,在正式开始以前,咱们先基于这两点展开介绍,以便对一些概念能够先有个基础认知。java
配合的源码调试环境在 这里 ,会跟随React主要版本进行更新,欢迎随意下载调试。
Fiber是什么?它是React的最小工做单元,在React的世界中,一切均可以是组件。在普通的HTML页面上,人为地将多个DOM元素整合在一块儿能够组成一个组件,HTML标签能够是组件(HostComponent),普通的文本节点也能够是组件(HostText)。每个组件就对应着一个fiber节点,许多个fiber节点互相嵌套、关联,就组成了fiber树,正以下面表示的Fiber树和DOM的关系同样:react
Fiber树 DOM树 div#root div#root | | <App/> div | / \ div p a / ↖ / ↖ p ----> <Child/> | a
一个DOM节点必定对应着一个Fiber节点,但一个Fiber节点却不必定有对应的DOM节点。git
fiber 做为工做单元它的结构以下:github
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { // Fiber元素的静态属性相关 this.tag = tag; this.key = key; // fiber的key this.elementType = null; this.type = null; // fiber对应的DOM元素的标签类型,div、p... this.stateNode = null; // fiber的实例,类组件场景下,是组件的类,HostComponent场景,是dom元素 // Fiber 链表相关 this.return = null; // 指向父级fiber this.child = null; // 指向子fiber this.sibling = null; // 同级兄弟fiber this.index = 0; this.ref = null; // ref相关 // Fiber更新相关 this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; // 存储update的链表 this.memoizedState = null; // 类组件存储fiber的状态,函数组件存储hooks链表 this.dependencies = null; this.mode = mode; // Effects // flags原为effectTag,表示当前这个fiber节点变化的类型:增、删、改 this.flags = NoFlags; this.nextEffect = null; // effect链相关,也就是那些须要更新的fiber节点 this.firstEffect = null; this.lastEffect = null; this.lanes = NoLanes; // 该fiber中的优先级,它能够判断当前节点是否须要更新 this.childLanes = NoLanes;// 子树中的优先级,它能够判断当前节点的子树是否须要更新 /* * 能够当作是workInProgress(或current)树中的和它同样的节点, * 能够经过这个字段是否为null判断当前这个fiber处在更新仍是建立过程 * */ this.alternate = null; }
首先要明白,React要完成一次更新分为两个阶段: render阶段和commit阶段,两个阶段的工做可分别归纳为新fiber树的构建和更新最终效果的应用。segmentfault
render阶段其实是在内存中构建一棵新的fiber树(称为workInProgress树),构建过程是依照现有fiber树(current树)从root开始深度优先遍历再回溯到root的过程,这个过程当中每一个fiber节点都会经历两个阶段:beginWork和completeWork。组件的状态计算、diff的操做以及render函数的执行,发生在beginWork阶段,effect链表的收集、被跳过的优先级的收集,发生在completeWork阶段。构建workInProgress树的过程当中会有一个workInProgress的指针记录下当前构建到哪一个fiber节点,这是React更新任务可恢复的重要缘由之一。数组
以下面的动图,就是render阶段的简要过程:架构
在render阶段结束后,会进入commit阶段,该阶段不可中断,主要是去依据workInProgress树中有变化的那些节点(render阶段的completeWork过程收集到的effect链表),去完成DOM操做,将更新应用到页面上,除此以外,还会异步调度useEffect以及同步执行useLayoutEffect。dom
这两个阶段都是独立的React任务,最后会进入Scheduler被调度。render阶段采起的调度优先级是依据本次更新的优先级来决定的,以便高优先级任务的介入能够打断低优先级任务的工做;commit阶段的调度优先级采用的是最高优先级,以保证commit阶段同步执行不可被打断。异步
Scheduler用来调度执行上面提到的React任务。
何为调度?依据任务优先级来决定哪一个任务先被执行。调度的目标是保证高优先级任务最早被执行。
何为执行?Scheduler执行任务具有一个特色:即根据时间片去终止任务,并判断任务是否完成,若未完成则继续调用任务函数。它只是去作任务的中断和恢复,而任务是否已经完成则要依赖React告诉它。Scheduler和React相互配合的模式可让React的任务执行具有异步可中断的特色。
为了区分任务的轻重缓急,React内部有一个从事件到调度的优先级机制。事件自己自带优先级属性,它致使的更新会基于事件的优先级计算出更新本身的优先级,更新会产生更新任务,更新任务的优先级由更新优先级计算而来,更新任务被调度,因此须要调度优先级去协调调度过程,调度优先级由更新任务优先级计算得出,就这样一步一步,React将优先级的概念贯穿整个更新的生命周期。
React优先级相关的更多介绍请移步 React中的优先级。
双缓冲机制是React管理更新工做的一种手段,也是提高用户体验的重要机制。
当React开始更新工做以后,会有两个fiber树,一个current树,是当前显示在页面上内容对应的fiber树。另外一个是workInProgress树,它是依据current树深度优先遍历构建出来的新的fiber树,全部的更新最终都会体如今workInProgress树上。当更新未完成的时候,页面上始终展现current树对应的内容,当更新结束时(commit阶段的最后),页面内容对应的fiber树会由current树切换到workInProgress树,此时workInProgress树即成为新的current树。
function commitRootImpl(root, renderPriorityLevel) { ... // finishedWork即为workInProgress树的根节点, // root.current指向它来完成树的切换 root.current = finishedWork; ... }
两棵树在进入commit阶段时候的关系以下图,最终commit阶段完成时,两棵树会进行切换。
在未更新完成时依旧展现旧内容,保持交互,当更新完成当即切换到新内容,这样能够作到新内容和旧内容无缝切换。
本文基本归纳了React大体的工做流程以及角色,本系列文章会以更新过程为主线,从render阶段开始,一直到commit阶段,讲解React工做的原理。除此以外,会对其余的重点内容进行大篇幅分析,如事件机制、Scheduler原理、重点Hooks以及context原理。
本系列文章耗时较长,落笔撰写时,17版本还未发布,因此参照的源码版本为16.13.一、17.0.0-alpha.0以及17共三个版本,我曾经对文章中涉及到的三个版本的代码进行过核对,逻辑基本无差异,可放心阅读。