手把手教你实现 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