手把手教你实现 React 渲染框架 看完不会来打我!
引言
下面这段代码是React渲染jsx的语法,运行会发生报错,也就是说React的代码不加一些处理是不能运行在js的
<div id="root"></div>
<script>
const element = <div>content</div> // jsx
const container = document.getElementById('root')
ReactDom.render(element,container)
</script>
接下来实现一下React转原生js实现渲染的过程
1.jsx语法会被Bable转化为以下语法
const element = <div>content</div> // jsx
const element = React.createElement('div', {
title: 'hello word'
}, 'content')
Babel 使用 @babel/preset-react 预设来处理 JSX 代码。这个预设包含了一个插件 @babel/plugin-transform-react-jsx,用于将 JSX 转换为函数调用
例如:<div>content</div>转换为React.createElement('div', {title: 'hello word'}, 'content')
转化后的React.createElement接受三个参数,分别是Dom类型,Dom属性,内容(子元素)
2.React.createElement会把 element转化为虚拟DOM树
const element = {
type: 'div',
props: {
title: 'hello word',
children: 'content'
}
}
经过React.createElement函数转换成js对象,type是 DOM 类型,props是参数,title是属性,children为内容(子元素),为下一步渲染做准备
3.ReactDom.render渲染到页面
const node = document.createElement(element.type)
node['title'] = element.props.title
const text = document.createTextNode(element.props.children)
node.appendChild(text)
container.appendChild(node)
ReactDom.render函数解析 DOM 节点树,创建元素,添加属性,添加内容(子元素),最后挂载到父元素
完整代码
<div id="root"></div>
<script>
const container = document.getElementById('root')
const element = {
type: 'div',
props: {
title: 'hello word',
children: 'content'
}
}
const node = document.createElement(element.type)
node['title'] = element.props.title
const text = document.createTextNode(element.props.children)
node.appendChild(text)
container.appendChild(node)
</script>
效果

以上是简单的示例介绍了渲染的流程,下面带大家详细分析渲染流程
React.createElement
在 React 中,React.createElement 函数用于创建虚拟 DOM 元素。它接受三个参数:元素类型、属性对象以及子元素。
const divObject = React.createElement('div', {
title: 'hello word',
}, React.createElement('p', null, 'dell'), React.createElement('b', null, 'lee'))
console.log(divObject)
上面的示例首先通过React.createElement创建一个js对象
起始节点为div,有一个title属性,内容是两个子节点
- 节点
p,属性为空,子节点为dell文本节点 - 节点
b,属性为空,子节点为lee文本节点
createElement 原理
以下是 React 源码中 React.createElement 函数的简化版本:
const React = {
createElement: function(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child => {
if(typeof child === 'object') {
return child;
}else {
return {
type: 'TEXT_NODE',
props: {
nodeValue: child,
children: []
},
}
}
})
}
}
}
};
在上面的源码中,createElement 函数接收一个 type 参数(元素类型)、一个 props 参数(元素的属性对象)以及可选的 children 参数(子元素)。
函数返回值一个对象分别包含以下几个属性
type为元素类型,值是一个字符串props则是一个对象,用来储存属性和子元素
props里面首先使用ES6 中的扩展运算符将 props 参数中的属性解析出来,children属性使用map遍历children参数,对不同类型的子元素分别进行了处理,
- 对象类型,直接返回子元素,
- 文本类型,封装成对象
type类型定义为TEXT_NODE,props.nodeValue为文本的值,props.children子元素为空数组
总结起来,createElement 函数通过创建一个对象来描述虚拟 DOM 元素,其中包含了元素的类型、属性和子元素等信息。对于子元素,会根据其类型进行判断,如果是对象类型,则直接添加到 props.children 中;如果是文本类型,则通过 createTextElement 函数创建对应的虚拟 DOM 对象。这样就生成了一个虚拟 DOM 元素,可以用于进行后续的渲染和更新操作。
ReactDom.render
在 React 中,ReactDom.render 函数用于渲染虚拟 DOM 元素。它接受两个参数:DOM 节点树,父元素。
const element = <div title="Hello Welcome"><p>Dell</p><b>Lee</b></div>
const root = document.getElementById("root");
ReactDOM.render(element, root);
上面的示例使用ReactDom.render把 DOM 节点树挂载到root上
render 函数实现
以下是 ReactDom.render 函数抽象的实现方式:
const ReactDOM = {
render: function(element, container) {
// 创建合理的元素节点
const dom = element.type === "TEXT_NODE" ? document.createTextNode("") : document.createElement(element.type);
// 给元素节点挂载属性
Object.keys(element.props).filter(key=> key!== 'children').forEach((name) => { dom[name] = element.props[name] })
// 使用 children 属性作一个递归
element.props.children.forEach(child => ReactDOM.render(child, dom));
container.append(dom);
}
};
ReactDOM.render接收两个参数element,container分别是 DOM 节点树,父元素
渲染 DOM 节点树的三个核心步骤:
- 创建合理的元素节点,元素和文本
- 给元素节点挂载属性,
props中除children以外的属性 - 使用
children属性作一个递归,继续渲染子节点
React16之前的版本采用的是这个写法,一次性把所有的 DOM 节点渲染完成,在渲染的过程中浏览器不能做别的操作,更新组件的时候会导致页面卡顿,React18 采用的是拆分成小的片段,等待浏览器空闲时渲染
Concurrent Mode
在 Concurrent Mode 中,React 可以中断渲染过程,处理优先级更高的任务,然后再返回并继续渲染。这种能力使 React 能够更好地处理复杂的用户交互、动画和其他需要实时性能的场景。
以下是Concurrent Mode的实现思路
// 下一个要执行的单元
let nextUnitWork = {};
// 该函数用来处理下一个执行的单元,同时返回下下一个执行单元
function performUnitOfWork(nextUnitOfWork) {
console.log(nextUnitOfWork);
return null;
}
// 自动调度
function workLoop(deadline) {
while(nextUnitWork){
nextUnitWork = performUnitOfWork(nextUnitWork);
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop)
requestIdleCallback 是什么?
requestIdleCallback是一个用于在浏览器空闲时执行任务的API。它允许开发者在浏览器空闲时执行一些较为耗时的任务,而不会影响到页面的性能和用户体验。 使用requestIdleCallback可以将一些耗时的操作分解成多个小任务,然后在浏览器空闲时依次执行这些任务,从而避免阻塞主线程。
首先使用requestIdleCallback解决浏览器什么时候空闲的问题,由于兼容性问题React并没有使用requestIdleCallback,而是自己封装了一套方法,requestIdleCallback接收一个参数workLoop为回调函数
workLoop函数起到一个自动调度的作用,参数deadline.timeRemaining()获取到本次浏览器的空闲时间,通过while循环执行下一个需要执行的单元,等待空闲时间结束,再次调用requestIdleCallback等待浏览器空闲时继续执行workLoop函数
performUnitOfWork该函数用来处理下一个执行的单元,同时返回下下一个执行单元,如果没有返回null渲染结束
总结,React中使用Concurrent Mode是想把一个比较大的 DOM 节点对象,打散成多个小的 DOM 节点对象,通过调度一段一段的去渲染
Fiber
Fiber架构的核心思想是将组件的渲染和更新过程拆分成多个小任务单元,然后通过调度器来决定任务的执行顺序。这样可以在每个任务单元之间进行中断、暂停和恢复,从而实现更灵活的调度和优化。
const element = (
<div>
<h1>
<p>Paragraph</p>
<a href='https://www.imooc.com'>Link</a>
</h1>
<h2>Subtitle</h2>
</div>
)
const root = document.getElementById("root");
ReactDOM.render(element, root);
分析这段代码,首先root节点是根节点,
div是root下面的子节点,但没有兄弟节点
h1和h2是兄弟节点,div分别是h1和h2的父节点
p和a是兄弟节点,h1分别是p和a的父节点
Fiber是一个个小的单元对象对象,加上父子,兄弟节点的链表关系,Fiber节点组合在一起就是Fiber Tree
Fiber的基础实现
Fiber的基础实现主要分3个步骤:
- 把 Fiber 对应的内容渲染到页面上
- 计算下一层 Fiber Tree
- 选择下一个要执行的 Fiber 单元
// 下一个要执行的单元
let nextUnitWork = null;
// 该函数用来处理下一个执行的单元,同时返回下下一个执行单元
function performUnitOfWork(fiber) {
// 1. 把 fiber 对应的内容渲染到页面上
if(!fiber.dom) {
fiber.dom = ReactDOM.createDom(fiber);
}
if(fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
// 2. 计算下一层 fiber tree
const elements = fiber.props.children;
let index = 0;
let prevSibling = null;
while(index < elements.length) {
const element = elements[index];
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null
}
if(index === 0) {
fiber.child = newFiber;
}else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
// 3. 选择下一个要执行的 fiber 单元
if(fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while(nextFiber) {
if(nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
// 自动调度
function workLoop(deadline) {
while(nextUnitWork){
nextUnitWork = performUnitOfWork(nextUnitWork);
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
const ReactDOM = {
createDom: function(fiber) {
// 创建合理的元素节点
const dom = fiber.type === "TEXT_NODE" ? document.createTextNode("") : document.createElement(fiber.type);
// 给元素节点挂载属性
Object.keys(fiber.props).filter(key=> key!== 'children').forEach((name) => { dom[name] = fiber.props[name] })
return dom;
},
render: function(element, container) {
nextUnitWork = {
dom: container,
props: {
children: [element]
}
}
}
};
重新改写ReactDOM对象,渲染 DOM 的功能分给createDom方法,
render只提供开始渲染的条件,把nextUnitWork赋值为一个初始化的Fiber
1.把 Fiber 对应的内容渲染到页面上,创建元素,挂载到父元素上
2.计算下一层 Fiber Tree,每一个子节点都生成Fiber,然后构建父子,兄弟之间的链路关系
3.选择下一个要执行的 Fiber 单元,自上而下,优先执行第一个子元素,例如root->div->h1->p
执行到最后一个子元素为止,然后执行最后一个元素兄弟元素,执行完返回去执行父元素的兄弟元素
这样就实现的一个复杂的 DOM 树渲染,打散成多个小的DOM单元,等到游览器空闲就开始渲染一部分工作
总结:正是因为workLoop这种循环队列加上Fiber Tree这种的数据结构,使得React17之后的版本,遇到比较复杂的 DOM 树渲染的时候,避免出现卡顿的现象,提升用户体验

目前还有一些问题,比如浏览器渲染了一些元素,突然有一个大的耗时任务需要执行,剩余的 DOM 元素则需要等待耗时任务执行完才能渲染,这种切块渲染方式,可能会导致 DOM 元素一块一块的渲染,而React则是希望等待执行完一次性的渲染到页面上
Render & Commit 阶段
针对上面的问题,React中引入了两个概念:
- Render阶段:用来创建 DOM 节点,生成Fiber Tree
- Commit阶段:把Fiber Tree中的元素渲染到页面
// 下一个要执行的单元
let nextUnitWork = null;
// workInProgress 当前正在计算的 fiber 节点
let wipRoot = null;
function commitWork(fiber) {
if(!fiber) {
return;
}
const domParent = fiber.parent.dom;
domParent.appendChild(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function commitRoot() {
commitWork(wipRoot.child)
wipRoot = null;
}
// 该函数用来处理下一个执行的单元,同时返回下下一个执行单元
// 用来生成 Fiber Tree 的一个函数,生成 fiber tree 的过程,在 React 中叫做 render
function performUnitOfWork(fiber) {
// 1. 把 fiber 对应的内容渲染到页面上
if(!fiber.dom) {
fiber.dom = ReactDOM.createDom(fiber);
}
// if(fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom);
// }
// 2. 计算下一层 fiber tree
const elements = fiber.props.children;
let index = 0;
let prevSibling = null;
while(index < elements.length) {
const element = elements[index];
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null
}
if(index === 0) {
fiber.child = newFiber;
}else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
// 3. 选择下一个要执行的 fiber 单元
if(fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while(nextFiber) {
if(nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
// 自动调度
function workLoop(deadline) {
while(nextUnitWork){
nextUnitWork = performUnitOfWork(nextUnitWork);
}
if(!nextUnitWork && wipRoot) {
// fiber tree 已经准备好了,需要一次性的挂载 DOM
// 一次性把 fiber tree 的内容渲染到页面上,这个过程叫做 react 中的 commit 阶段
commitRoot()
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
const ReactDOM = {
createDom: function(fiber) {
// 创建合理的元素节点
const dom = fiber.type === "TEXT_NODE" ? document.createTextNode("") : document.createElement(fiber.type);
// 给元素节点挂载属性
Object.keys(fiber.props).filter(key=> key!== 'children').forEach((name) => { dom[name] = fiber.props[name] })
return dom;
},
render: function(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
}
}
nextUnitWork = wipRoot;
}
};
performUnitOfWork方法只负责创建 DOM 元素(生成Fiber Tree)不具备渲染的能力,在 React 中叫做 render 阶段
wipRoot用来表示当前正在计算的 Fiber 节点,并在render中赋初始值
workLoop函数中等待所有的执行单元执行完成,并且存在正在计算的 Fiber 节点,调用commitRoot函数一次性把 Fiber Tree 的内容渲染到页面上,这个过程叫做 React 中的 Commit 阶段
commitRoot函数,调用commitWork函数从wipRoot中的子节点开始渲染(初始值root已经被渲染,所以每次从子节点开始),最后全部渲染后把wipRoot的状态置为null
commitWork函数,首先先去渲染当前节点,挂载到父节点上,接着递归渲染子节点,等待所有子节点渲染完成再去渲染兄弟节点
总结:rander阶段就是创建 DOM 生成 Fiber Tree 的过程,Commit阶段是把Fiber Tree 做一个遍历一次性的渲染到页面
Reconciliation 阶段
在React中,Reconciliation(协调)阶段是指React通过比较新旧虚拟DOM树的差异,找出需要更新的部分,并进行相应的更新操作的过程。这个过程是React中非常重要的一部分,用于确保页面的UI与数据的一致性。
举个例子说明一下Reconciliation 阶段存在的重要性
let element = (
<div>
<h1>
<p>Paragraph</p>
<a href='https://www.imooc.com'>Link</a>
</h1>
<h2>Subtitle</h2>
</div>
)
const root = document.getElementById("root");
ReactDOM.render(element, root);
element = (
<div>
<h1>
Paragraph update
</h1>
<h2>Subtitle</h2>
</div>
)
ReactDOM.render(element, root);
这段代码中ReactDOM.render执行了两次,根据目前的实现方法,每次都是重新创建 DOM 元素,对于重复的 DOM 重新创建会浪费性能,Reconciliation 阶段就是解决DOM的复用的问题
Reconciliation 阶段的实现在Render 和 Commit 两个阶段
// 下一个要执行的单元
let nextUnitWork = null;
// workInProgress 当前正在计算的 fiber 节点
let wipRoot = null;
// 存储上一次渲染对应的 FiberTree Root
let currentRoot = null;
// 存储本次渲染需要删除的 fiber 节点
let deletions = [];
const isEvent = key => key.startWith("on");
const isProperty = key => key !== "children" && !isEvent(key);
const isNew = (prev, next) => key=> preProps[key] !== nextProps[key];
const isGone = (next) => key => !(key in nextProps);
// 当 DOM 可以复用时,复用 DOM 节点的逻辑
function updateDom(dom, preProps, nextProps) {
// 清除老的或者被改变的 dom 节点事件处理函数
Object.keys(preProps)
.filter(isEvent)
.filter(key => !(key in nextProps) || isNew(preProps, nextProps)(key))
.forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, preProps[name])
})
// 清除老的 DOM 属性
Object.keys(preProps)
.filter(isProperty)
.filter(isGone(nextProps))
.forEach(name => dom[name] = "")
// 增加新的或者修改老的 DOM 属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(preProps, nextProps))
.forEach(name => dom[name] = nextProps[name])
// 新增事件监听
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(preProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name])
})
}
function commitWork(fiber) {
if(!fiber) {
return;
}
const domParent = fiber.parent.dom;
if(fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if(fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom);
} else if(
fiber.effectTag === "UPDATE" && fiber.dom != null
){
updateDom(fiber.dom, fiber.alternate.props, fiber.props)
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
// commit 函数,用于一次性更新 DOM
function commitRoot() {
deletions.forEach(commitWork)
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
// 调协函数
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while(index < elements.length || oldFiber != null) {
// 1. fiber ,fiber 合并成一个大树,删掉老fiber 上需要删除的东西
const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
if(sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: 'UPDATE'
}
}
if(element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
parent: wipFiber,
dom: null,
alternate: null,
effectTag: 'PLACEMENT'
}
}
if(oldFiber && !sameType) {
oldFiber.effectTag = 'DELETION';
deletions.push(oldFiber);
}
if(oldFiber) {
oldFibler = oldFibler.sibling;
}
if(index === 0) {
wipFiber.child = newFiber;
}else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
// 该函数用来处理下一个执行的单元,同时返回下下一个执行单元
// 用来生成 Fiber Tree 的一个函数,生成 fiber tree 的过程,在 React 中叫做 render
function performUnitOfWork(fiber) {
// 1. 把 fiber 对应的内容渲染到页面上
if(!fiber.dom) {
fiber.dom = ReactDOM.createDom(fiber);
}
// 2. 计算下一层 fiber tree
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
// 3. 选择下一个要执行的 fiber 单元
if(fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while(nextFiber) {
if(nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
// 自动调度
function workLoop(deadline) {
while(nextUnitWork){
nextUnitWork = performUnitOfWork(nextUnitWork);
}
if(!nextUnitWork && wipRoot) {
// fiber tree 已经准备好了,需要一次性的挂载 DOM
// 一次性把 fiber tree 的内容渲染到页面上,这个过程叫做 react 中的 commit 阶段
commitRoot()
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
const ReactDOM = {
createDom: function(fiber) {
// 创建合理的元素节点
const dom = fiber.type === "TEXT_NODE" ? document.createTextNode("") : document.createElement(fiber.type);
// 给元素节点挂载属性
Object.keys(fiber.props).filter(key=> key!== 'children').forEach((name) => { dom[name] = fiber.props[name] })
return dom;
},
render: function(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot,
}
deletions = [];
nextUnitWork = wipRoot;
}
};
新建两个变量currentRoot和deletions
currentRoot存储上一次渲染对应的 FiberTree Rootdeletions存储本次渲染需要删除的 Fiber 节点
currentRoot在渲染完成后赋值,用于下一次渲染的时候做对比
deletions在创建DOM的时候添加值(上一次渲染Fiber Tree中用不到的 DOM),方便Commit 阶段删除
Render 阶段
Render 阶段的变化主要发生在计算下一层 Fiber Tree上,把这一块的逻辑封装成了一个函数reconcileChildren,接收两个参数wipFiber, elements分别是当前元素 Fiber 和子元素,while循环里面执行的逻辑是把Fiber 合并成一个大树,删掉老Fiber 上需要删除的东西,合成过程
- 添加(PLACEMENT) 新元素类型在上一次渲染的Fiber中不存在
- 更新(UPDATE)
sameType变量为true,意味着新的 DOM 元素类型在上一次渲染的Fiber中存在 - 删除(DELETION) 在新Fiber不存在旧的Fiber存在的元素类型
Commit 阶段
在commitWork函数中根据fiber.effectTag区分 DOM 属于那种类型,新增和删除这两个直接就在父元素上进行相应的操作,更新需要在updateDom函数中额外处理一下,也就是复用 DOM 节点的逻辑
updateDom函数接受三个参数,DOM 当前渲染的 DOM,preProps新Fiber的参数,nextProps旧Fiber的参数,从参数也可以看出来主要是对属性做处理,通过preProps和nextProps中参数的差异做删除,添加和更改,操作的对象是 DOM 对应的属性和事件。
总的来说,在比较新旧虚拟DOM树时,React会尽可能地复用已有的DOM节点,而不是直接销毁和重新创建。这样可以减少对DOM的操作,提高性能。React还会根据组件的key属性来判断是否需要重新渲染组件,以确保页面的稳定性和一致性。
讲到这里有关渲染的原理基本都讲完了,另外再补充一个功能点函数组件的实现
函数组件的实现
渲染函数组件跟渲染jsx有两个区别:
- Fiber没有 DOM
- 没办法直接取
children
通过示例观察一下结构
function App(props) {
return <h1>hello, {props.name}</h1>
}
const element = <App name="Dell" />
const root = document.getElementById("root");
ReactDOM.render(element, root);
<App name="Dell" />转化为React.createElement(App, { name: 'Dell' })App是一个函数,children作为函数的返回值,之前的处理方式是App.dom,获取子元素是children,很明显需要额外处理一下数据
部分代码
function updateHostComponent(fiber) {
// 1. 把 fiber 对应的内容渲染到页面上
if(!fiber.dom) {
fiber.dom = ReactDOM.createDom(fiber);
}
// 2. 计算下一层 fiber tree
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
}
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
// 该函数用来处理下一个执行的单元,同时返回下下一个执行单元
// 用来生成 Fiber Tree 的一个函数,生成 fiber tree 的过程,在 React 中叫做 render
function performUnitOfWork(fiber) {
const isFunctionComonent = fiber.type instanceof Function;
if(isFunctionComonent) {
updateFunctionComponent(fiber);
}else {
updateHostComponent(fiber)
}
// 3. 选择下一个要执行的 fiber 单元
if(fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while(nextFiber) {
if(nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
在performUnitOfWork创建DOM 元素时分别对 DOM 和函数作处理,updateFunctionComponent方法处理函数的情况,updateHostComponent方法处理 DOM 的情况
function commitDeletion(fiber, domParent) {
if(fiber.dom) {
domParent.removeChild(fiber.dom);
}else {
commitDeletion(fiber.child, domParent);
}
}
function commitWork(fiber) {
if(!fiber) {
return;
}
let domParentFiber = fiber.parent;
while(!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;
if(fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if(fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
} else if(
fiber.effectTag === "UPDATE" && fiber.dom != null
){
updateDom(fiber.dom, fiber.alternate.props, fiber.props)
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
在删除 DOM 的时候通过commitDeletion判断是否有dom属性,如果有就直接删除,没有就使用递归找到子元素中的 DOM 删除
执行新增的时候遇到函数式组件需要一层层的往上找,找到最近的一个能挂载元素的 DOM
完整代码
// 下一个要执行的单元
let nextUnitWork = null;
// workInProgress 当前正在计算的 fiber 节点
let wipRoot = null;
// 存储上一次渲染对应的 FiberTree Root
let currentRoot = null;
// 存储本次渲染需要删除的 fiber 节点
let deletions = [];
const isEvent = key => key.startWith("on");
const isProperty = key => key !== "children" && !isEvent(key);
const isNew = (prev, next) => key=> preProps[key] !== nextProps[key];
const isGone = (next) => key => !(key in nextProps);
// 当 DOM 可以复用时,复用 DOM 节点的逻辑
function updateDom(dom, preProps, nextProps) {
// 清除老的或者被改变的 dom 节点事件处理函数
Object.keys(preProps)
.filter(isEvent)
.filter(key => !(key in nextProps) || isNew(preProps, nextProps)(key))
.forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, preProps[name])
})
// 清除老的 DOM 属性
Object.keys(preProps)
.filter(isProperty)
.filter(isGone(nextProps))
.forEach(name => dom[name] = "")
// 增加新的或者修改老的 DOM 属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(preProps, nextProps))
.forEach(name => dom[name] = nextProps[name])
// 新增事件监听
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(preProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name])
})
}
function commitDeletion(fiber, domParent) {
if(fiber.dom) {
domParent.removeChild(fiber.dom);
}else {
commitDeletion(fiber.child, domParent);
}
}
function commitWork(fiber) {
if(!fiber) {
return;
}
let domParentFiber = fiber.parent;
while(!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;
if(fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if(fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
} else if(
fiber.effectTag === "UPDATE" && fiber.dom != null
){
updateDom(fiber.dom, fiber.alternate.props, fiber.props)
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
// commit 函数,用于一次性更新 DOM
function commitRoot() {
deletions.forEach(commitWork)
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
// 调协函数
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while(index < elements.length || oldFiber != null) {
// 1. fiber ,fiber 合并成一个大树,删掉老fiber 上需要删除的东西
const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
if(sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: 'UPDATE'
}
}
if(element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
parent: wipFiber,
dom: null,
alternate: null,
effectTag: 'PLACEMENT'
}
}
if(oldFiber && !sameType) {
oldFiber.effectTag = 'DELETION';
deletions.push(oldFiber);
}
if(oldFiber) {
oldFibler = oldFibler.sibling;
}
if(index === 0) {
wipFiber.child = newFiber;
}else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
function updateHostComponent(fiber) {
// 1. 把 fiber 对应的内容渲染到页面上
if(!fiber.dom) {
fiber.dom = ReactDOM.createDom(fiber);
}
// 2. 计算下一层 fiber tree
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
}
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
// 该函数用来处理下一个执行的单元,同时返回下下一个执行单元
// 用来生成 Fiber Tree 的一个函数,生成 fiber tree 的过程,在 React 中叫做 render
function performUnitOfWork(fiber) {
const isFunctionComonent = fiber.type instanceof Function;
if(isFunctionComonent) {
updateFunctionComponent(fiber);
}else {
updateHostComponent(fiber)
}
// 3. 选择下一个要执行的 fiber 单元
if(fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while(nextFiber) {
if(nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
// 自动调度
function workLoop(deadline) {
while(nextUnitWork){
nextUnitWork = performUnitOfWork(nextUnitWork);
}
if(!nextUnitWork && wipRoot) {
// fiber tree 已经准备好了,需要一次性的挂载 DOM
// 一次性把 fiber tree 的内容渲染到页面上,这个过程叫做 react 中的 commit 阶段
commitRoot()
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
const ReactDOM = {
createDom: function(fiber) {
// 创建合理的元素节点
const dom = fiber.type === "TEXT_NODE" ? document.createTextNode("") : document.createElement(fiber.type);
// 给元素节点挂载属性
Object.keys(fiber.props).filter(key=> key!== 'children').forEach((name) => { dom[name] = fiber.props[name] })
return dom;
},
render: function(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot,
}
deletions = [];
nextUnitWork = wipRoot;
}
};
总结
以上就是React18渲染jsx语法实现过程,首先使用Babel转换成React.createElement函数,再通过React.render开启渲染流程,采用分段渲染的模式,等待浏览器空闲时才做渲染,Render 阶段生成Fiber Tree树形结构,Commit 阶段渲染 DOM,这两个阶段都有Reconciliation(协调)的参与,尽可能地复用已有的DOM节点,提升渲染性能
希望对大家学习React有所帮助,有什么问题欢迎评论区留言,有什么地方表达不清楚的欢迎各位大佬提出宝贵的建议,如果喜欢可以点赞,关注,后面也会持续更新更多优质文章
转载自:https://juejin.cn/post/7370980495155232780