React 源码学习

将一个最简单的组件渲染到页面上,查看她的函数调用栈,可以将其划分为三个部分

  • 产生更新 —— 调度
  • 需要决定更新什么组件 —— 协调
  • 将更新的组件渲染到页面 —— 渲染

React的设计理念

React为了践行快速响应 快速响应 快速响应 快速响应 快速响应 快速响应的设计理念,做了那些事情呢?

  1. React 解决 GPU的瓶颈
    • 主流浏览器的刷新频率为 60Hz
    • 1000ms / 60Hz = 16.6ms 浏览器刷新一次
    • 在这16.6ms中,会依次执行 JavaScript -> 样式布局 -> 样式绘制
    • 如果JS脚本执行超过了16.6ms,这一帧就没有时间留给样式布局,以及样式绘制浏览器就会掉帧,表现形式就是浏览器的滚动不流畅,在输入框输入的内容不能及时的响应在页面上
    • 节流 & 防抖? 治标不治本
    • React 和浏览器做了一个约定, 浏览器给React 预留了一点时间,React利用这部分预留的时间来完成自己的工作,如果某一个工作需要的时间特别长,超出了这部分预留的时间,React会中断自己的工作,并将控制权交给浏览器,等待下一帧自己预留的那部分时间到来以后React再继续之前被中断的工作,这样浏览器在每一帧都有时间进行样式布局和样式绘制,这样子就能减少掉帧的可能性
  2. React解决 IO 的瓶颈 —— 将人机交互的成果融入 UI 交互
    • 框架层面实现异步更新机制
老的React架构

two parts

  1. 决定渲染什么组件 —— Reconciler 协调器
    • 协调器确定本次更新有什么组件需要被渲染 (Diff算法发生在此,Diff 官方叫法叫做 reconcile),确定需要更新的组件后会被交到渲染器
  2. 将组件渲染到视图中 —— Renderer 渲染器 (不同的渲染器会将组件渲染到不同的渲染数组中)
    • ReactDOM 渲染器,(浏览器, SSR)服务端渲染
    • ReactNative 渲染器 (渲染 App 原生组件)
    • ReactTest 渲染器 (渲染JS对象)
    • ReactArt 渲染器 (canvas SVG)
      在这里插入图片描述

当和浏览器协商到预留时间后,工作执行时,老的 React架构会在预留时间内完成工作,但是如果没有完成,则会先中断任务,此时对于用户来说只会更新第一个组件。这在用户看来就是一个BUG,所以 React 团队,在16 重构了React架构。

新的React架构

three parts

  1. 调度更新 —— 每个更新会被赋予一个优先级,优先级高就会被更快的调度,这个模块被称为 Scheduler 调度器
  2. 决定需要更新什么组件 —— Reconciler 协调器
  3. 将组件更新到视图中 —— Renderer 渲染器
    在这里插入图片描述
React 的新架构 —— Fiber (利用中断,恢复的思想完成异步可中断的更新)

我们在 React 中做的就是践行 代数效应 (Algebraic Effects)
—— Sebastian Markbage
代数效应 —— 代数效应是函数式编程中的概念,用于将副作用从函数调用中分离

async await 是有传染性的, 如果需要得到一个 async 函数的返回值, 则调用 async 函数的 (若也是函数)也需要被改变为 async 函数,将返回值用 await 接收

  • Fiber 纤程

可以理解为协程的一种实现,JS中 协程已经有一种实现 (Generator)

Q: 为什么不用Generator 实现异步可中断的更新,而自己实现一套纤程呢?
A : 1. Generator 和 async await 一样,也是有传染性的
2. 设计 Fiber 的两个原因:1). 更新可以中断并继续
2). 更新可以拥有不同的优先级(高优先级的更新可以打断低优先级的更新)

Fiber 架构的工作原理
  • Fiber 的含义:
    1. 作为架构来说:之前React 15 的协调器(Reconciler)采用递归的方式执行,数据保存在递归的调用栈中,所以被称作 Stack Reconciler; React 16 的协调器是基于 Fiber 节点实现的,所以被称为 Fiber Reconciler
    2. 作为静态数据结构来说:每个Fiber 节点对应一个组件,保存了该组件的类型对应的 DOM 节点等信息,这时的 FiberNode 也就是我们所说的 虚拟 DOM

我们有一个<App/>组件当我们首次调用ReactDOM.render()时,会创建整个应用的根节点(FiberRootNode)由于我们可以多次调用ReactDom.render 将不同的应用挂载到不同的DOM节点下 所以,每个节点都有它自己的根节点RootFiber。在一个页面中 可以有多个RootFiber,但是只能有一个FiberRootNode来管理这些RootFiber。函数组件App会创建一个对应的Fiber节点该Fiber节点的类型为FunctionComponent,原生DOM节点会创建一个Fiber节点,类型为HostComponent,文本则会创建文本Fiber节点。

  • 这些Fiber节点是如何连接的呢?

其中 FiberRootNode.current 指向RootFiber,而RootFiber.child 指向自定义组件,自定义组件.child 指向原生DOM节点。如果是兄弟节点则使用,sibling指向兄弟。而 儿子节点的 return 又指向了父节点
你可能会问,为什么不用 parent 指向父节点,而是使用return指向呢?因为 React 15 stack Reconciler 采用递归的形式工作。 这一点在 Fiber Reconciler 采用遍历的形式实现可中断的递归,所以,也复用了这种思想。
但在根节点这里比较特殊,RootFiber会使用 stateNode 指向 FiberRootNode。

  1. 作为动态工作单元来说:Fiber保存了组件需要更新的状态以及需要执行的副作用
// Fiber数据结构
function FiberNode{
	tag: workTag,
	pendingProps: mixed,
	key: null | string,
	mode: TypeOfMode
} {
	// Instance
	this.tag = tag; // Fiber 对应的组件类型 FunctionComponent ClassComponent HostComponent
	this.key = key; // 我们常用的 key 属性
	this.elementType = null; // 大部分情况 elementType 和 type 相同
	this.type = null; // FunctionComponent 使用 React Meno包裹时候它们不同; function class tagName
	this.stateNode = null; // 对于HostComponent来说指它对应的真实DOM节点
	
	// Fiber
	this.return = null; // 会将Fiber节点连接组成一颗Fiber Tree 指向父节点
	this.child = null; // 会将Fiber节点连接组成一颗Fiber Tree 指向孩子节点
	this.sibling = null; // 会将Fiber节点连接组成一颗Fiber Tree 指向兄弟节点
	this.index = 0; // 对于多个同级Fiber 节点 代表它们插入的位置索引

	this.ref = null; // 常用的 ref 属性
	
	// 从这里开始往下,都是讲Fiber 当作动态的工作单元使用时的属性
	this.pendingProps = pendingProps;
	this.memoizedProps = null;
	this.updateQueue = null;
	this.memoizedState = null;
	this.dependencies = null;
	
	this.mode = mode;
	
	// Effects
	// 带有 effect 都跟副作用相关
	// 对于HostComponent 它的副作用包括 DOM节点的增删改查
	// 对于FunctionComponent 它的副作用代表着我们使用了 useEffect 或 uselayoutEffect 这两个hook
	this.effectTag = NoEffect;
	this.subtreeTag = NoSubtreeEffect;
	this.deletions = null;
	this.nextEffect = null;

	this.firstEffect = null;
	this.lastEffect = null;
	
	// lane 相关的属性与优先级的调度有关	
	this.lanes = NoLanes;
	this.childLanes = NoLanes;

	this.alternate = null; // 关系着 fiber 的工作方式
}
  • Fiber架构使用一种叫双缓存的工作机制 —— 什么是双缓存?
    在内存中构建当前帧替换上一帧的技术被称作双缓存

在这里插入图片描述

当完成 workInProgress Fiber Tree 完成了渲染,FiberRootNode 会指向 workInProgress Fiber Tree 的 RootFiber
这时,workInProgress Fiber Tree 就成为了 Current Fiber Tree
在这里插入图片描述
接下来,我们点击 p 标签,触发一次更新,每次触发更新,都会创建一颗 workInProgress Fiber Tree,我们将 current Fiber树的节点,称为 current 节点,此时,current FiberRoot.alternate 属性 已经指向了一个 RootFiber,所以在创建workInProgress Fiber Tree时 会基于这个RootFiber来创建。在本次更新中,除了RootFiber,其他节点都有对应的current Fiber 存在,这种将 current Fiber与本次更新返回的 JSX 结构做对比 生成 workInProgress Fiber Tree的过程就叫做 diff算法,所以 首屏渲染与更新最大区别在于在创建fiber树的过程中,是否有diff算法。

在这里插入图片描述

如何调试源码?
  1. 从 React 仓库拉取最新的代码
  2. 安装依赖并执行构建命令 (构建 React ReactDOM scheduler 的CommonJS版本)
  3. 使用 create-react-app 创建新应用
    在这里插入图片描述
  • React 文件目录结构
根目录
 |- fixtures # 包含一些给贡献者准备的小型 React 测试项目
 |- packages # 包含元数据 (比如 package.json) 和 React 仓库所有 package 源码 (子目录)
 |- scripts # 各种工具链的脚本, 比如 git, jest, eslint等
架构工作流程概览

在这里插入图片描述
在这里插入图片描述

可以根据之前讲的调度器,协调器和渲染器的工作来对比着看
调度器大体的目的是为了创建根Fiber节点,当首次执行ReactDOM.render 时,会创建FiberRootNode
协调器创建workInProgress Fiber Tree的流程被称为递与归,这里递就是我们观察到的beginWork,归的过程就是completeWork。 有几个beginWork 就有几个 completeWork
在React内部,协调器被称为render阶段,渲染器被称为commit阶段
渲染器的主要目的是将变化过的节点渲染到视图上,所以渲染器的工作流程可以分为三个子阶段

  1. 渲染到视图之前(commitBeforeMutationEffects),2. 渲染到视图(commitMutationEffects), 3. 渲染到视图之后(callCallBack)
深入理解 JSX
  • JSX 与 Fiber 的关系
  • React Component 与 React Element 的关系
    1. React Element 就是 React.createElement调用的结果
    2. React Component 就是 class Component 或则 function Component, 是作为 React.createElement 的第一个参数
“递”(begin)阶段 mount 时流程

render 阶段的开始,开始于renderRootSync, commit 阶段开始于commitRoot,下面会介绍 render阶段所做的工作;render阶段使用了遍历实现了可中端的递归,其中,递归可以分为递和归。递阶段执行的就是beginWork 归阶段执行的就是completeWor

先了解整个 render 阶段的工作流程,现在我们知道 递阶段的七点是beginWork 归阶段的起点是completeWork。

第一个进入beginWork的节点,可以看到 它的 tag 为 3我们发现 HostRoot 的 tag为3(当前应用的根节点)。
当我们继续执行,下一个进入beginWork的节点就没有了 current。之前在 Fiber 架构 双缓冲机制中介绍,对于首屏渲染,只有当前应用的根节点存在 current, 而其他节点只存在 workInProgress.
当我们工作到的 DOM节点没有子节点后,会执行 completeWork,然后会进入兄弟节点的beginWOrk。 这个过程就是 深度优先遍历。
React 会对于只有唯一一个文本节点的子节点,做出了一些优化,在这种情况下,这种文本节点不会生成自己的 FiberNode
不断的进行 beginWork,completeWork。 这样,render 阶段就完成了,接下来会进入 commit 阶段。

接下来我们看看,beginWork,completeWork 做了些什么

首先 beginWork 它会根据 workInProgress.tag 进入不同的 case。
这里 div 是一个 HostComponent, 所以他会进入 updateHostCoomponent 逻辑。在这之中,首先,会赋值一些变量。其中 isDirectTextChild, 就是为了检测,当前的 FiberNode,它是否只有唯一一个文本节点(如果是这种情况,React 不会为它的文本子节点创建一个独立的 FiberNode; 这是一种优化的路径)
接下来,我们会进入 reconcileChildren 这个方法,在执行这个方法之前,当前workInProgress.child 是null,所以 reconcileChildren 这个方法会为当前 workInProgress 创建它的子FiberNode。其次,我们也可以从它的名称中看出一些端倪,当前执行的阶段叫做 render 阶段,而render 阶段是发生在协调器中的,协调器的 英文是 reconcile,这个方法的名字叫做 reconcileChildren,所以它和reconcile的关系很紧密。
我们进入 reconcileChildren,判断 current 是否为 null 来进入 mountChildFibers / reconcileChildFibers. 这两个方法有什么区别呢?
(React 源码中, 后缀带old 的代表当前使用的, 带new 的是React 团队为了实验一些新的功能所使用的版本,我们只需要关注old里的功能)
在源码中发现, 不管是 mountChildFibers 还是 reconcileChildFibers 都是通过 ChildReconciler(false/ true) 方法来调用, 它们传入了不同的布尔值,而这个布尔值从字面意思上来看(shouldTrackSideEffects)就是是否追踪副作用。
那么,什么是副作用呢?
我们可以看到, 如果是删除一个 child 时,它不追踪副作用,那么他会直接return。而如果要追踪副作用,最终,他会为需要删除的 FiberNode 的 effectTag 赋值为 Deletion. 我们在看看其他的,比如placeChild, 它代表需要将当前FiberNode 对应的DOM节点插入到页面中, 如果它不需要追踪副作用,那么它会直接return掉,如果它需要追踪副作用,他会为这个FiberNode的effectTag 赋值为 Placement. 那么 Placement, Deletion 到底有什么作用呢?
搜索 ReactSideEffectTags.js 文件,这个文件下保存了 React 中 所有的副作用,我们知道 render 阶段不会执行具体的 DOM 操作具体的 DOM 操作是在 commit 阶段执行的,render 阶段需要做的就是为需要执行 DOM 操作的 Fiber 节点打上标记, 比如如果这个FiberNode 对应的DOM 节点需要插入在页面中, 他会为当前的FiberNode打上 Placement的标签,如果这个FiberNode 对应的 DOM 节点需要删除,那么他会为这个FiberNode打上Deletion的标记,这些标记都是使用二进制来表示,为什么呢?原因在于考虑一种情况,如果我们有一个Fiber节点,它对应的DOM节点,首先需要插入到页面中,其次它需要更新它的属性,那么它需要同时存在Placement 以及 Update这两个 EffectTag,使用二进制掩码的形式就能很方便的实现这个功能(利用按位或)
接下来,回到我们的childFibers, 继续执行,我们刚刚说了, 进入mountChildFIbers创建的 Fiber 节点它是不会被标记effectTag的那么, 对应的DOM 节点是如何插入在页面中的呢?这个疑问,我们会在下一节来解答。我们可以看到reconcileChildFibers这个方法中它会判断当前,newchild的类型,对不同的类型进入不同的处理逻辑,比如,他会判断是否是一个 object, 其次,它会判断是否有 $$typeof 的属性,如果有的话,并且它的值为 REACT_ELEMENT_TYPE,那么就会把它当作一个单个的 React Element 来处理
如果当前的newChild 是一个 string 类型, 或则number类型那么就代表,它是一个文本节点,那么他就会被当作单个的文本节点来处理
如果当前的 newchild是一个数组,他就会帮当作一个数组处理。现在我们进入 reconcileSingElement(单个 React y元素),在这个方法中会判断它的child是否存在,接下来我们会判断它的类型,最终进入了createFiberFromElement 也就是通过这个 React Element 数据来创建一个Fiber 节点。 在 createFiberFromElement 方法内部,会调用,createFiberFromTypeAndProps,进入这个方法它会判断 type的类型当前HostComponent它的他ype是string(例子)所以它会进入相应的逻辑,接下来它会创建对应的Fiber 节点,可以看到 FiberNode 有非常多的属性。
接下来我们来总结一下:
beginWork 的目的是:当某一个 FiberNode 进入 beginWork 时它最终的目的是为了创建当前 FiberNode 的第一个子 FiberNode, 它会经历如下图所示,从下到上的步骤
进入beginWork 后,首先,它会判断当前 FiberNode 的类型进入不同的update 逻辑,其次,在update的逻辑中它会判断当前workInProgress Fiber 是否存在对应的 current Fiber来决定是否标记 effectTag, 接着它会进入reconcileChildFibers, 在这里,它会判断当前FiberNode 的child是什么类型,来执行不同的创建操作,最终会创建它的子FiberNode。
每次执行beginWork 都只会创建一个FiberNode
在这里插入图片描述

“归”(complete)阶段 mount 时流程

首先会根据不同的 workInProgress.tag 会进入不同的 case 。
它会判断 current 是否存在,在首屏渲染时它的 current 是不存在的。
我们会为HostComponent 对应的FiberNode 创建 对应的DOM节点(在createInstance中创建)
createInstance 中会执行 createElement.
创建完DOM节点后会将创建的DOM节点插入到之前已经创建好的 DOM 树中(在appendAllchildren中执行)
接下来会将 当前dom 保存在 当前dom节点 对应的 stateNode 属性上。
最后我们已经有了 DOM节点 那我们需要为DOM 节点设置一些属性(在 finalizeInitialChildren中)首先会判断这是否是一个自定义的标签,接下来会根据 HostComponet tag 来进入不同的逻辑,接下来会判断我们的 props 是否合法。接下来进入初始化DOM属性的操作,这里会做一些属性的判断(比如是否是style属性,是否是 children )。
最终会进入 setValueForProperty, 在这里做一些设置属性的逻辑。执行node.setAttribute 方法, 为我们的属性赋值。当执行完这个操作后,一个FiberNode 的 completeWork 就做完了,开始执行下一个 FiberNode 的 completeWork
对于Function Component 和 其他一些 Component 都不会执行 completeWork

现在我们看看appendAllchildren,这个方法的意义在于每次执行它时,都会将已经创建好的DOM节点挂载到当前的DOM节点下。
当我们归阶段,从每一个子节点一路向上归到根节点时,我们创建的每一个子节点的DOM元素都会挂载在它的父级节点下。这样子,当我们执行到 app 时,我们已经有一颗构建好的DOM树。
在reconcileChild中 当某个FIber节点不存在对应的currentNode时候它是不会被标记 effectTag的那么首屏渲染这些DOM 节点是如何挂载在页面中的呢?
对于首屏渲染,会有一个节点进入到这个逻辑,而这个存在current 的FIberNode也就是当前应用的根FiberNode,它的tag 为 3,我们进入这个逻辑,可以看到它会进入reconcileSingleElement, 当 reconcileSingElement 执行完以后它会执行 placeSingleChild,此时它的shouldTrackSideEffects 为 true,所以当前应用的根FiberNode创建的子FIberNode 会被 赋值 effectTag,也就是app节点会被赋值 effectTag。
当所有 FiberNode 的completeWork执行完后,会执行
var finisheWork = root.current.alternate;
root 是整个应用唯一的根节点 FiberRootNode
root.current 指向了当前应用的根节点也就是 currentFiber 树
root.current.alternate 指向的是 workInProgress Fiber Tree 的根节点,也就是本次更新创建的 workInFiber 树的根节点

“递”(beginWork)阶段 update 的流程

首次执行React.render 时 会创建整个应用的FiberRootNode,FiberRootNode.current 指向了 当前应用的根节点 rootFiber ,当进入首屏渲染的逻辑时会基于current RootFiber创建workInProgress rootFiber。
创建workInProgressRootFiber的过程发生在createWorkInProgress 的函数中,可以从调用栈中看见第一个执行createWorkInProgress 传入的参数为 root.current, 其中 root 就是 FiberRootNode, root.current 就指向的是 current rootFiber, 它的 tag 为3
在 createWorkInFiber 中,它会判断两种情况,一种是workInProgress 是 null ,一种是 workInProgress 不是 null, 对于我们首屏渲染来说 current rootFiber 不存在alternate指针 所以 workInProgress rootFiber 是不存在的
所以会进入 workInProgress 为null的环节。在这个环节中
我们会createFiber,创建一个新的FiberNode,并且将这个FiberNode 的参数赋值为它对应的 current 节点的同名参数。接下来就会进入构建 workInProgressFiber Tree 的流程 从我们的例子中,第一个FiberNode 是app function FiberNode,它的child 是div,而div是一个 hostComponent,所以它会在render阶段的updateHostComponent中它会执行 reconcileChildren…
执行完 reconcileChildren 后 workInProgress.child 就是一个新的FiberNode

整个 render 完成以后就会进入 commit 阶段,commit阶段渲染完页面以后整个视图就更新了。更新完视图以后FiberRootNode的current 指针就会指向之前的 workInProgress rootFiber, 这样workInProgressFiber 树就变成了 current Fiber Tree

“归”(complete)阶段 update 的流程

React 会将 所有 发生过更新的component 对应的 Fiber 节点会被打上effecttag,在归阶段用链表存储。

架构篇指 commit 阶段

接着上面。在 commit 阶段会遍历这条链表,执行对应的操作。这个操作被称为 mutation, 对于 hostComponent 来说 mutation 意味着 DOM 节点的 增删改。
整个 commit 的工作可以分为三个子阶段

  1. mutation 前 —— before mutation 阶段
  2. mutation —— mutation 阶段
  3. mutation 后 —— layout 阶段

commit 阶段开始于 commitRoot
这个方法中,会执行 runWithPriority ,runWithPriority 是由调度器(Scheduler)提供,这个方法接收两个参数,第一个参数是一个调度的优先级,第二个参数是一个调度的回调函数,在这个回调函数中,触发的任何调度都会以第一个参数作为优先级。所以 commitRoot 实际要执行的是 commitRootImpl

在 commitRootImpl中,我们会判断 rootWithPendingPassiveEffects 是否为 null, 如果不为 null,就会执行 flushPassiveEffects (查看再次之前是否有还未执行的effect)。
在这里插入图片描述

before mutation 阶段

在这个阶段会执行, commitBeforeMutaionEffects
这个函数会做三个事情

  1. 跟DOM组件的 foucs blur 相关的操作
  2. 执行 commitBeforeMutationEffectOnFiber, 在这个方法中会执行getSnapShotBeforeUpdate 这个生命周期函数。
  3. 如果当前 FiberNode 的 EffectTag 中 包含了 passive, 就需要调度 passiveEffect的回调函数。
mutation 阶段

mutation 阶段会在 commitMutationEffects 函数,这个方法是一个 while 循环,它会遍历包含effect 的FiberNode链表。
首先,它会判断是否需要重置文本节点
接下来,它会判断是否有 ref的更新
再接下来,它会判断是否有 placement | update | deletion | hydrating

在 mutaion 阶段会执行,componentWillUnmount 生命周期函数,此时root.current 还指向之前的 Fiber Tree

layout 阶段

在 commitLayoutEffects 会调用 componentDidMount/ Update 这两个生命周期函数
layout阶段会遍历执行 commitLayoutEffects
此时,current Fiber 树已经指向了此次更新的 workInProgress Fiber 树

在这里插入图片描述

diff 算法流程概览

一个 DOM 节点 在某一时刻,最多会有 4 个节点和他相关。

  1. current Fiber。 如果该 DOM 节点 已经在页面中,current Fiber 代表该 DOM节点 对应的 FiberNode

  2. workInProgress Fiber. 如果该 DOM节点 将在本次更新中渲染到页面中,workInProgress Fiber 代表 DOM节点 对应的 FiberNode。

  3. DOM 节点本身。

  4. JSX对象。 即 ClassComponent 的 render 方法的换回结果,或 FunctionComponent 的调用结果。 JSX对象 中包含DOM节点信息。
    Diff 算法的本质是 对比 1 和 4, 生成 2.

Diff 的 3 个策略
5. 只对同级进行 Diff。 如果一个 DOM节点 在前后两次更新中跨越了层级,那么 React 不会尝试复用他。
6. 两个不同类型的元素会产出不同的树。如果元素由 div 变为 p, React 会销毁 div 及其子孙节点,并新建 p 及其子孙节点。
7. 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定。

状态更新

状态更新流程概览

整个状态更新的流程,大体上是这样的:
我们需要一些机制触发状态更新,并且经过一些流程,进入render阶段,在 render阶段中,会执行 reconcile 也就是 diff 算法,最终diff 的结果会被交给 commit 阶段 commit 阶段执行视图更新。
其中 render 阶段的起点是 performSyncWorkOnroot 或 performConcurrentWorkOnRoot, 这取决于我们的应用是同步的模式还是并发的模式。
什么是并发模式呢? 也就是我们说的 Concurrent 模式,这个模式是 React 一个实验性的功能,再未来的 React 中 会全面的开启,如果开启了 Concurrent 模式那么我们的更新就会获得不同的优先级,不同的优先级,会以异步的方式来调度执行,而我们当前使用 reactDOM.render 创建的应用是同步的模式也就是所有的更新都是同步进行的。
在这里插入图片描述
我们知道,在React中,有很多方式可以触发更新:
比如触发首屏渲染的 ReactDOM.render
对于 class Component 的 this.setState 以及 this.forceUpdate
对于 function COmponent 的 useState 以及 useReducer
这些方法调用的场景各不相同,它们是如何接入同一套状态更新机制的呢?
答:每次 状态更新 都会创建一个保存更新状态相关内容的对象,我们叫他 Update,在render 阶段, 的beginWork 中 会根据 Update 计算新的 state

对于 Function Component 来说 useState 触发的更新的方法会调用dispatchAction方法 -> 同步或并发(render) -> commitRoot

下面来简单根据调用栈来看一下 useState 的更新流程
在dispatchAction 中, 第一个参数为 fiber,接下来我们会创建一个变量 update,这个update就是我们刚才讲过的。
action 就是本次更新的传参,接下来,这个update会被保存在一条环状链表 pending 最终我们会调用 scheduleUpdateOnFiber, 可以从方法的名称判断是在这个Fiber中调度这个 update,接下来我们有一个问题,我们知道,在render阶段,我们会从当前应用的根节点 rootFiber 一直向下 深度优先遍历,但是,dispatchAction 触发的是 app 这个 function Component 对应的 Fiber, 所以我们需要从 app function component 对应的 Fiber 一直向上遍历到应用的根节点,这一步的操作叫做 markUpdateLaneFromFiberToRoot这里第一个参数sourceFiber 是 触发更新的那个Fiber,这里会将Fiber的return也就是它的父级Fiber 赋值给 parent变量,并且一直循环将parent.return 赋值给 parent,这样,我们会一直向上遍历父级Fiber节点,直到这个 FiberNode 的tag为 HostRoot,也就是当前应用的根节点,我们将当前应用的根节点的stateNode 也就是整个应用的根节点赋值给root最终我们会返回这个root。那么这个遍历过程中都做了什么呢,其实,就是为遍历到的每一个parentFiberNode的childLanes 来赋值,我们之前已经直到 Lane 是跟优先级相关的变量,在这里,我们会讲到少部分优先级相关的知识,优先级的详细讲解,会放在concurrent 模式来讲。可以看到,这里会判断当前更新的优先级是否是同步的优先级由于我们采用的是concurrent 模式所以当前的更新并不是一个同步的更新。我们会调用ensureRootIsScheduled,从方法名可以看到,这是确保当前整个应用的根节点被调度。为什么需要它被调度呢?因为我们从整个应用的根节点向下遍历到app那个function component中存在一个update所以说我们就需要调度它,这一步我们会将过期未执行的Lane标记为过期。接下来我们会获取优先级最高的Lane,如果当前没有任务的话那么就会返回。由于 React 是通过 调度器 scheduler来调度,所以需要将 lane的调度转换为 scheduler的调度,通过lanePriorityToSchedulerPriority这个方法来转换,转换完以后就调度本次更新,将我们转换后的优先级作为本次更新调度的优先级传入scheduleCallback,调度的就是 render的起点方法。而当我们调用了render阶段的起点时此时从我们整个应用的根节点一直向下遍历后就会找到我们包含更新的那个function component 对应的FiberNode,所以,我们整个更新流程大体就是。
创建update对象,对于function component 来说就是在dispatchAction方法中创建update对象,下一步,从Fiber到root,也就是调用markUpdateLaneFromFiberRoot,这样子我们就得到了整个应用的根节点,接下来 调度整个应用的根节点调用的方法是ensureRootIsScheduled,而在ensureRootIsScheduled中会判断当前这个任务是什么优先级,是否是同步的还是并发的,其中同步的会执行performSyncWorkOnRoot, 并发的会执行performConcurrentWorkOnRoot, 而我们刚才那个任务是并发的,所以说它最终被调度的方法是performComcurrentWorkOnRoot.
在这里插入图片描述
让我们来看看我们整体状态更新的流程

在这里插入图片描述

优先级与Update

优先级与Update 两者有什么关系呢?
可以从SchedulerPriorities.js 文件看到
NoPriority 是初始化时候的无优先级
ImmediatePriority 是立刻执行的优先级 也就是同步的优先级,在React中,这是最高优的优先级
UserBlockingPriority 由用户触发的更新会拥有这个优先级,比如点击事件。
NormalPriority 是一般优先级,这种优先级是最常见的,比如请求服务端的数据,当数据返回时更新状态那么此时在数据返回以后的更新状态就是这个优先级
IdlePriority 空闲优先级

在这里插入图片描述

优先级是一个全局的概念,而Update存在于触发更新的那个FiberNode中的。
我们知道,在每次更新中,一个组件对应的Fiber会拥有自己的状态,那么这个状态是如何计算得出的呢? 有这样一个公式
在这里插入图片描述
组件在某一刻的新状态等于组件在上一刻的基础状态基于当前一个优先级的update计算得到,假设,我们用绿色代表NormalPriority 红色代表UserBlockingPriority那么,如果本次更新我们的优先级是UserBlockingPriority我们的组件状态就是baseState 基于 update2计算后得到的 newState,因为我们的update1是一个NormalPriority 它的优先级是低于本次更新的优先级的所以不参与本次更新的计算。

Update 的计算

首先,来了解 Update 的设计理念,它参照了git,代码版本控制。
在这里插入图片描述

ReactDOM.render 的完整流程
React.render 初始化时会执行那些流程呢?
当前,我们整个应用的根节点是不存在的,所以我们会创建整个应用的根节点。

可以看到,this.setState内会调用this.updater.enqueueSetState方法。
在enqueueSetState方法中就是我们熟悉的从创建update到调度update的流程了。

而useEffect是在commit阶段完成渲染后异步执行。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值