公司的技术栈是 react,但是其实使用了这么久的 react,对于 react 的实现原理还是两眼一抹黑,于是一个月来都在通过各种文章视频来了 react 的底层实现,整理总结出 react fiber 架构的具体实现,要讲清 react fiber 的架构还是挺不容易的,个人认为 react fiber 架构比较复杂,所以本篇文章应该要持续更新一段时间
createElement
在执行 react 的整个流程前,首先要解析一下jsx,在 react 项目的开发中,通常要引入 react:
import React from 'React';
但是,我们并没有用到React上的什么方法,但是为啥要引入呢??原因就是因为jsx在babel编译过后会变成React.createElement,引入React,主要是预防找不到React而产生报错。所以,实现React要从先实现createElement开始
我们来看一下这样一段代码:
const title = <h1 className="title">Hello, world!</h1>;
这段代码并不是合法的 js 代码,它是jsx的语法扩展,通过它我们就可以很方便的在 js 代码中书写 html 片段。
本质上,jsx 是语法糖,上面这段代码会被babel转换成如下代码:
const title = React.createElement(
'h1',
{ className: 'title' },
'Hello, world!'
);
本质上来说 JSX 是React.createElement(component, props, ...children)方法的语法糖。
当我们有如下的 jsx 结构时:
let element1 = (
<div id="A1" style={style}>
A1文本
<div id="B1" style={style}>
B1文本
<div id="C1" style={style}>
C1文本
</div>
<div id="C2" style={style}>
C2文本
</div>
</div>
<div id="B2" style={style}>
B2文本
</div>
</div>
);
通过babel的转换,会变成这样:
React.createElement(
'div',
{ id: 'A1' },
React.createElement(
'div',
{ id: 'B1' },
'B1文本',
React.createElement('div', { id: 'C1' }, 'C1文本'),
React.createElement('div', { id: 'C2' }, 'C2文本')
),
React.createElement('div', {
id: 'B2',
})
);
那么,现在就来自己实现一下createElement处理方法
新建react.js文件,编写方法
/**
* 创建元素(虚拟DOM)的方法
* @param {*} type 元素的类型 div span p
* @param {*} config 配置对象 属性 key ref
* @param {...any} children 所有的儿子,传入一个数组,如果没有儿子,如<div/>,则为空数组
*/
function createElement(type, config, ...children) {
return {
type,
props: {
...config, //属性扩展 id、key、style
children: children.map((child) => {
//兼容处理,如果是普通元素返回自己,如果是文本类型,返回文本元素对象
//比方说B1文本那么children就是["B1文本"],改为了
//{type:Symbol(ELEMENT_TEXT),props:{text:"B1文本",children:[]}}也不可能有children了
return typeof child === 'object'
? child
: {
type: 'ELEMENT_TEXT',
props: { text: child, children: [] },
};
}),
},
};
}
const React = {
createElement,
};
export default React;
完成了createElement,在index.js文件中导入,并查看jsx处理完的结果
import React from './react';
let style = { border: '3px solid red', margin: '5px' };
let element1 = (
<div id="A1" style={style}>
A1文本
<div id="B1" style={style}>
B1文本
<div id="C1" style={style}>
C1文本
</div>
<div id="C2" style={style}>
C2文本
</div>
</div>
<div id="B2" style={style}></div>
</div>
);
console.log(element1);
打印结果如下,由于层级过多,有的就不展开了:
这样,完整的节点jsx对象结构就生成出来了,这就是react的virtual-dom
ReactDOM.render
有了虚拟 dom,就可以使用ReactDOM.render方法把虚拟 dom 挂载到页面上了,该方法创建了一个根节点(rootFiber),把刚刚解析过的虚拟 dom 挂载在该节点下
// 新建react-dom.js
/**
* render是要把一个元素渲染到一个容器内部
* @param {*} element 元素
* @param {*} container 容器
*/
function render(element, container) {
let rootFiber = {
tag: TAG_ROOT, //每个virtual-dom会有一个tag标示此元素类型
stateNode: container,
props: { children: [element] },
};
scheduleRoot(rootFiber);
}
const ReactDOM = {
render,
};
export default ReactDOM;
//index.js中
import ReactDOM from './react-dom';
ReactDOM.render(element1, document.querySelector('#root'));
可以看到,ReactDOM.render中构建了一个 root 节点,把刚刚的 virtual-dom 挂在这个 root 节点下,然后调用scheduleRoot进行调度,fiber 就是在scheduleRoot调度的过程中产生的
既然本文是讨论 fiber,那什么是 fiber??
Fiber
在了解什么是 fiber 之前,我们应该要了解一下为什么会出现 fiber
React 15 架构
每当有更新发生时,Reconciler(协调器)会做如下工作:
- 调用函数组件、或 class 组件的 render 方法,将返回的JSX 转化为虚拟 DOM
- 将虚拟 DOM 和上次更新时的虚拟 DOM 对比
- 通过对比找出本次更新中变化的虚拟 DOM
- 通知
Renderer将变化的虚拟 DOM 渲染到页面上
对于 React 的更新来说,递归遍历应用的所有节点由于递归执行,计算出差异,然后再更新 UI。递归是不能被打断的,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了 16ms(小于 60 帧),用户交互就会卡顿。
React 16 的设计思想
React 16 实现的思路是这样的:将运算切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。这就是 React 16 中的 Fiber 设计思想。
为了达到能中断处理这种效果,就需要有一个调度器 (Scheduler) 来进行任务分配,Fiber Reconciler 在执行过程中,会分为 2 个阶段:
- 1、阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。
- 2、阶段二,将需要更新的节点一次性批量更新,这个过程不能被打断。
阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。
Fiber 的实现细节
Fiber Reconciler 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。
每个 Fiber 节点有个对应的 React element,多个 Fiber 节点是如何连接形成树呢?靠如下三个属性
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
举个例子,如下的组件结构:
function App() {
return (
<div>
i am
<span>KaSong</span>
</div>
);
}
对应的Fiber结构为:
用数据结构来描述则为
const newFiber = {
tag:null, //组件类型 CLASS、FUNCTION_COMPONENT、TEXT、HOST等
type:null //createElement时接收的节点类型type,如果是原生的节点类型,那么就是一个字符串(div、span等),如果是一个组件(我们自己定义的、或者React内置提供的)那么就是一个变量
props: null, //虚拟dom属性,如style
stateNode: null, //如果是原生的节点类型,是虚拟dom对应的真实节点,如果是class/func组件,则是组件实例
updateQueue: null, // 数据更新队列
return: null, //父fiber
alternate: null, //捞fiber,用于新旧fiber对比更新
effectTag: null, //副作用标示,render会收集副作用 增加 删除 更新
nextEffect: null, //fiber节点组合成effectlist链表,指向链表当前节点的下一个节点
};
如何实现浏览器控制权切换?
react 16 的可中断调度,其实就是基于浏览器的 requestIdleCallback这个 api,在每个帧的空闲时间来处理fiber,
每个帧的开头包括样式计算、布局和绘制,JavaScript 执行, Javascript 引擎和页面渲染引擎在同一个渲染线程,GUI 渲染和 Javascript 执行两者是互斥的,如果某个任务执行时间过长,浏览器会推迟渲染。
假如某一帧里面要执行的任务不多,在不到 16ms(1000ms/60fps)的时间内就完成了上述任务的话,那么这一帧就会有一定的空闲时间,这段时间就恰好可以用来执行requestIdleCallback的回调
由于requestIdleCallback利用的是帧的空闲时间,所以就有可能出现浏览器一直处于繁忙状态,导致回调一直无法执行,这其实也并不是我们期望的结果,那么这种情况我们就需要在调用requestIdleCallback的时候传入第二个配置参数timeout了
// timeout指定多少毫秒之后若浏览器还是没空,则不等了,强制执行
requestIdleCallback(workFunction, { timeout: 2000 });
知道了requestIdleCallback的用法,就可以来看一段示例:
/**
* 函数fn接受deadline对象作为参数,deadline对象有两个属性:timeRemaining和didTimeout。
* timeRemaining() 返回当前帧还剩余的毫秒数。
* didTimeout 指定的时间是否过期。
*/
function workFunction(deadline) {
while (
(deadline.timeRemaining() > 0 || deadline.didTimeout) &&
taskList.length > 0
) {
//如果当前帧还有剩余时间,或超时了但是任务还没执行完,就执行任务
doWork();
}
// 如果当前帧没有剩余时间了,就把控制权还给浏览器,在下个帧继续执行任务
if (taskList.length > 0) {
requestIdleCallback(workFunction);
}
}
// 执行requestIdleCallback,workFunction是要执行的函数
requestIdleCallback(workFunction, 2000);
我们可以看到,taskList就是待执行的任务列表,而fiber调度的实现就是把fiber变成一个链表,按链表顺序执行,时间不够就把链表的下个节点保存等到下一帧执行
schedule 过程发生了什么?
了解完 fiber,接下来就来完成 fiber 的实现
react 从根节点开始渲染和调度,分为两个阶段 : diff+render 阶段,commit 阶段
diff+render 阶段
diff+render 阶段 对比新旧虚拟 DOM,进行增量更新或创建,花时间长,可进行任务拆分,此阶段可暂停
render 阶段两个任务:
- 1.根据虚拟 DOM 生成 fiber 树
- 2.收集 effectlist
render 阶段的成果是 effectlist, 即知道哪些节点更新哪些节点增加删除了
commit 阶段
commit 阶段,根据 render+diff 阶段的成果(effectlist),进行 DOM 更新创建,此阶段不能暂停
下面通过代码看看schedule过程,整个过程从react-dom.js文件中的scheduleRoot方法开始
schedule 过程代码实现
scheduleRoot
在scheduleRoot中,把rootFiber保存在两个变量中,workInProgressRoot保存 RootFiber 应用的根,nextUnitOfWork保存供requestIdleCallback消费的下个任务单元
let workInProgressRoot = null; //RootFiber应用的根
let nextUnitOfWork = null; //下一个任务单元
export function scheduleRoot(rootFiber) {
// rootFiber保存在workInProgressRoot和nextUnitOfWork中
workInProgressRoot = rootFiber;
nextUnitOfWork = rootFiber;
}
在workInProgressRoot和nextUnitOfWork赋值之后,requestIdleCallback就开始工作了
workLoop
/**
* 回调返回浏览器空闲时间,判断是否继续执行任务
* @param {*} deadline
*/
function workLoop(deadline) {
//react是否要让出控制权
let shouldYield = false;
// 如果当前有下个任务单元,且不需要让出控制权,则处理下个任务单元
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// 如果当前帧剩余时间过短,则让出控制权
shouldYield = deadline.timeRemaining() < 1;
}
//如果全部任务单元处理结束,则进入commit
if (!nextUnitOfWork && workInProgressRoot) {
console.log('render阶段结束');
commitRoot();
}
//每一帧都要执行这个代码
window.requestIdleCallback(workLoop, { timeout: 500 });
}
//react询问浏览器是否空闲,这里有个优先级的概念 expirationTime
window.requestIdleCallback(workLoop, { timeout: 500 });
在上面的代码中,浏览器会在每一帧的空闲时间执行workLoop,在workLoop方法中,只要存在下一个任务单元,且当前帧有空闲时间,则开始工作,所以在workInProgressRoot和nextUnitOfWork赋值之后,浏览器就会在每帧的空闲时间进行 fiber 的处理了,在代码中我们也可以发现,处理 fiber 的函数是performUnitOfWork,接下来看看这个处理函数做了什么
performUnitOfWork
// 每一帧需要穿插执行的内容
function performUnitOfWork(currentFiber) {
beginWork(currentFiber);
if (currentFiber.child) {
return currentFiber.child; //有孩子返回孩子
}
// 没孩子
while (currentFiber) {
//完成当前节点
completeUnitOfWork(currentFiber);
console.log(currentFiber);
//有弟弟返回弟弟
if (currentFiber.sibling) {
return currentFiber.sibling; //有弟弟返回弟弟
}
//没弟弟找到父亲继续执行
currentFiber = currentFiber.return;
}
}
以上部分代码现在看起来有点懵,因为现在能知道的信息就是child属性是在虚拟 dom 上的,代表当前节点的子节点们,但是sibling和return是啥?别急,我们先从前几行开始看
beginWork(currentFiber);
if (currentFiber.child) {
return currentFiber.child; //有孩子返回孩子
}
这部分应该目前还看得懂,beginWork方法处理当前虚拟 dom,处理完后如果当前虚拟 dom 有子节点,则让nextUnitOfWork等于子节点,把子节点变成下个任务单元供workLoop继续进行可中断调度
那beginWork方法中做了什么?
beginWork
function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) {
//根节点
updateHostRoot(currentFiber);
} else if (currentFiber.tag === TAG_TEXT) {
//文本节点
updateHostText(currentFiber);
} else if (currentFiber.tag === TAG_HOST) {
//原生dom节点
updateHost(currentFiber);
}
// console.log('beginWork', currentFiber);
}
function updateHostRoot(currentFiber) {
//先处理自己 如果是一个原生节点,创建真实DOM 2.创建子fiber
let newChildren = currentFiber.props.children; //[element]
reconcileChildren(currentFiber, newChildren); //reconcile协调
}
function updateHostText(currentFiber) {
if (!currentFiber.stateNode) {
//如果此fiber没有创建DOM节点
currentFiber.stateNode = createDom(currentFiber);
}
}
function updateHost(currentFiber) {
if (!currentFiber.stateNode) {
//如果此fiber没有创建DOM节点
currentFiber.stateNode = createDom(currentFiber);
}
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}
function createDom(currentFiber) {
//文本节点
if (currentFiber.tag === TAG_TEXT) {
return document.createTextNode(currentFiber.props.text);
} else if (currentFiber.tag === TAG_HOST) {
// 其他原生dom节点 如div span
let stateNode = document.createElement(currentFiber.type);
//处理属性
setProps(stateNode, {}, currentFiber.props);
return stateNode;
}
}
beginWork就是根据每个节点的 tag (tag 是在根据虚拟节点创建 fiber,即后面要讲的reconcileChildren方法中生成的)进行相应的处理,生成真实的 dom 节点,并把节点的属性通过setProps方法添加在真实节点上(setProps的具体实现看源码),然后把真实节点挂在当前节点的stateNode属性上
由于根节点和原生 dom 节点会有子节点,所以要继续进行下一步reconcileChildren,传入当前节点的子节点,处理子节点,生成子节点的 fiber,而文本节点不会有子节点,所以不需要reconcileChildren处理子节点
reconcileChildren
reconcileChildren是虚拟 dom 生成 fiber 的步骤,是 fiber 的核心之一,作用是生成当前正在处理的节点的子节点的 fiber,因为这个方法只用来处理子节点,这也是为什么最外层要挂一个 rootFiber 的原因。
直接看代码
function reconcileChildren(currentFiber, newChildren) {
let newChildIndex = 0; //新子节点的索引
let prevSibiling; //上一个新的子fiber
//遍历我们子虚拟DOM元素数组,为每一个虚拟DOM创建子Fiber
while (newChildIndex < newChildren.length) {
let newChild = newChildren[newChildIndex]; //取出虚拟DOM节点
let tag;
if (newChild.type == ELEMENT_TEXT) {
tag = TAG_TEXT;
} else if (typeof newChild.type === 'string') {
tag = TAG_HOST; //如果type是字符串,那么这是一个原生DOM节点div
}
// 处理当前fiber的子fuber
let newFiber = {
tag,
type: newChild.type,
props: newChild.props,
stateNode: null, //div还没有创建DOM元素
return: currentFiber, //父Fiber returnFiber
effectTag: PLACEMENT, //副作用标示,render会收集副作用 增加 删除 更新 第一次进来都是增加
nextEffect: null, //effect list也是一个单链表 顺序和完成顺序一样 节点可能会少 只放需要变动的fiber节点,其他的绕过
};
if (newFiber) {
if (newChildIndex == 0) {
//如果索引是0,就是大儿子
currentFiber.child = newFiber;
} else {
prevSibiling.sibling = newFiber; //大儿子指向弟弟
}
prevSibiling = newFiber;
}
newChildIndex++;
}
}
reconcileChildren接收两个参数,第一个是当前正在处理的虚拟 dom,第二个是当前 dom 的所有子节点,该方法用于生成当前虚拟 dom 的所有子节点的 fiber,子 fiber 通过sibling连接,父子 fiber 通过child和return连接,这样父子组件就可以形成一个链表,方便用于中断调度
具体内容看注释就行,关键的是创建完 fiber 后把子节点串起来的代码要细细解读下
if (newFiber) {
if (newChildIndex == 0) {
//如果索引是0,就是大儿子
currentFiber.child = newFiber;
} else {
prevSibiling.sibling = newFiber; //大儿子指向弟弟
}
prevSibiling = newFiber;
}
这部分代码的意思是,在循环创建当前节点的子节点 fiber 的过程中进行以下分支操作:
- 如果是第一个子节点,则当前
fiber.child等于第一个子节点 - 如果不是第一个子节点,则把上一个子节点的
sibling指向自己
这样,父节点和所有子节点就通过child和sibling以及生成 fiber 过程中的return串起来了
completeUnitOfWork 的时机
在上面的代码中,我们解读了performUnitOfWork方法的beginWork部分,这部分主要是深度优先遍历了节点及各个节点的子节点,下面还有一部分内容:
// 每一帧需要穿插执行的内容
function performUnitOfWork(currentFiber) {
beginWork(currentFiber);
if (currentFiber.child) {
return currentFiber.child; //有孩子返回孩子
}
+ // 没孩子
+ while (currentFiber) {
+ //完成当前节点
+ completeUnitOfWork(currentFiber);
+ console.log(currentFiber);
+ //有弟弟返回弟弟
+ if (currentFiber.sibling) {
+ return currentFiber.sibling; //有弟弟返回弟弟
+ }
+ //没弟弟找到父亲继续执行
+ currentFiber = currentFiber.return;
}
}
这部分的代码是什么意思呢?
来看个例子,比方说有以下节点,
在经过第一轮处理后节点间的关系如下
这时候,当前处理节点的指针指向C1,它没有子节点,所以就会进入新增的这一部分代码。这段代码的执行逻辑是首先执行completeUnitOfWork来把该节点加入effectList的处理中,然后看看C1有没有兄弟节点,发现C1有兄弟节点C2,就把nextUnitOfWork指向C2,让C2成为下一个任务单元。
当C2处理时,同样会进入beginWork,然后生成真实的 dom 节点挂在C2的stateNode属性上,但是C2并没有子节点,所以reconcileChildren实际上没有执行,然后进入performUnitOfWork的判断,发现C2节点既没有子节点,也没有兄弟节点,就拿到C2.return,即B1,nextUnitOfWork指向B1,让B1成为下一个任务单元继续执行,以此类推,最终结果如下
而completeUnitOfWork的执行顺序如下
completeUnitOfWork 的过程
completeUnitOfWork方法实际上就是生成effectlist的方法,effectlist实际上就是在 fiber 上新增了firstEffect、nextEffect、lastEffect属性,生成属性后的节点间关系如下:
完整过程例子
假如我们有以下节点:
<div id="father">
<div id="son1">son1</div>
<div id="son2">son2</div>
</div>
则整个流程的执行过程如下:
当前处理单元:root
- 1.通过
createElement生成虚拟 dom - 2.执行
ReactDOM.render方法,传入真实要挂载的 dom 节点 - 3.
render方法生成一个 root 节点,把 1 中的虚拟 dom 挂载在该节点下 - 4.执行
scheduleRoot,保存 root 节点为第一个任务单元开始调度 - 5.浏览器通过
workLoop在空闲时间执行任务单元,第一个任务单元为 root - 6.
workLoop调用performUnitOfWork方法,该方法开始对当前节点进行处理 - 7.
beginWork方法判断当前节点为 root 节点,root 节点没有对应的真实 dom,直接进入reconcileChildren生成子节点 fiber - 8.
reconcileChildren接收到当前节点(root)与子节点(id:fater),开始创建子节点(id:fater)的 fiber,并把当前子节点(id:fater)的 fiber 的 return 指向 root - 创建完子节点 fiber,因为 id:fater 节点是第一个子节点,所以把父节点(root)的 clild 指向子节点(id:fater)
- 由于没有兄弟节点了,本轮
reconcileChildren结束,此时currentFiber是 root,currentFiber.child是 id:fater 节点, - 把
currentFiber.child,即 id:fater 节点作为下一个任务单元
当前处理单元:id==fater 节点
- 浏览器在空闲时执行任务单元,现在执行的当前节点是 id:fater
beginWork方法判断当前节点为原生 dom 节点,fater 节点有对应的真实 dom 节点,通过createDom方法创建真实 dom,挂载在当前节点的stateNode属性上,然后进入reconcileChildren生成子节点 fiberreconcileChildren接收到当前节点(id:fater)与子节点(id:son1;id:son2),开始循环创建子节点的 fiber,先创建(id:son1)的 fiber,并把当前子节点(id:son1)的 fiber 的 return 指向父节点(id:fater)- 创建完当前子节点(id:son1)的 fiber,因为 id:son1 节点是第一个子节点,所以把父节点(fater)的 clild 指向子节点(id:son1)
- 开始创建第二个子节点(id:son2)的 fiber,并把当前子节点(id:son2)的 fiber 的 return 指向父节点(id:fater),
- 创建完当前子节点(id:son2)的 fiber,因为 id:son2 节点不是第一个子节点,所以把上一个子节点(id:son1)的 sibling 指向自己
- 当前节点(id:fater)生成了所有子节点的 fiber,没有子节点了,所以把第一个子节点(id:son1)当做下一个任务单元
当前处理单元:id==son1 节点
- 浏览器在空闲时执行任务单元,现在执行的当前节点是 id:son1
beginWork方法判断当前节点为原生 dom 节点,文本节点有对应的真实 dom 节点,通过createDom方法创建真实 dom,挂载在当前节点的stateNode属性上,然后进入reconcileChildren生成子节点 fiberreconcileChildren接收到当前节点(id:son1)与子节点(textLson1),开始创建子节点的 fiber 并把当前子节点(text:son1)的 fiber 的 return 指向父节点(id:son1)- 当前节点(id:son1)生成了所有子节点的 fiber,没有子节点了,所以把第一个子节点(text:son1)当做下一个任务单元
当前处理单元:text==son1 文本节点
- 浏览器在空闲时执行任务单元,现在执行的当前节点是 text:son1
beginWork方法判断当前节点为文本节点,文本节点有对应的真实 dom 节点,通过createDom方法创建文本,挂载在当前节点的stateNode属性上,文本节点没有子节点,所以不进入reconcileChildren- 回到
performUnitOfWork,当前节点(text:son1)没有子节点,所以complete当前节点(即执行completeUnitOfWork),当前节点也没有兄弟节点,所以把父节点(id:son1)complete - 父节点(id:son1)
complete后,发现有兄弟节点(id:son2),所以把兄弟节点(id:son2)当做下一个任务单元
当前处理单元:id==son2 节点
- 浏览器在空闲时执行任务单元,现在执行的当前节点是 id:son2
beginWork方法判断当前节点为原生 dom 节点,文本节点有对应的真实 dom 节点,通过createDom方法创建真实 dom,挂载在当前节点的stateNode属性上,然后进入reconcileChildren生成子节点 fiberreconcileChildren接收到当前节点(id:son2)与子节点(text:son2),开始创建子节点的 fiber 并把当前子节点(text:son2)的 fiber 的 return 指向父节点(id:son2)- 当前节点(id:son2)生成了所有子节点的 fiber,没有子节点了,所以把第一个子节点(text:son2)当做下一个任务单元
当前处理单元: text==son2 文本节点
- 浏览器在空闲时执行任务单元,现在执行的当前节点是 text:son2
beginWork方法判断当前节点为文本节点,文本节点有对应的真实 dom 节点,通过createDom方法创建文本,挂载在当前节点的stateNode属性上,文本节点没有子节点,所以不进入reconcileChildren- 回到
performUnitOfWork,当前节点(text:son2)没有子节点,所以complete当前节点(即执行completeUnitOfWork),当前节点也没有兄弟节点,所以把父节点(id:son2)complete - 父节点(id:son2)
complete后,发现节点(id:son2)没有下一个兄弟节点了,所以把(id:father)complete - 所有节点执行完毕,生成
effect list
commit
当所有节点都处理完,生成effect list后,就要进入commit阶段了,生成effect list的阶段可以打断,但是现在要进行的commit阶段是不能被打断的
commit阶段做的主要事情是从下到上拿到各个 fiber 的 stateNode,它是当前 fiber 的真实节点信息,把节点挂载在父节点的真实节点上,来看代码
// 没有下一个任务单元,fiber处理结束,effectlist生成完成
if (!nextUnitOfWork && workInProgressRoot) {
console.log('render阶段结束');
commitRoot();
}
function commitRoot() {
//拿到effect list 的firstEffect,从first effect开始挂载
let currentFiber = workInProgressRoot.firstEffect;
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
workInProgressRoot = null;
}
function commitWork(currentFiber) {
if (!currentFiber) return;
let returnFiber = currentFiber.return;
let returnDOM = returnFiber.stateNode;
//console.log(currentFiber)
if (currentFiber.effectTag === PLACEMENT) {
returnDOM.appendChild(currentFiber.stateNode);
}
currentFiber.effectTag = null;
}
代码中的节点从下到上根据effectlist的顺序逐渐往上挂载,到最后会挂载到 root 的 stateNode 上,而 root 的 stateNode 是 render 函数传进来的根节点,如下:
//#root 为root的stateNode
ReactDOM.render(element, document.querySelector('#root'));
已经存在在页面上了,所以所有真实节点就被全部挂载到页面上了
至此,react 的完整渲染过程就讲解完了,我对前端 mvvm 的理解又加深了一步,继续加油吧