手写mini-react!超万字实现mount首次渲染流程🎉🎉

hey🖐! 我是小黄瓜😊😊。一枚小透明,期待关注➕ 点赞,共同成长~

写在前面

本系列会实现一个简单的react,包含最基础的首次渲染,更新,hooklane模型等等,本文是本系列的第一篇。这对于我也是一个很大的挑战,不过这也是一个学习和进步的过程,希望能坚持下去,一起加油!期待多多点赞!😘😘

本文致力于实现一个最简单的首次渲染流程,代码均已上传至github,期待star!✨: github.com/kongyich/ti...

期待点赞!😁😁

食用前指南!本文涉及到react的源码知识,需要对react有基础的知识功底,建议没有接触过react的同学先去官网学习一下基础知识,再看本系列最佳!

准备部分的知识非常重要!!!

准备

一. Fibe架构

1. Fiber

fiber​在整个react架构的整个流程都至关重要。可以说,无论是react的初次渲染还是后续的更新,都是以fiber​作为最小粒度进行执行。

fiber​其实就是保存了一个节点在各个执行流程中所需要的各种信息:自身的类型,所代表的真实dom节点,与父/子/兄弟节点之间的关系,更新相关的信息,优先级等等。

js 复制代码
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; // 对应的DOM元素的标签类型,div、p...
  this.stateNode = null; // 实例,类组件场景下,是组件的类,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)树中的和它一样的节点,
  * */
  this.alternate = null;
}

FiberNode​ 这个构造函数中有非常多的属性,这些属性根据用途可以分为三类:

  • 构成Fiber结构

其实整个fiber​树是以链表的形式进行首尾连接的,这就需要在每个Fiber​中使用一些属性来保存位置和结构信息,比如父节点,兄弟节点,子节点等等。用来构成整棵Fiber​树。

js 复制代码
// 父节点
this.return = null;
// 保存第一个子节点
this.child = null;
// 保存同级兄弟节点
this.sibling = null;
// 当前位置下标
this.index = 0;
  • 保存数据

与自身fiber​相关的数据,例如节点的类型,真实dom,key值等。

在React​中元素可以是<div>aa</div>​等基本HTML​元素,也可以是<App />​这样的组件,所以会在fiber​中进行类型的标识。 ‍

js 复制代码
// Fiber 的类型。例如 FunctionComponent (函数组件)、ClassComponent(类组件)
this.tag = tag;
// 组件的key
this.key = key;
// ReactElement 的类型。
// 例如 REACT_ELEMENT_TYPE(自定义元素),REACT_PORTAL_TYPE(portal)、REACT_FRAGMENT_TYPE(Fragment)等。
this.elementType = null;
// 对应的DOM元素的标签类型,div、p...
this.type = null;
// fiber对应的真是dom
this.stateNode = null;
  • 作为调度功能使用

fiber​树在生成或者更新的过程中存在调度优先级的功能,高优先级的功能会被优先执行。所以就需要一些属性对每个fiber​进行标识优先级,如果对应fiber​存在一些副作用操作,比如增加,删除等,也会在fiber​中利用位运算的方式进行标识。

js 复制代码
// 副作用标识
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
// 该fiber中的优先级
this.lanes = NoLanes;
// 子树优先级
this.childLanes = NoLanes;
// 缓存fiber
this.alternate = null;

比如说我们现在有如下jsx​代码:

js 复制代码
function App() {
  return (
    <div>
      hello
      <p>瓜瓜</p>
    </div>
  )
}

它最终会会形成如下fiber​链表:

其中每一个 fiber​ 是通过 return​ , child​ ,sibling​(同级) 三个属性建立起联系的。return​: 指向父级 Fiber 节点。 child​: 指向子 Fiber 节点。 sibling​(同级):指向兄弟 fiber​ 节点。

整个fiber​是以链表的形式进行连接的,关于链表这种数据结构在下文有介绍。

2. 双缓存树

整个fiber​架构中还存在双缓存树的概念,在每个fiber​结构中的alternate​保存着与之对应的workInProgress​缓存树。

当进行视图更新的时候,会同时存在两棵fiber​树,一个current​树,是当前渲染在页面上内容对应的fiber​树。另一个是workInProgress​树,它是依据current​树深度优先遍历构建出来的新的fiber​树,保存在内存中。所有的更新最终都会体现在workInProgress​树上。而每个fiber​节点alternate​属性指向另一棵树中的自己。

当更新未完成的时候,页面上始终展示current​树对应的内容,当更新结束时(commit​阶段的最后),页面内容对应的fiber​树会由current​树切换到workInProgress​树,此时workInProgress​树即成为新的current​树。

如下图所示:

还有一个点需要注意一下几个概念,element​ 、fiber​ 、 DOM​、其实这是在不同的处理过程的不同形态。

  • elementReact 视图层在代码层面的表现,也就是的 jsx 语法,元素结构,都会被创建成 element 对象的形式。上面保存了 propschildren 等信息。
  • DOM 是元素在浏览器上真正的dom元素,也就是用于在浏览器中绘制的html。
  • fiber 可以说是是 element 和真实 DOM 之间的交流桥梁,每一个类型 element 都会有一个与之对应的fiber 类型,element 变化引起更新流程都是通过 fiber 做一次调和改变,然后形成新的 DOM 做视图渲染。

调和指的是:新旧dom树进行对比的过程。

3. FiberRootNode与HostRootFiber

FiberRootNode​表示应用根节点。它保存着应用的状态信息和组件信息。

每个应用都会有唯一的FiberRootNode​实例用来维护整个应用的状态和组件信息。

FiberRootNode​是单例对象,每个应用程序只会有一个实例,如果一个页面有多个React​应用,那么会有多个实例。

HostRootFiber​是使用createHostRootFiber​创建的Fiber​根节点,它包含整棵组件树的信息,代表一整棵fiber​树。

使用这两个属性可以再来模拟一下切换缓存树的整个过程:

首先在mount​时,会在内存中生成一棵fiber​树,在完成整个render​流程,渲染到页面中后会被赋值到FiberRootNode​的current​属性上,此时workInProgress​为null​:

而当更新发生时,会在内存中重新构建一棵fiber​树,所有的标记等操作均在此fiber​树中完成,当render​流程完成后,current​指针会指向workInProgress​,旧的fiber​树会被舍弃,workInProgress​再次恢复为null​:

二. flag与tag

flag相关

当react更新时,会涉及到一些对于节点的副作用操作,涉及到页面dom的变更,这时对应的fiber​节点中将会利用位运算(详细过程参见下文中位运算部分)标记相应的副作用标记,节点的新增/删除/更新,保存在fiber​节点的flag​属性中。

同时在completeWork​的向上归并的过程中,为了避免深层次的遍历,子树是否存在副作用变更也会保存在相应fiber​节点的subtreeFlags​ 属性中(下文将会详细说明整个过程)。

js 复制代码
// 无副作用标识
export const NoFlags = 0b0000000;
// 节点新增标记
export const Placement = 0b0000001;
// 节点更新标记
export const Update = 0b0000010;
// 节点删除标记
export const ChildDeletion = 0b0000100;

tag相关

tag​主要在fiber​中标记当前节点是什么类型。例如:HostComponent​ 代表当前节点为原生dom,比如<div></div>​。

FunctionComponent​ 代表当前节点为函数组件,比如<App />​。HostText​ 代表当前节点为文本节点,比如12344​。

js 复制代码
// 函数组件
export const FunctionComponent = 0

// 项目挂载的根节点
export const HostRoot = 3

// <div>
export const HostComponent = 5

// div -> text
export const HostText = 6

二. 位运算

那么为啥要使用位运算呢? 其实目的就是可以对流程判断进行统一管理,使整个节点的判断更加的清晰易用。 那么什么是位运算呢?移位运算就是对二进制进行有规律低移位。其实可能在日常的开发中并不会经常接触二进制的数据,但是对于二进制js​也已经进行了支持,比如toString​方法:

js 复制代码
 (4).toString(2) // 100

toString​方法传入参数2,就代表将某个数字转换为二进制数据。 JavaScript 中的按位操作符有:

下面举几个例子,主要看下 AND​ 和 OR​ :

js 复制代码
 # 例子1
 A = 10001001
 B = 10010000
 A | B = 10011001
 
 # 例子2
 A = 10001001
 C = 10001000
 A | C = 10001001
js 复制代码
 # 例子1
 A = 10001001
 B = 10010000
 A & B = 10000000
 
 # 例子2
 A = 10001001
 C = 10001000
 A & C = 10001000

位运算结合 按位与 、按位或 在权限系统中有非常巧妙的应用。可以使用|​(按位与) 可以用来赋予权限,&​(按位与) 可以用来校验权限。例如:

添加权限

js 复制代码
 let a= 100
 let b = 010
 let c = 001
 
 // 给用户赋全部权限(使用前面讲的 | 操作)
 // 拥有了全部权限
 let user = a | b | c  // 111

取反

按位非(~),对一个二进制操作数逐位进行取反操作 (0、1互换)。

js 复制代码
// 逐位取反
 A = 10001001
 ~A = 01110110

校验权限

js 复制代码
 let a = 100
 let b = 010
 let c = 001
 
 // 给用户赋 a b 两个权限
 let user = a | b // 110
 
 console.log((user & a) === a) // true  有 a 权限
 console.log((user & b) === b) // true  有 b 权限
 console.log((user & c) === c) // false 没有 c 权限

在react中的应用

位运算在react​中在fiber​节点标记副作用和优先级中大量使用了位运算,比如在处理fiber​节点时,发现此节点发生了变动,就在节点上标记一个"更新"标记,如果发现被删除,就在节点上标记"删除"的标记。也就是标记flag​。

react源码内部有多个上下文环境,在执行方法时经常需要判断"当前处于哪个上下文环境中":

js 复制代码
// 未处于 React 上下文
const NoContext = 0b0000

// 处于batchedupdates 上下文
cost BatchedContext = 0b0001

// 处于 render 阶段
cost RenderContext = 0b0010

//处于commit 阶段
const CommitContext = 0b0100

当执行流程进入 render​ 阶段,会使用按位或标记进入对应上下交:

js 复制代码
let executionContext = NoContext;
executionContext |= RenderContext:
// 等价于
executionContext = executionContext | RenderContext:

执行过程如下:

js 复制代码
  0b000 0000 0000 0000 0000 0000 0000 0000 // Nocontext
| 0b000 0000 0000 0000 0000 0000 0000 0010 // Rendercontext
------------------------------------------
  0b000 0000 0000 0000 0000 0000 0000 0010

此时可以结合找位与和 NoCconiext​ 来判断"是否处在某一上下文中":

js 复制代码
// 是否处在RenderContext上下文中,结果为true
(executionContext & RenderContext) !== NoContext
// 是否处在Commitcontext 上下文中,结果为 false
(executionContext & CommitContext) !== Nocontext

离开 RenderContext​ 上下文后,结合按位与、按位非移除标记:

js 复制代码
// 从当前上下文中移除 RenderContext 上下文
executionContext &= ~RenderContext;
// 是否处在 Rendercontext上下文中,结果为false
(executionContext & RenderContext) !== NoContext

执行过程如下:

js 复制代码
  0b000 0000 0000 0000 0000 0000 0000 0010 // executionContext
& 0b111 1111 1111 1111 1111 1111 1111 1101 // ~RenderContext
-------------------------------------------
  0b000 0000 0000 0000 0000 0000 0000 0000

三. 链表及相关数据结构

1. 链表

链表是一种数据结构,链表中的每个节点至少包含两个部分:数据域​和指针域​。其中数据域用来存储数据,而指针域用来存储指向下一个数据的地址,如下图所示:

  1. 链表中的每个节点至少包含两个部分:数据域和指针域
  2. 链表中的每个节点,通过指针域的值,形成一个线性结构
  3. 查找节点O(n),插入节点O(1),删除节点O(1)
  4. 不适合快速的定位数据,通过动态的插入会让删除数据的场景

模拟实现一个简单的链表:

  1. 首先定义一个Node
js 复制代码
 class Node{
     constructor(val) {
         // 数据域
         this.val = val
         // 指针域
         this.next = null
     }
 }
  1. 接下来实现添加和打印功能:
js 复制代码
 class LinkNodeList{
     constructor() {
         this.head = null
         this.length = 0
     }
     // 添加节点
     append(val) {
         // 使用数据创建一个节点node
         let node = new Node(val)
         let p = this.head
         if(this.head) {
             // 找到链表的最后一个节点,把这个节点的.next属性赋值为node
             while(p.next) {
                 p = p.next
             }
             p.next = node
         } else {
             // 如果没有head节点,则代表链表为空,直接将node设置为头节点
             this.head = node
         }
         this.length++
     }
     // 打印链表
     print() {
         if(this.head) {
             let p = this.head
             let ret = ''
 
             do{
                 ret += `${p.val} --> `
                 p = p.next
             }while(p.next)
             ret += p.val
         } else {
             console.log('empty')
         }
     }
 }
 
 let linkList = new LinkNodeList()
 
 linkList.append(1)
 linkList.append(2)
 linkList.append(3)
 linkList.append(4)
 
 linkList.print() // 1 --> 2 --> 3 --> 4
 
 console.log(linkList.length) // 4

对应到在react​中应用最为广泛的fiber​树,每个节点中按照child​,sibling​ 与子节点和兄弟节点进行连接:

如果有如下jsx​:

js 复制代码
function App() {
	return <div>
		<span>hi, react</span>
	</div>;
}

2. update

react​中每一次出触发更新,都会创建一个代表一次更新的数据结构 ------ Update​。

js 复制代码
// update
{
	action
}

而每一个update​都会被放置在一个新的数据结构UpdateQueue​中等待被消费:

js 复制代码
// updateQueue
{
	shared: {
		pending: null
	}
}

三. 副作用

副作用会让一个函数变的不纯,纯函数是根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。

在本文中指会造成DOM变动的操作,比如DOM的新增/修改。

四. 整体执行流程

1. render

render​阶段实际上是在内存中构建一棵新的fiber​树(称为workInProgress​树),构建过程是依照现有fiber​树(current​树)从root​开始深度优先遍历再回溯到root​的过程,这个过程中每个fiber​节点都会经历两个阶段:beginWork​和completeWork​。

beginWork​是向下调和的过程。就是由 fiberRoot 按照 child 指针逐层向下调和,而completeWork​是向上归并的过程,如果有兄弟节点,会返回 sibling​(同级)兄弟,没有返回 return​ 父级,一直返回到 FiebrRoot​。 组件的状态计算、diff​的操作以及render​函数的执行,发生在beginWork​阶段,effect​链表的收集、被跳过的优先级的收集,发生在completeWork​阶段。

构建workInProgress​树的过程中会有一个workInProgress​的指针记录下当前构建到哪个fiber​节点,这是React更新任务可恢复的重要原因之一。

期间还会有Scheduler​进行任务调度,以便高优先级的任务会被优先处理。 但是本文重点不在此,所以不会赘述,以后会写别的文章。

2. commit

render​阶段结束后,会进入commi​t阶段,该阶段不可中断,主要是去依据workInProgress​树中有变化的那些节点(render​阶段的completeWork​过程收集到的effect​链表),去完成DOM操作,将更新应用到页面上,除此之外,还会异步调度useEffect​以及同步执行useLayoutEffect​。

commit​ 细分可以分为三个阶段:

  • Before mutation 阶段:执行 DOM 操作前

没修改真实的 DOM ,是获取 DOM 快照的最佳时期,如果是类组件有 getSnapshotBeforeUpdate​,会在这里执行。

  • mutation 阶段:执行 DOM 操作

对新增元素,更新元素,删除元素。进行真实的 DOM 操作。

  • layout 阶段:执行 DOM 操作后

从盘古开天辟地开始

jsx与babel

首先来看一下在日常开发中离我们最近的一种使用形式,在编写页面代码的时候通常是使用jsx​的形式来编写的,比如创建一个App​组件:

js 复制代码
function App() {
	return (
		<div>hello</div>
	)
}

我们可以在jsx中很方便的控制dom的显示隐藏,进行流程控制,数据绑定,列表渲染,条件渲染等等。但是jsx本身并不是dom,它不能直接在浏览器中显示出来,而且浏览器也并不能直接处理jsx,所以中间必然会经过一系列的处理过程,用来将jsx处理为真实dom。

react​处理jsx代码时,会通过babel​直接转换为函数调用,将整个jsx代码拆解作为参数调用jsx函数(这里是一个函数名),然后调用ReactElement​ 函数生成element​(上文已经解释过)。至此,对于整个jsx的处理过程已经完成了。

接下来就以一段简单的代码为例,探索一下是怎么被转化为element​的;

js 复制代码
function App() {
	return (
		<div>123</div>
	)
}

首先<div>123</div>​这段代码会被babel插件@babel/plugin-transform-react-jsx​进行编译,在babel官网中可以查看到编译后的结果:

可以看到babel帮我们引入了一个jsx方法,而将其他的属性当作参数传入。这是在React17以后,提供的一种转换方式:引入jsx-runtime层。

而在React17之前,是将其直接编译位对React.createElement​方法的调用,也就是说编译后的结果与react的代码强关联:

假设源码如下:

  • React17之前
js 复制代码
import React from 'react';

function App() {
  return <h1>Hello World</h1>;
}

转换过程,会将上述JSX转换为如下的createElement​代码:

js 复制代码
import React from 'react';

function App() {
  return React.createElement('h1', null, 'Hello world');
}
  • React17之后
js 复制代码
function App() {
  return <h1>Hello World</h1>;
}

下方是新 JSX 被转换编译后的结果:

js 复制代码
// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';

function App() {
  return _jsx('h1', { children: 'Hello world' });
}

引入jsx-runtime层这种的核心在于:JSX编译出来的代码与React库本身进行了解耦,只将JSX转换为了与React无关的JS形式的调用描述,没有直接使用React.createElement​。引入了jsx-runtime这一层,屏蔽具体的调用细节,只专注JSX到JS代码最基础的映射。 至于这个_jsx​的具体实现,就是内部调用的是React.createElement​还是另一种createElement​,则可以由库内部来进行实现。 ‍

jsx​函数返回生成的element​对象,保存一个节点所属的属性,子级节点等信息:

js 复制代码
const REACT_ELEMENT_TYPE = supportSymbol
	? Symbol.for('react.element')
	: 0xeac7;
js 复制代码
element = {
	$$typeof: REACT_ELEMENT_TYPE,
	type,
	key,
	ref,
	props,
}

element​中$$typeof​这个属性是用来标记当前对象是一个ReactElement​,如果不是原生Symbol​类型或者填充,则使用普通数字来代替,这也区分了element​并非普通对象,而是特殊的ReactElement​。

那么接下来我们看一下jsx​函数是如何对babel编译后的结果进行解析的:

js 复制代码
export const jsx = function (type, config) {
	let key = null;
	const props = {};
	// ref属性单独保存
	let ref = null;
	// 遍历config
	for (const prop in config) {
		const val = config[prop];
		// key 与 ref属性单独进行保存
		if (prop === 'key') {
			if (val !== undefined) {
				key = '' + val;
			}
			continue;
		}

		if (prop === 'ref') {
			if (val !== undefined) {
				ref = val;
			}
			continue;
		}
		// 保存其他属性
		// hasOwnProperty意为只保存对象的自有属性
		if ({}.hasOwnProperty.call(config, prop)) {
			props[prop] = val;
		}
	}
	// 调用ReactElement方法生成element对象
	return ReactElement(type, key, ref, props);
};

babel​编译完成之后,会将节点类型传入jsx​函数的第一个参数type​,其他属性都会放在config​参数中。例如:

js 复制代码
// 一 单个DOM
// dom
<div class="box">
	123
</div>
// 对应的编译结果
import { jsx as _jsx } from "react/jsx-runtime";
_jsx("div", {
  class: "box",
  children: "123"
});


// 二 多个DOM
<div class='box'>
	<span>456</span>
</div>
// 对应的编译结果
import { jsx as _jsx } from "react/jsx-runtime";
_jsx("div", {
  class: "box",
  children: _jsx("span", {
    children: "456"
  })
});

jsx​函数处理完成后返回ReactElement​函数的处理结果:

js 复制代码
const ReactElement = function (type, key, ref, props) {
	const element = {
		$$typeof: REACT_ELEMENT_TYPE,
		type,
		key,
		ref,
		props
	};
	return element;
};

ReactElement​函数的逻辑很简单,直接创建一个对象,增加REACT_ELEMENT_TYPE​的标识, 然后保存传入的属性返回。

整体流程如下:

createRoot与render

以上这些都是babel​和react​在幕后帮我们已经处理好的工作,回想一下在我们的reatc​项目中是如何将jsx挂载在页面中的?

js 复制代码
function App() {
	return <div>hi,react</div>;
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

在入口函数中调用createRoot​函数将dom节点传入,创建整个应用的根节点。然后调用返回值中的render​函数挂载App组件。

注意!render​函数传入<App />​进行处理时,实际上是创建了一个以App​函数体为type​的element​对象。会被babel​编译为这样:

js 复制代码
// 注意此App为App函数体!
jsx(App, {})

生成的element​对象是这样子的:

js 复制代码
{
	$$typeof: REACT_ELEMENT_TYPE,
	type: function App(),
	key,
	ref,
	props
}

所以初始化的第一次调和实际上是处理<App />​ 的函数组件。

js 复制代码
export function createRoot(container) {
	// 创建根节点
	const root = createContainer(container);

	// 返回render函数,在挂载和处理element的时候调用
	return {
		render(element) {
			// 处理element的入口
			return updateContainer(element, root);
		}
	};
}
  • createContainer
  1. 创建根节点,创建hostRootFiberfiberRootNode,其中hostRootFiber为普通的fiber节点,而fiberRootNode作为整个应用的根节点,需要在更新完毕之后控制切换fiber树。
  2. 初始化更新队列,前面已经提到过,每次更新会生成一个update对象,而updateQueue用于保存该节点的更新队列。
js 复制代码
export function createContainer(container) {
	// 创建hostRootFiber
	const hostRootFiber = new FiberNode(HostRoot, {}, null);
	// 创建fiberRootNode
	const root = new FiberRootNode(container, hostRootFiber);
	// 初始化更新队列
	hostRootFiber.updateQueue = createUpdateQueue();
	return root;
}

FiberNode​ 这个构造函数上文中已经解释过了。值得注意的是,在创建fiber​树的根节点时,也就是hostRootFiber​节点,使用了自己单独的tag​:

js 复制代码
export const HostRoot = 3;

用于表示这个节点为fiber​树的根节点。

js 复制代码
// FiberRootNode
export class FiberRootNode {
	constructor(container, hostRootFiber) {
		this.container = container;
		this.current = hostRootFiber;
		hostRootFiber.stateNode = this;
		this.finishedWork = null;
	}
}

FiberRootNode​类中container​保存dom节点,current​指针指向当前currentfiber​树的头节点,同时当前currentfiber​树的头节点有一个指针stateNode​ 指向当前应用根节点,而this​在FiberRootNode​ 类的内部指向自身:

js 复制代码
export const createUpdateQueue = () => {
	return {
		shared: {
			pending: null
		}
	}
}

createUpdateQueue​ 函数创建一个更新队列。

  • render

在创建完根节点以及fiber​的头节点后,接下来就要进入到了开始生成fiber​树的工作中,render​函数中使用update​更新对象接入到整个初始化逻辑,将初始化的挂载看作一次更新逻辑:

js 复制代码
render(element) {
	// 处理element的入口
	return updateContainer(element, root);
}

js 复制代码
export function updateContainer(element, root) {
	// 获取fiber书的头节点
	const hostRootFiber = root.current;
	// 创建更新对象
	const update = createUpdate(element);
	enqueueUpdate(
		hostRootFiber.updateQueue,
		update
	);
	// 开始生成fiber树
	scheduleUpdateOnFiber(hostRootFiber);

	return element;
}

element​参数为编译后的element​对象,root​为创建的应用根节点。

updateContainer​函数总共做了两件事:

  1. 创建基于element对象的更新对象,将其加入根节点的更新队列
  2. 开始调度执行生成整棵fiber
js 复制代码
export const createUpdate = (action) => {
	return {
		action
	};
};
js 复制代码
export const enqueueUpdate = (updateQueue, update) => {
	updateQueue.shared.pending = update;
};

对于更新任务的操作,其实就是创建一个对象,然后加入目标节点的pending​属性中。对于fiber​树的根节点就相当于:

js 复制代码
// element为编译后的整个element对象
hostRootFiber.updateQueue.shared.pending = element;

scheduleUpdateOnFiber​ 函数开始生成fiber​树的流程。hostRootFiber​ 根节点当作参数传入,其自身保存着所有的子节点与所有的属性。

初始化的render流程

在触发更新的行为中,通常有两种方式

  1. 初始化mount
  2. 用户触发的页面变化

后续的更新可能发生于任意组件,而更新流程是从根节点递归的,所以每次触发的更新行为需要先从发生更新的节点向上寻找到根节点,然后从根节点开始生成一棵新的fiber​树,此时也就是workInProgress​树。一个统一的根节点保存通用信息。

js 复制代码
export function scheduleUpdateOnFiber(fiber) {
	// 查找应用根节点
	const root = markUpdateFromFiberToRoot(fiber);
	// 构造fiber树
	renderRoot(root);
}
js 复制代码
function markUpdateFromFiberToRoot(fiber: FiberNode) {
	let node = fiber;
	let parent = node.return;
	// 循环查找父节点
	while (parent !== null) {
		node = parent;
		parent = node.return;
	}
	// 如果当前查找到HostRootFiber,则取它的stateNode
	if (node.tag === HostRoot) {
		return node.stateNode;
	}
	return null;
}

接下来调用renderRoot​ 函数创建workInProgress​树,此时传入的参数root​为整个应用的根节点。

  • renderRoot
  1. 创建基于HostRootFiberworkInProgress
  2. 调用workLoop
js 复制代码
function renderRoot(root) {
	// 初始化workinProgress树
	preparereFreshStack(root);

	do {
		try {
			workLoop();
			break;
		} catch (e) {
			if (__DEV__) {
				console.warn('workLoop发生错误');
			}
		}
	} while (true);
}

这里使用do...while​循环来防止如果某一次处理出现错误被捕获后可以重新执行workLoop​函数,如果workLoop​ 函数顺利执行完毕,会使用break​终止循环,不会产生多余的处理。

js 复制代码
// 设置全局变量
let workInProgress = null;
function preparereFreshStack(root) {
	workInProgress = createWorkInProgress(root.current, {});
}

workInProgress​ 是一个全局变量,意为当前正在工作的fiber​节点。

由于此时我们的root​保存的是应用根节点,所以需要使用current​获取当前current​树的头节点,也就是HostRootFiber​。

在创建workInProgress​时,根据是否拥有alternate​判断是否处于更新/初始化流程,因为在首次初始化后会讲将alternate​指针指向对应的fiber​节点。在createWorkInProgress​ 的过程中,复用current​树的属性来创建对应的workInProgress​树的节点。

js 复制代码
export const createWorkInProgress = (current, pendingProps) => {
	let wip = current.alternate;
	// 判断是否为初始化流程
	if (wip === null) {
		// mount

		wip = new FiberNode(current.tag, pendingProps, current.key);
		wip.stateNode = current.stateNode;
		// 互相绑定alternate指针
		wip.alternate = current;
		current.alternate = wip;
	} else {
		// update
	}
	wip.type = current.type;
	wip.updateQueue = current.updateQueue;
	wip.child = current.child;
	wip.memoizedState = current.memoizedState;
	wip.memoizedProps = current.memoizedProps;

	return wip;
};
  • workLoop

workLoop​可以看做是一个加工器,每一个fiber​都需要传入进行加工。只要 workInProgress​ 不为 null​(还有需要调和的 fiber​),那么workLoop​ 会循环调用 performUnitOfWork​。

js 复制代码
function workLoop() {
	// 循环performUnitOfWork
	while (workInProgress !== null) {
		performUnitOfWork(workInProgress);
	}
}

在开始workLoop​流程之前,有必要先来看一下在workLoop​函数中处理和创建fiber​树的模型。

首先调用一个叫做workLoop​的工作循环去构建workInProgress​树,构建过程分为两个阶段:向下遍历和向上回溯,向下和向上的过程中会对途径的每个节点进行beginWork​和completeWork​。

React对每个节点进行beginWork​操作,进入beginWork​后,首先判断节点及其子树是否有更新,若有更新,则会在计算新状态和diff​之后生成新的Fiber​,然后在新的fiber​上标记flags​(effectTag​),最后return​子节点,以便继续针对子节点进行beginWork​。若它没有子节点,则返回null​,这样说明这个节点是末端节点,可以进行向上回溯,进入completeWork​阶段。

比如现在有如下DOM结构:

text 复制代码
 div
  |
  |
 span ---- p
		   |
		   |
	   'hi,react'

那么此时整个执行流程为:

以上fiber​树的构建顺序为:

js 复制代码
HostRootFiber beginWork 执行
App组件 beginWork 执行
div beginWork 执行
span beginWork 执行
span completeWork 执行
p beginWork 执行
text beginWork 执行
text completeWork 执行
p completeWork 执行
div completeWork 执行
App组件 completeWork 执行
HostRootFiber completeWork 执行
js 复制代码
function performUnitOfWork(fiber) {
	// 执行beginWork
	const next = beginWork(fiber);
	fiber.memoizedProps = fiber.pendingProps;
	if (next === null) {
		// 执行completeWork
		completeUnitOfWork(fiber);
	} else {
		// 存在子节点,继续向下处理
		workInProgress = next;
	}
}

beginWork​它的返回值有两种情况:

  • 返回当前节点的子节点,然后会以该子节点作为下一个工作单元继续beginWork,不断往下生成fiber节点,构建workInProgress树。
  • 返回null,当前fiber子树的遍历就此终止,从当前fiber节点开始往回进行completeWork

beginWork

**beginWork**函数是通过当前节点生成子级**fiber**的过程。

beginWork​流程主要任务:

  • 生成fiber
  • 标记dom节点的新增/修改

fiber​节点有多种类型,在前面我们定义了HostRoot​ 根节点,HostText​ 文本节点,HostComponent​ dom节点,FunctionComponent​函数组件节点。需要分别处理。

FunctionComponent​类型因为会涉及到更新和hook​,所以在本文mount​流程暂时不会实现。

文本节点的更新只需要替换文本内容,所以无需要特殊处理。

js 复制代码
export const beginWork = (wip) => {
	// 返回子fiberNode
	switch (wip.tag) {
		case HostRoot:
			// 处理根节点类型
			return updateHostRoot(wip);
		case HostComponent:
			// 处理根dom节点
			return updateHostComponent(wip);
		case HostText:
			return null;
			// 处理函数组件节点
		case FunctionComponent:
			return updateFunctionComponent(wip);
		default:
			if (__DEV__) {
				console.warn('beginWork未实现的类型');
			}
			break;
	}
	return null;
};

首先进入beginWork​流程的是根节点HostRootFiber​,他的tag​类型是HostRoot​,所以首先执行updateHostRoot​ 函数对根节点进行处理:

  • 根节点

对于根节点HostRootFiber​,需要取出他的子节点生成fiber​,还记得创建根节点的时候我们的整个element​节点被保存到了哪里?updateQueue​。

pending​对象里面保存着整个element​对象,初始化时创建的element​对象也被看作是一次更新,所以在updateHostRoot​函数中,就需要对其进行处理。

js 复制代码
function updateHostRoot(wip) {
	// 获取element对象
	const baseState = wip.memoizedState;
	const updateQueue = wip.updateQueue;
	const pending = updateQueue.shared.pending;
	// 一次更新完成后,置空更新队列
	updateQueue.shared.pending = null;
	// 执行更新
	const { memoizedState } = processUpdateQueue(baseState, pending);

	// 保存执行结果
	wip.memoizedState = memoizedState;
	// 获取子级
	const nextChildren = wip.memoizedState;
	// 调和子节点
	reconcilerChildren(wip, nextChildren);
	return wip.child;
}

processUpdateQueue​函数用于执行在fiber​节点中保存的更新逻辑,返回值为执行结果。

reconcilerChildren​ 函数用于调和子节点,需要两个参数,当前fiber​节点和后续子级。值得注意的是,由于根节点的更新队列保存的是element​,所以processUpdateQueue​函数的返回值就是当前fiber​节点的子级。

js 复制代码
export const processUpdateQueue = (baseState, pendingUpdate) => {
	// 定义返回结果
	const result = {
		memoizedState: baseState
	};

	if (pendingUpdate !== null) {
		const action = pendingUpdate.action;
		// 如果更新任务为函数,则执行函数
		if (action instanceof Function) {
			result.memoizedState = action(baseState);
		} else {
			result.memoizedState = action;
		}
	}

	return result;
};

processUpdateQueue​​ 的实现比较简单,判断更新任务是否为函数,如果是函数,则传入初始值,并执行函数。最后更新​memoizedState​​ 。

reconcilerChildren​ 函数中使用alternate​ 来判断是否为初始化,首次渲染后依照双缓存树的逻辑会将与之对应的fiber​节点赋值到alternate​属性中。所以alternate​属性是否为null​,也就成为是否为更新流程的依据。

js 复制代码
function reconcilerChildren(wip, children) {
	// 获取alternate属性
	const current = wip.alternate;

	if (current !== null) {
		// update 更新
		wip.child = reconcilerChildFibers(wip, current.child, children);
	} else {
		// mount
		wip.child = mountChildFibers(wip, null, children);
	}
}

childReconciler​ 函数接受一个参数标识是否追踪副作用,关于这个参数会在后续更新的章节发挥作用,在本次初始化流程暂时标识为不追踪。

childReconciler​ 函数返回reconcilerChildFibers​函数用于针对不同类型的子节点进行处理。

子节点为对象类型则将其作为element​对象进行处理。

子节点为字符串类型则将其作为文本进行处理。

如果子节点为element​对象,则判断$$typeof​的类型,目前的element​对象类型只设置了REACT_ELEMENT_TYPE​ 类型,所以针对element​对象会进入reconcilerSingleElement​ 函数进行处理。(目前只针对单节点处理)

js 复制代码
// 更新 需要追踪副作用
export const reconcilerChildFibers = childReconciler(true);
// 初始化新增
export const mountChildFibers = childReconciler(false);
// 接收标识,是否追踪副作用
function childReconciler(shouldTrackEffects) {
	return function reconcilerChildFibers(
		returnFiber,
		currentFiber,
		newChild
	) {
		// 判断当前子节点类型
		// element
		if (typeof newChild === 'object' && newChild !== null) {
			switch (newChild.$$typeof) {
				case REACT_ELEMENT_TYPE:
					return placeSingleChild(reconcilerSingleElement(returnFiber, currentFiber, newChild))
				default:
					if (__DEV__) {
						console.warn('未实现的reconciler类型');
					}
			}
		}
		// 文本
		if (typeof newChild === 'string' && newChild !== null) {
			return placeSingleChild(reconcilerSingleTextNode(returnFiber, currentFiber, newChild))
		}

		if (__DEV__) {
			console.warn('未实现的reconciler类型', newChild);
		}

		return null;
	};
}

在生成子节点的fiber​后,还需要将此fiber​节点增加"新增"的标记,也就是在fiber​对象的flags​属性中赋值Placement​(新增)的标记,用于后续针对不同的操作类型对dom进行不同的处理。

js 复制代码
function placeSingleChild(fiber) {
	if (shouldTrackEffects && fiber.alternate === null) {
		fiber.flags |= Placement;
	}
	return fiber;
}

可以看到在placeSingleChild​ 函数中,被标记新增的条件alternate​ 需要为null​,shouldTrackEffects​ 需要为true​。

但是这就会有一个问题了,我们执行初始化渲染的时候alternate​属性肯定全都为null​,而且是否追踪副作用,也就是shouldTrackEffects​ 也是根据alternate​属性是否为null​来确定的,那岂不是在整个初始化阶段都无法标记flags​了?

不知道大家还记不记得在基于HostRootFiber​来生成本次workInProgress​的头节点的时候,两者的alternate​属性已经相互被绑定了:

这其实也是一种优化手段,如果是首次渲染流程,只需要给根节点标记"新增"的flags​即可,随后将生成的dom整体挂载到页面上,避免给每一个fiber​节点标记"新增"的副作用,增加额外的操作。

由于在处理流程中是生成子级fiber​,所以当前生成的fiber​节点的retrun​属性直接指向当前的fiber​节点。

js 复制代码
function reconcilerSingleElement(returnFiber, currentFiber, element) {
		const key = element.key;
		const fiber = createFiberFromElement(element);
		// 绑定retrun
		fiber.return = returnFiber;
		return fiber;
	}

注意在定义fiber​节点类型的时候需要注意,在生成函数组件的element​ 对象时,其type​会被赋值为整个函数组件的函数体,此时element​对象的type​属性为函数类型,代表该节点为函数组件。

而在HostRootFiber​节点处理时生成<App />​组件的fiber​节点的tag​为FunctionComponent​。

js 复制代码
export function createFiberFromElement(element) {
	const { type, key, props } = element;
	// 默认为函数组件类型
	let fiberTag = FunctionComponent;

	if (typeof type === 'string') {
		fiberTag = HostComponent;
	} else if (typeof type !== 'function') {
		console.log('为定义的type类型', element);
	}
	// 创建fiber节点
	const fiber = new FiberNode(fiberTag, props, key);
	fiber.type = type;
	return fiber;
}

此时创建fiber​节点时,将element​对象的props​属性当作第二个参数传入FiberNode​类,所以当前fiber​节点的子级都被保存在了该fiber​节点的pendingProps​ 属性中。

再来回顾一下通过babel​生成的element​对象:

js 复制代码
_jsx("div", {
  class: "box",
  children: _jsx("span", {
    children: "456"
  })
});

通过jsx​函数处理后的props​包含class​和children​属性。

此时针对非文本节点的element​对象就处理完成了,接下来文本类型也很简单:

js 复制代码
function reconcilerSingleTextNode(
		returnFiber,
		currentFiber,
		content
	) {
		const fiber = new FiberNode(HostText, { content }, null);
		fiber.return = returnFiber;
		return fiber;
	}

因为文本节点中的文本无论是创建还是更新都只是单纯的替换,FiberNode​类第二个参数接收节点的props​,会被赋值为pendingProps​ ,也就是工作时的props​,所以直接将文本内容直接作为content​ 属性的内容值创建文本类型的fiber​节点。

  • 函数组件

函数组件需要完成的最重要的任务就是执行保存在type​中的函数,获取该节点的后续子级的element​对象。

js 复制代码
function updateFunctionComponent(wip) {
	// 获取子级element
	const nextChildren = renderWithHooks(wip);
	// 生成子级fiber节点
	reconcilerChildren(wip, nextChildren);
	// 返回子级
	return wip.child;
}

执行函数,传入props​参数,这也就是为什么可以在子级组件通过props​来获取父级的属性。

js 复制代码
export function renderWithHooks(wip) {
	const Component = wip.type;
	const props = wip.pendingProps;
	// 执行函数
	const children = Component(props);

	// 重置操作
	return children;
}
  • 普通DOM

对于普通的dom节点的处理直接在pendingProps​属性取出节点的子级然后执行reconcilerChildren​ 即可。

js 复制代码
function updateHostComponent(wip) {
	// element对象的props被保存到了pendingProps中
	const nextProps = wip.pendingProps;
	const nextChildren = nextProps.children;

	reconcilerChildren(wip, nextChildren);
	return wip.child;
}

至此一次完整的的beginwork​流程初始化就结束了。

completeWork

当前fiber​节点如果存在子节点,继续将子节点赋值给workInProgress​(当前正在工作的fiber​节点),执行子节点的beginWork​流程。

如果没有子节点,也就是执行beginwork​完毕返回值为null​,因为react​遍历方式是深度优先遍历,就代表当前分支已经处理完成,就会执行该节点的completeWork​函数。

js 复制代码
function performUnitOfWork(fiber) {
	const next = beginWork(fiber);
	fiber.memoizedProps = fiber.pendingProps;
	if (next === null) {
		// 开始执行completeWork
		completeUnitOfWork(fiber);
	} else {
		workInProgress = next;
	}
}

js 复制代码
function completeUnitOfWork(fiber) {
	let node = fiber;

	do {
		// 执行当前节点completeWork
		completeWork(node);
		// 执行完毕查找兄弟节点
		const sibling = node.sibling;
		if (sibling !== null) {
			workInProgress = sibling;
			return;
		}
		// 兄弟节点为null,寻找父级
		node = node.return;
		// 代表当前工作的fiber节点为父节点
		workInProgress = node;
	} while (node !== null);
}

当一个节点的completeWork​函数也执行完毕后,首先会寻找当前fiber​的兄弟节点,执行兄弟节点的beginWork​。

如果没有兄弟节点,则寻找父节点,执行父节点的completeWork​,直到回溯到根节点,完成整棵fiber​树的处理。

completeWork​节点主要的任务有:

  • 创建基于fiber节点的真实DOM节点,插入到父级的stateNode属性中,等待commit节点挂载到页面中。
  • 依次收集副作用标记到上级,一层一层的向上收集,处理更新时可以非常方便的获知哪一个分支发生了更新或者删除的变动。
js 复制代码
export const completeWork = (wip) => {
	// node
	const newProps = wip.pendingProps;
	const current = wip.alternate;
	switch (wip.tag) {
		case HostComponent:
			if (current !== null && wip.stateNode) {
				// update
				// 更新流程
			} else {
				// mount
				// 构建DOM
				const instance = createInstance(wip.type, newProps);
				// 将根据fiber生成的真实dom赋值给stateNode
				wip.stateNode = instance;
				appendAllChildren(instance, wip);
			}
			// 向上收集副作用
			bubbleProperties(wip);
			return null;
		case HostText:
			if (current !== null || wip.stateNode) {
				// update
				// 更新流程
			} else {
				// mount
				// 构建DOM
				const instance = createTextInstance(newProps.content);
				// 将根据fiber生成的真实dom赋值给stateNode
				wip.stateNode = instance;
			}
			// 向上收集副作用
			bubbleProperties(wip);
			return null;
		case HostRoot:
			// 向上收集副作用
			bubbleProperties(wip);
			return null;
		case FunctionComponent:
			// 向上收集副作用
			bubbleProperties(wip);
			return null;
		default:
			if (__DEV__) {
				console.log('未被complete处理的节点', wip);
			}
			break;
	}
};

completeWork​ 函数中我们依然需要区别类型进行处理,在执行生成真实DOM的流程时,只需要处理HostComponent​和HostText​ 这两种类型,只有这两种类型对应真实DOM节点div​,span​,p​...和文本类型。而FunctionComponent​和HostRoot​仅仅是我们处理Fiber​树时创建的fiber​节点,在浏览器中并没有相对应的节点类型,因此只需要收集副作用。

  • HostComponent
js 复制代码
// 构建DOM
const instance = createInstance(wip.type, newProps);
// 将根据fiber生成的真实dom赋值给stateNode
wip.stateNode = instance;
appendAllChildren(instance, wip);

createInstance​根据type​创建DOM,然后更新props​。

js 复制代码
export function createInstance(type, props) {
	// 处理props
	const element = document.createElement(type);

	return element;
}

completeWork​函数的处理流程是回溯的过程在处理当前节点时,意味着所有当前节点的子节点(tag​类型为HostComponent​或者HostText​)都已经创建完真实DOM,而当前正在处理的HostComponent​ 节点必然是处于当前fiber​分支最顶端的节点,所以接下来将所有子节点都插入到当前节点的stateNode​属性。

js 复制代码
const appendAllChildren = (parent, wip) => {
	let node = wip.child;

	while (node !== null) {
		// 如果当前为HostComponent或者HostText,插入到父级stateNode
		if (node.tag === HostComponent || node.tag === HostText) {
			appendInitialChild(parent, node.stateNode);
		// 跳过其他非HostComponent或者HostText节点
		} else if (node.child !== null) {
			// 保持父节点连接
			node.child.return = node;
			node = node.child;
			continue;
		}
		// 处理到本次最高级fiber节点,退出
		if (node === wip) {
			return;
		}

		while (node.sibling === null) {
			// 是否已经回到原点
			if (node.return === null || node.return === wip) {
				return;
			}
			node = node.return;
		}
		// 查找兄弟节点
		node.sibling.return = node.return;
		node = node.sibling;
	}
};

我们在插入DOM节点的时候需要处理的只有HostComponent​或者HostText​类型,因为有如下情况:

js 复制代码
// 函数组件
function Box() {
	return (
		<p>123</p>
	)
}


// dom
<div>
	<span></span>
	<Box />
</div>

当我们在为div​节点寻找子节点时,<Box />​显然是无法插入的,因为它是一个函数组件,所以我们就要向下查找第一个类型为HostComponent​或者HostText​类型的子节点,也就是p​节点。终止本次循环,然后开始下一次循环。

  • 如果当前节点node​与wip​相同,说明已经处理到最顶层的节点了,结束处理。

  • 如果没有查找到符合条件的子节点

    • 没有兄弟节点,则开始向上回溯,找到拥有兄弟节点的fiber​节点

    • 有兄弟节点,处理兄弟节点

将子节点插入到父节点的stateNode​属性。

js 复制代码
export function appendInitialChild(parent, child) {
	parent.appendChild(child);
}

  • HostText

文本节点的处理就比较简单了,直接创建文本的DOM节点,赋值给stateNode​属性:

js 复制代码
export function createTextInstance(content) {
	return document.createTextNode(content);
}

  • bubbleProperties 收集副作用

completeWork​函数是一个不断回溯的过程,所以这就方便了将子级同层的副作用标识flags​使用位运算逐层统一挂载到父级的属性subtreeFlags​ 中。为什么要向上收集副作用呢?这是为了方便在处理上层节点的时候可以更快的获知在该分支中有需要触发的更新。

现在一个fiber​节点有两个副作用相关的属性:

flags​:当前fiber​节点自身的副作用标记

subtreeFlags​:所有子节点副作用标记的集合

假设有如下fiber​树:

text 复制代码
div
 |
div -- span -- p
 |
div -- div -- p

那么收集副作用的流程为:

js 复制代码
function bubbleProperties(wip) {
	// 初始化
	let subtreeFlags = NoFlags;
	let child = wip.child;
	// 逐层收集
	while (child !== null) {
		// 收集副作用标识
		subtreeFlags |= child.subtreeFlags;
		subtreeFlags |= child.flags;
		// 寻找兄弟节点
		child.return = wip;
		child = child.sibling;
	}

	wip.subtreeFlags |= subtreeFlags;
}

初始化的commit流程

commit​阶段的主要任务是

  1. fiber树的切换
  2. 根据fiber节点的stateNode属性插入到浏览器中完成渲染。
  3. 执行Placement相关的操作
js 复制代码
function renderRoot(root) {
	// 初始化
	preparereFreshStack(root);

	do {
		try {
			workLoop();
			break;
		} catch (e) {
			workInProgress = null;
		}
	} while (true);
	// 保存处理完成后的整棵fiber树
	const finishedWork = root.current.alternate;
	root.finishedWork = finishedWork;
	// 开始commit阶段
	commitRoot(root);
}

commitRoot​函数是整个commit​阶段的起点,传入FiberRootNode​节点。

如何判断当前是哪一个子阶段需要执行的操作,如果当前fiber​节点的subtreeFlags​(子级副作用)或者flags​ (节点自身副作用)存在对应的副作用标记,则需要执行commit​阶段的mutation​子阶段。如果不存在,则没有必要进行处理,这样就节省了逐层遍历查找的性能消耗,这也是收集副作用标记的作用。

js 复制代码
export const Placement = 0b0000001;
export const Update = 0b0000010;
// MutationMask标记为Placement和Update的集合
export const MutationMask = Placement | Update;

js 复制代码
// 验证是否具有新增/更新的副作用
const subtreeHasEffect = (finishedWork.subtreeFlags & MutationMask) !== NoFlags;

const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;

finishedWork​ 属性是本次更新处理完成生成的workInProgress​树。 这里还需要注意在处理完整个commit​流程之后,需要切换current​双缓存树,将current​指针切换为workInProgress​。

切换的时机在mutation​ 之后,layout​ 之前。

js 复制代码
function commitRoot(root) {
	const finishedWork = root.finishedWork;

	if (finishedWork === null) return;

	// 本次更新dom后重置finishedWork
	root.finishedWork = null;

	// 三个子阶段分别执行的操作
	const subtreeHasEffect =
		(finishedWork.subtreeFlags & MutationMask) !== NoFlags;

	const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;

	if (subtreeHasEffect || rootHasEffect) {
		// beforeMutation
		// mutation
		commitMutationEffects(finishedWork);
		// 处理完成后切换current
		root.current = finishedWork;
		// layout
	} else {
		root.current = finishedWork;
	}
}

首先还是依据副作用标识查找当前节点分支最底层的具有更新标识的节点(新增/修改)。定义全局变量nextEffect​ 代表子节点。

  • 如果当前节点具有副作用标识并且存在子节点,继续向下查找
  • 否则此节点已经为当前节点分支的最后一个具有副作用标识的节点,则开始回溯,向上遍历

从该分支具有副作用标识的最底部开始向上处理。

js 复制代码
let nextEffect = null;
export function commitMutationEffects(finishedWork) {
	nextEffect = finishedWork;

	while (nextEffect !== null) {
		// 向下遍历
		const child: FiberNode | null = nextEffect.child;

		if (
			(nextEffect.subtreeFlags & MutationMask) !== NoFlags &&
			child !== null
		) {
			nextEffect = child;
		} else {
			// 向上遍历
			up: while (nextEffect !== null) {
				commitMutationEffectsOnFiber(nextEffect);
				// 处理兄弟节点
				const sibling = nextEffect.sibling;

				if (sibling !== null) {
					nextEffect = sibling;
					break up;
				}
				// 兄弟节点为null,则继续向上遍历
				nextEffect = nextEffect.return;
			}
		}
	}
}

commitMutationEffectsOnFiber​函数中针对不同的副作用标识调用不同的处理函数,这里首次渲染只涉及到Placement​,所以只处理新增的副作用逻辑。处理完成后将副作用标记从当前节点的flags​属性中删除。

js 复制代码
const commitMutationEffectsOnFiber = (finishedWork) => {
	const flags = finishedWork.flags;
	// fiber Placement
	if ((flags & Placement) !== NoFlags) {
		commitPlacement(finishedWork);
		// 处理完成,删除Placement标记
		finishedWork.flags &= ~Placement;
	}
	// fiber Update
};

首先需要获取当前节点的父节点DOM。

js 复制代码
const commitPlacement = (finishedWork) => {

	// parent DOM
	// 获取父节点DOM
	const hostParent = getHostParent(finishedWork);

	if (hostParent !== null) {
		// 开始插入
		insertOrAppendPlacementNodeIntoContainer(finishedWork, hostParent);
	}
};

在获取父节点时,依然要通过循环的方式来获取,因为父节点依然有可能是函数组件节点等非DOM类的fiber​。

js 复制代码
function Box() {
	return (
		// 需要循环向上寻找真正的父节点div
		<div><div>
	)
}

<div>
	<Box />
</div>

‍ 在向上查找父节点的处理过程中,只需要获取HostComponent​ 和HostRoot​ 类型,通过HostRoot​ 根节点来获取初始化的跟节点。不包含HostText​类型节点,因为HostText​虽然是DOM节点,但是他不拥有子节点,所以在寻找父节点的过程中不会对其进行处理。

在获取初始化项目时的跟节点需要先通过parent.stateNode​ 来获取FiberRootNode​,然后获取container​ 。

js 复制代码
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
js 复制代码
function getHostParent(fiber) {
	let parent = fiber.return;

	while (parent) {
		const parentTag = parent.tag;
		// HostComponent DOM节点类型
		if (parentTag === HostComponent) {
			return parent.stateNode;
		}
		// HostRoot 根节点
		if (parentTag === HostRoot) {
			return parent.stateNode.container;
		}
		parent = parent.return;
	}

	return null;
}

同理,当前节点的子节点在进行插入时,也需要判断当前节点是否为真实DOM节点,这里需要判断是否为HostComponent​ 或 HostText​类型。当满足条件时插入到父节点的stateNode​。

js 复制代码
function insertOrAppendPlacementNodeIntoContainer(
	finishedWork,
	hostParent,
) {
	// fiber host
	// 当前节点为真实DOM节点
	if (finishedWork.tag === HostComponent || finishedWork.tag === HostText) {
		appendChildToContainer(hostParent, finishedWork.stateNode);
		return;
	}
	// 向下查找
	const child = finishedWork.child;
	if (child !== null) {
		insertOrAppendPlacementNodeIntoContainer(child, hostParent);
		// 处理兄弟节点
		let sibling = child.sibling;
		while (sibling !== null) {
			insertOrAppendPlacementNodeIntoContainer(sibling, hostParent);
			sibling = sibling.sibling;
		}
	}
}

js 复制代码
// 插入
export function appendChildToContainer(
	parent,
	child
) {
	parent.appendChild(child);
}

目前首次渲染的commit​阶段就是根据在render​阶段所标记的副作用标记来对整个fiber​树中的真实DOM节点进行挂载的过程。

至此首次渲染的简单实现就完成了。

写在最后

未来可能会更新实现mini-reactantd源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳

相关推荐
新缸中之脑9 分钟前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
hmz85612 分钟前
最新网课搜题答案查询小程序源码/题库多接口微信小程序源码+自带流量主
前端·微信小程序·小程序
看到请催我学习18 分钟前
内存缓存和硬盘缓存
开发语言·前端·javascript·vue.js·缓存·ecmascript
blaizeer1 小时前
深入理解 CSS 浮动(Float):详尽指南
前端·css
速盾cdn1 小时前
速盾:网页游戏部署高防服务器有什么优势?
服务器·前端·web安全
小白求学11 小时前
CSS浮动
前端·css·css3
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(POST)
前端·csrf
XiaoYu20022 小时前
22.JS高级-ES6之Symbol类型与Set、Map数据结构
前端·javascript·代码规范
golitter.2 小时前
Vue组件库Element-ui
前端·vue.js·ui
儒雅的烤地瓜2 小时前
JS | JS中判断数组的6种方法,你知道几个?
javascript·instanceof·判断数组·数组方法·isarray·isprototypeof