react源码从入门到入定
学习react源码断断续续,反反复复,东奔西跑,匆匆忙忙,连滚带爬,兵荒马乱的也快一年了,该说不说雀氏比vue源码难得多,趁现在开窍了一点点我将以迅雷不及掩耳之小叮当之势把理解记录下来(免得过几天又忘了)
参考资料
react技术揭秘
build your own react
从jsx开始
每当我们在react项目中从从容容,游刃有余的使用jsx时,<div id='first'><span>我是一个dom元素</span></div>,有没有想过这样一个问题,就是这么个表达式,一不是html,二不是js,为什么在编译器中不报错。且可以正常运行。
从这里引入第一个问题"为什么在react之前的版本中,需要引入react才能使用jsx,最新的版本中不需要了",百闻不如实践,我们直接把这行代码丢给babel,看看转化结果。
从babel的转义结果可以看出,由于 _jsx 函数是编译器在运行时自动导入的,开发者就不再需要亲自动手写那行 import React 了。
当然 在之前的版本中可能是react.createElement方法,这里不用纠结,名字不重要,都是为了生成虚拟dom,反正最后都会转化成fiber
自己实现jsx
不关注任何细节,先直接把react官方实现的jsx打印出来看看是什么结构。
js
const aaa = <div id='first'><span>我是一个dom元素</span></div>
console.log('aaa', aaa)
当然,能使用jsx的前提是必须要先引入babel,这里我使用的是之前下载好的本地的
当然,学源码已经很苦逼了,为了节约时间,我也给各位看官准备好了一个可以直接运行的小demo
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
#app {
word-break: break-all;
}
</style>
<body>
<div id="app"></div>
<!-- <script src="./react优先级调度算法-基础版.js"></script> -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/babel-standalone/7.28.4/babel.js"></script>
</body>
</html>
<script type="text/babel" >
const aaa = <div id='first'><span>我是一个dom元素</span></div>
console.log('aaa', aaa)
</script>
这里把jsx打印出来的结构为
所以我们的目标是

源码课程不包含简单代码实现,那是额外的费用
js
const myJsxFn = (type, options) => {
return {
type,
props: options
}
}
最后看控制台官方转化输出的aaa和我自己实现myJsxFn转化的bbb的打印

好了,不能说一模一样,只能说有点关系。此章over
从jsx到fiber
好了,很多人背面经都会背这么一句话,fiber就是链表结构的虚拟dom,思考如下问题
为什么要从虚拟dom变成fiber
为什么要变成链表结构?
假设我们有一个如图所示的dom结构

它对应的虚拟dom结构应该是这样的

而同一个dom结构对应的fiber结构是这样的

这样的结构带来的优势是如果渲染器或者update时渲染到了我是一个dom元素时,传统虚拟dom无法中断执行,如果保存当前渲染的节点(current=我是一个dom元素)中断了,再恢复渲染时,react找不到下一个需要执行的节点。而同样的例子放在fiber中,如果执行到我是一个dom元素后,判断当前节点没有子节点,还可以通过return返回到它的父级,再通过siblings渲染兄弟节点继续渲染流程。同样都是虚拟dom,但是vue就没有这个问题,而且vue组件更新时也不会把它的子组件全部更新掉,根因是有个依赖收集的过程。感兴趣的可以额外去学习下(vue的学习成本比react低多了 嘤嘤嘤)
圆规正传,回答了上述问题,从虚拟dom->fiber可以实现增量渲染,不然就会像react16版本之前,组件树一大时页面无法响应用户操作,而链表结构是为了实现从断点继续(没有优先级的概念,单线程js只能等渲染树全部执行完再去响应其他操作)
从fiber到任务调度
上一章已经说到了fiber可以从断点继续,那么很明显react的新架构是一个节点一个节点的渲染的。先写出一个伪代码,这里和源码保持一致用一样的命名。workLoop(工作循环),表示这个函数如果有未完成有工作就会一直循环。
js
let nextUnitOfWork = null
const workLoop = (time) => {
if(nextUnitOfWork){
// 在这个函数中会更新nextUnitOfWork,直到nextUnitOfWork = null结束工作
performUnitOfWork(nextUnitOfWork)
}
workLoop(time)
}
如果还有还有未完成的任务,就继续执行。那么什么时候开始这个循环呢,这里介绍一个api,rerequestIdleCallback(先把话讲在前面,react之前的实验版本用过一次这个api,但是由于兼容性、性能等原因被后续react自研实现的调度器schedule替代了,但是这里我们先不讲调度器,一口吃不成个react胖子,我先吃成个react瘦子,react官方说了,调度器可以脱离react独立运行,作为一个单独的库去调用,所以我换篇文章单独学习🤔)。

根据用法,我们把函数微调成这样
js
// 下一个需要执行的单元
let nextUnitOfWork = null
const workLoop = (deadline) => {
// 是否需要让出进程
let shouldYield = false
while (!shouldYield && nextUnitOfWork) {
performUnitOfWork(nextUnitOfWork)
if (deadline.timeRemaining < 1) {
shouldYield = true
}
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)

wait wait wait,半吊子作者写到这里问了ai一个问题(为后文埋一个伏笔 (什么时候开启第一次workLoop))

watever,我们先学个源码的思维方式,反正真正的源码也没有用这个api。
找呀找呀找节点---reconciler核心(performUnitOfWork)
好了,代码实现到这里,再粗心的网友也可以发现performUnitOfWork还没有实现,了解一段代码的逻辑先从函数名开始,从命名可以看出当前函数执行的动作是运行当前工作单元,从函数的运行逻辑分析,该函数应该执行当前工作单元,并且更新下一个需要执行的fiber。
这一part很重要,react协调器(reconciler)的核心函数。为了方便理解以及学习,我们先掌握前置知识
前置知识1 (双缓存机制)
在react运行的过程中,永远会有两棵fiber树(其实私认为fiber其实不是树,称做fiber表应该更合适),一个是当前页面正在展示的fiber树,称为currentFiber,一个是还在内存中运行的正在构建的(也就是下次需要展示的)workInProgressfiber树,他们两者通过alternate互相连接。
currentFiber.alternate = workInProgressFiber
workInProgressFiber.alternate = curretnFiber

在react的render方法中会创建一个全局唯一的fiber节点FiberRootNode(所有fiber的根节点,全局唯一)同时会根据传入render的jsx对象(比如React.render(jsx, targetDom))创建一个rootFiber,这个rootFiber是当前渲染的组件的根节点(全局不唯一,因为可以执行多次render)

以这个dom举个🌰,那id=first的就是rootFiber,这里之所以要区分这两个fiber是因为每次render时会创建不同的rootfiber(表示每次传入render的jsx的根节点),但是整个应用只会有一个fiberRoot,表示所有节点的根fiber。当首次渲染时,内存模型大概就是这个样子。
在首次渲染时,rootFiber没有对应的内存树,rootFiber.alternate = null

同时,在render函数里也会将这个rootFiber赋值给nextUnitOfWork,以此开启第一次任务调度(回收上文的伏笔)
js
// 案例中使用的dom结构是
const actualDom = <div id='first' className='123'>
<span>我是一个dom元素</span>
我是一个普通文本节点
<div>
<span>第三个节点</span>
</div>
</div>
// 下一个需要执行的单元
let nextUnitOfWork = null
// 是否需要让出进程
let shouldYield = false
const jsxObj = myJsxFn('div', {
id: 'first',
className: '123',
children: [
myJsxFn('span', {
children: '我是一个dom元素'
}),
'我是一个普通文本节点',
myJsxFn('span', {
children: '我是一个普通文本节点'
}),
]
})
const Render = (jsx, container) => {
const rootFiber = jsx
// 用此标记这是根节点rootFiber
jsx.return = null
const fiberRoot = {
current: rootFiber
}
// 初次渲染,先手动挂个初始值
rootFiber.alternate = null
// 初始化首次需要执行的单元
nextUnitOfWork = rootFiber
}
Render(jsxObj, document.getElementById('app'))

前置知识2(fiber的心智模型)
虚拟dom是一个树形结构,要通过当前节点并返回下一个子节点,需要遍历到当前树的每一个节点必然会用到递归,在performUnitOfWork函数实现中会执行两个动作,beginWork和completeWork,其中beginWork执行的是递动作,completeWork执行的是归动作,在执行beginWork时,代码会找到当前节点的首个子节点,当遍历到叶子节点时,开始执行completeWork,遍历其父节点的兄弟节点,直至遍历完整个tree。
源码大致流程

请注意 这里源码中的workInProgress 就等于上述我们自己源码中的nextUnitOfWork 其含义都表示下一个需要执行的fiber节点
首次渲染时,此时内存里面是没有rootFiber对应的缓存树的。那么rootFiber.alternate === null那么自然而言fiberRoot.current = null(见上图代码的第二行),这个很关键,因为在源码中都会使用这个来判断react是执行mount还是执行update。当mounted时,会递归的遍历rootFiber的所有子节点,执行创建和挂载 的操作(不会进行diff,因为没有子节点可以复用),当执行update时,会执行更新和替换的操作(也就是执行耳熟能详的diff算法,react会尝试复用已有的节点)。当然,不管是mounted还是update,都会执行上文提到的performUnitOfWork来完成fiber的创建or替换工作。
从源码中不难看出,如果当前fiber有子节点,会一直执行beginWork,直到遍历到叶子节点时再执行completeWork。
前置完毕,开始剖析performUnitOfWork
根据前置知识2,当务之急是搭建出performUnitOfWork的大致骨架,其实很简单,其实并不难
我们只需要把上图勾中的精华部分摘抄出来即可
js
const performUnitOfWork = (unitOfWork) => {
const current = unitOfWork.alternate;
let next;
next = beginWork(current, unitOfWork)
if (next === null) {
completeUnitOfWork(unitOfWork)
} else {
nextUnitOfWork = next
}
}
performUnitOfWork图示

performUnitOfWork第一步会执行beginWork,beginWork最新源码传送门
代码截图一览
从代码中可以看到有对current是否等于null的判断 ,如果判断为挂载流程,则会进一步根据workInProgress.tag来新建对应的子节点

但是不管是什么tag类型的子节点最后都会执行到reconcileChildren,可以追踪到每一个tag执行的不同逻辑里去看,会发现每一个都会有reconcileChildren函数的踪影。也就是说不管是函数组件、类组件、懒加载组件等等,最后都会执行一个叫reconcileChildren的函数。这里不挂源码图以免太绕,这个函数并不复杂。挂上AI解释


从代码看出reconcileChildren函数会返回当前workInProgress 节点的子节点(第一个子节点)
reconcileChildren函数就是完成从vdom到fiber结构转换的关键函数。
that all,我先画个流程图,免得我的观众们&写完这篇博客数个月之后的笔者迷失在源码沙漠里。
截止目前为止的全部流程图

实现beginWork
beginwork的简易流程图

根据源码结构以及简易流程图,根据beginWork要实现的功能(遍历当前节点,执行深度优先算法,一直找到最后一个child节点)我们初步要实现的伪代码是
js
调用beginWork
beginWork里调用reconcileChildren,
(根据ai释义)reconcileChildren会把当前执行节点和他的子节点通过child串好,再把children通过sibilings串好。最后返回第一个节点(大儿子)
(当然,在reconcileChildren里会判断是首次渲染还是更新,执行对应的mounted和diff)
那么根据我们案例中的dom结构

执行beginWork依次遍历的节点应该是
js
1、beginWork 第一个肯定是id=first的div节点
1.1、执行过程中span节点应该通过return关联到div节点,div通过child关联到span节点
1.2、div的三个节点应该通过siblings串联起来
1.3 拿到返回的第一个节点也就是span节点
2、beginWork 执行span节点
2.1、执行过程中文本节点应该通过return关联到span节点,span通过child关联到文本节点
2.2、span没有其他的child节点,不需要通过siblins串联
2.3 拿到返回的第一个节点也就是文本节点
3、beginWork 执行文本节点
文本节点返回的child是null,beginWork执行完毕,后续开始执行completeWork(回溯流程)
编写beginWork
js
const beginWork = (current, wipFiber) => {
return reconCileChildren(current, wipFiber, wipFiber.props.children)
}
const createTextNode = (text, returnFiber) => {
return {
type: "Text",
nodeValue: text,
return: returnFiber,
alternate: null,
props: {
children: null
}
}
}
const reconCileChildren = (current, wipFiber, nextChildren) => {
if (current === null) {
if (typeof nextChildren === 'string') {
// 文本节点
const textNode = createTextNode(nextChildren, wipFiber)
wipFiber.child = textNode;
return wipFiber.child
}
if (wipFiber.type === 'Text') {
return null
}
let preChid = null
nextChildren.forEach((item, index) => {
if (typeof item === 'string') {
item = createTextNode(item, wipFiber)
}
item.alternate = null;
if (index === 0) {
// 如果是第一个子节点
wipFiber.child = item;
item.return = wipFiber;
} else {
preChid.siblings = item;
}
preChid = item;
})
}
return wipFiber.child
}
在performWork中打印next,验证代码是否正常执行
js
const performUnitOfWork = (unitOfWork) => {
console.log('unitOfWork是', unitOfWork)
const current = unitOfWork.alternate;
let next;
next = beginWork(current, unitOfWork)
if (next === null) {
// completeUnitOfWork(unitOfWork)
} else {
nextUnitOfWork = next
}
}
代码运行结果

蒸蚌,完全正确!
实现completeWork
上一节已经实现了beginWork,此时nextUnitOfWork也来到了递阶段的最后一个节点我是一个dom元素。既然到了叶子节点,那么需要开始回溯。和beginWork有所区别的是,completeWork在回溯阶段第一件事是创建当前fiber节点的dom对象(这是为了最后commitWork时可以直接进行挂载操作)。之后才会遍历siblings节点。completeWork遍历到根节点(也就是我们在render函数中打上的tag,根据return === null判断是否是根节点)结束所有工作
js
const createDom = (currentFiber) => {
if (currentFiber.type === 'Text') {
const TextNode = document.createTextNode(currentFiber.nodeValue)
return TextNode
}
const domNode = document.createElement(currentFiber.type)
Object.entries(currentFiber.props).forEach(([key, value]) => {
if(key === 'children'){
// 由于props中也含有children属性,但是children不需要作为props挂载到dom属性上
return;
}
domNode.setAttribute(key, value)
})
return domNode
}
const completeUnitOfWork = () => {
// 源码中叫completedWork,容易跟函数名混淆
let currentCompleteWork = nextUnitOfWork;
// completeWork会给当前fiber创建好dom对象
while(currentCompleteWork !== null){
currentCompleteWork.stateNode = createDom(currentCompleteWork)
if (currentCompleteWork.siblings) {
// 如果当前节点有兄弟节点 拿兄弟节点执行beginWork
nextUnitOfWork = currentCompleteWork.siblings;
return
}
currentCompleteWork = currentCompleteWork.return;
}
nextUnitOfWork = null;
}
优化虚拟completeWork(挂载当前节点dom&当前节点的子节点dom)
从上一节的createDom中我们可以看到,我们只创建了当前遍历fiber节点的dom节点。而并没有append上他的子节点。
js
const performUnitOfWork = (unitOfWork) => {
const current = unitOfWork.alternate;
let next;
next = beginWork(current, unitOfWork)
if (next === null) {
completeUnitOfWork(unitOfWork)
console.log('打印回溯后的节点', jsxObj)
} else {
nextUnitOfWork = next
}
}
所以我们打印出最终结构可以看到
span节点并没有挂载它的子文本节点我是一个dom节点,
所以我们在completeUnitOfWork中创建dom节点的同时还需要完成对子节点dom的挂载。
函数实现很简单直接挂
js
const appendAllChildren = (currentStateNode, currentCompleteWork) => {
// 完成对子节点dom的挂载
let fiberChild = currentCompleteWork.child;
while (fiberChild) {
currentStateNode.appendChild(fiberChild.stateNode)
// 通过链表append所有的child
fiberChild = fiberChild.siblings;
}
}
const completeUnitOfWork = () => {
// 源码中叫completedWork,容易跟函数名混淆
let currentCompleteWork = nextUnitOfWork;
// completeWork会给当前fiber创建好dom对象
while (currentCompleteWork !== null) {
if (!currentCompleteWork.stateNode) {
currentCompleteWork.stateNode = createDom(currentCompleteWork)
}
appendAllChildren(currentCompleteWork.stateNode, currentCompleteWork)
if (currentCompleteWork.siblings) {
// 如果当前节点有兄弟节点 拿兄弟节点执行beginWork
nextUnitOfWork = currentCompleteWork.siblings;
return
}
currentCompleteWork = currentCompleteWork.return;
}
nextUnitOfWork = null;
}
performUnitOfWork工作总结
1、执行performUnitOfWork
2、workInProgress有child -->执行 beginWork(构建当前节点 以及节点的子节点的child和sibings指针)
3、workInProgress无child 有sibings -> 执行completeWork(构建当前节点的dom & append子节点的dom)
4、没有siblings没有child -> workInProgress = workInProgress.fater 再次执行1 直到workInProgress = null
截止到目前为止,我们已经完成了挂载阶段的全部流程。
实现了挂载阶段的全部代码
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
#app {
word-break: break-all;
}
</style>
<body>
<div id="app"></div>
<!-- <script src="./react优先级调度算法-基础版.js"></script> -->
<script src="./libs/react.js"></script>
<script src="./libs/babel.js"></script>
</body>
</html>
<script type="text/babel">
const myJsxFn = (type, options) => {
return {
type,
props: options,
}
}
const actualDom = <div id='first' className='123'>
<span>我是一个dom元素</span>
我是一个普通文本节点
<div>
<span>我是第三个span节点</span>
</div>
</div>
// 下一个需要执行的单元
let nextUnitOfWork = null
// 是否需要让出进程
let shouldYield = false
const vDom = myJsxFn('div', {
id: 'first',
className: '123',
children: [
myJsxFn('span', {
children: '我是一个dom元素'
}),
'我是一个普通文本节点',
myJsxFn('span', {
children: '我是第三个span节点'
}),
],
})
let fiberRoot = null
// 当前内存中正在构建的fiber树
let workInProgressRootFiber = null
const Render = (jsx, container) => {
const rootFiber = jsx
jsx.return = null
// 赋值全局唯一的fiberRoot对象
if(fiberRoot === null) {
fiberRoot = {
type: 'FiberRoot',
current: rootFiber,
}
}
// 初次渲染,先手动挂个初始值
rootFiber.alternate = null
rootFiber.container = container
workInProgressRootFiber = rootFiber
// 初始化首次需要执行的单元
nextUnitOfWork = rootFiber
}
Render(vDom, document.getElementById('app'))
const workLoop = (deadline) => {
shouldYield = false
while (!shouldYield && nextUnitOfWork) {
performUnitOfWork(nextUnitOfWork)
if (deadline.timeRemaining < 1) {
shouldYield = true
}
}
if (workInProgressRootFiber && !nextUnitOfWork) {
commitWork()
} else {
requestIdleCallback(workLoop)
}
}
const beginWork = (current, wipFiber) => {
if (current !== null) {
// 执行diff更新流程 先省略
return
}
return reconCileChildren(current, wipFiber, wipFiber.props.children)
}
const createTextNode = (text, returnFiber) => {
return {
type: "Text",
nodeValue: text,
return: returnFiber,
alternate: null,
props: {
children: null
}
}
}
const reconCileChildren = (current, wipFiber, nextChildren) => {
if (current === null) {
if (typeof nextChildren === 'string') {
// 文本节点
const textNode = createTextNode(nextChildren, wipFiber)
wipFiber.child = textNode;
return wipFiber.child
}
if (wipFiber.type === 'Text') {
return null
}
let preChid = null
nextChildren.forEach((item, index) => {
if (typeof item === 'string') {
item = createTextNode(item, wipFiber)
}
item.alternate = null;
if (index === 0) {
// 如果是第一个子节点
wipFiber.child = item;
} else {
preChid.siblings = item;
}
item.return = wipFiber;
preChid = item;
})
}
return wipFiber.child
}
const createDom = (currentFiber) => {
if (currentFiber.type === 'Text') {
const TextNode = document.createTextNode(currentFiber.nodeValue)
return TextNode
}
const domNode = document.createElement(currentFiber.type)
Object.entries(currentFiber.props).forEach(([key, value]) => {
if (key === 'children') {
// 由于props中也含有children属性,但是children不需要作为props挂载到dom属性上
return;
}
domNode.setAttribute(key, value)
})
return domNode
}
const appendAllChildren = (currentStateNode, currentCompleteWork) => {
// 完成对子节点dom的挂载
let fiberChild = currentCompleteWork.child;
while (fiberChild) {
currentStateNode.appendChild(fiberChild.stateNode)
// 通过链表append所有的child
fiberChild = fiberChild.siblings;
}
}
const completeUnitOfWork = () => {
// 源码中叫completedWork,容易跟函数名混淆
let currentCompleteWork = nextUnitOfWork;
// completeWork会给当前fiber创建好dom对象
while (currentCompleteWork !== null) {
currentCompleteWork.stateNode = createDom(currentCompleteWork)
appendAllChildren(currentCompleteWork.stateNode, currentCompleteWork)
if (currentCompleteWork.siblings) {
// 如果当前节点有兄弟节点 拿兄弟节点执行beginWork
nextUnitOfWork = currentCompleteWork.siblings;
return
}
currentCompleteWork = currentCompleteWork.return;
}
nextUnitOfWork = null;
}
const performUnitOfWork = (unitOfWork) => {
const current = unitOfWork.alternate;
let next;
next = beginWork(current, unitOfWork)
if (next === null) {
completeUnitOfWork(unitOfWork)
} else {
nextUnitOfWork = next
}
}
requestIdleCallback(workLoop)
const commitWork = () => {
const targetDom = workInProgressRootFiber.container
const renderTemplate = workInProgressRootFiber.stateNode
targetDom.appendChild(renderTemplate)
// 更新fiberRoot的current指向当前页面展示的fiber树
fiberRoot.current = workInProgressRootFiber
workInProgressRootFiber = null
}
</script>
从updated到组件复用(简易的diff算法)
接下来将学习到组件更新时触发的diff算法讲到组件的复用。本文主要侧重点是react源码的思维范式,所以对schedule的实现,diff算法的详细实现都是通过语法糖来完成,毕竟diff算法,schedule的实现也算得上是react的核心,值得用单独的文章去学习。
修改render方法
从上面实现的render方法中,我们只考虑了挂载的场景,而在更新时,此render函数就没有起到触发workLoop函数以及以及触发diff的作用,所以我们修改render函数成以下结构
js
const Render = (jsx, container) => {
// 赋值全局唯一的fiberRoot对象
if (fiberRoot === null) {
fiberRoot = {
type: 'FiberRoot',
current: null,
}
}
const rootFiber = {
type: jsx.type,
props: jsx.props,
// 复用容器或旧 DOM
stateNode: fiberRoot.current ? fiberRoot.current.stateNode : null,
// 🔗 建立双向连接的关键!
alternate: fiberRoot.current,
container: container,
return: null
}
workInProgressRootFiber = rootFiber
// 初始化首次需要执行的单元
nextUnitOfWork = rootFiber
}
对比之前的实现,可以看到render函数中的rootFiber只新增了两个属性stateNode和alternate。stateNode是用于保存当前fiber对应的dom树,alternate属性用于保存上一次渲染的fiber树中对应的fiber节点(在前置知识---双缓存机制中有提到过)其对应关系如下图所示

修改reconcileChildren方法
在之前的学习中我们了解到reconcileChildren方法的作用是将传入的vdom节点转换成fiber节点,将所有子节点通过sibings串联起来,父节点通过child链接上第一个子节点,并且返回第一个子节点也就是child作为下一个reconcileChildren的入参。
因为reconcileChildren只会完成当前节点&其子节点的fiber转化工作。所以需要递归调用自身来完成整个fiber树的构建工作。
在之前的reconcileChildren函数实现中中我们只处理了current === null也就是初次挂载时的实现

然后在后续rerender时,
由于页面上已经有渲染的fiber树,current不会是null,所以在这里我们需要处理更新时的流程(也就是著名的diff算法 -> 打tag)

打上标记之后,需要消费这个标记

同样的,对于之前的版本,我们增加了对组件tag为"UPDATE"时的处理,避免了重复创建组件的dom元素,但是对"UPDATE"的元素我们还需要有一个更新的过程(有可能是props,有可能是Text发生了变化)这个改动我们体现在appendChildren函数里。
这里我们增加了对tag为UPDATE的fiber的特殊处理

至此,所有函数修改完成。 再次出发render函数看有没有变化
js
const updateVDom = myJsxFn('div', {
id: 'first33333',
className: '123',
children: [
myJsxFn('span', {
id: '我是更新之后的span元素',
children: '我是一个dom元素11111'
}),
'我是一个普通文本节点12',
myJsxFn('span', {
children: '我是第三个span节点哈哈哈'
}),
],
})
setTimeout(() => {
// 开始更新react
Render(updateVDom, document.getElementById('app'))
}, 3000);
三秒之后页面成功渲染了修改之后的dom,更新完成!

全部测试代码
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
#app {
word-break: break-all;
}
</style>
<body>
<div id="app"></div>
<!-- <script src="./react优先级调度算法-基础版.js"></script> -->
<script src="./libs/react.js"></script>
<script src="./libs/babel.js"></script>
</body>
</html>
<script type="text/babel">
const myJsxFn = (type, options) => {
return {
type,
props: options,
}
}
const actualDom = <div id='first' className='123'>
<span>我是一个dom元素</span>
我是一个普通文本节点
<div>
<span>我是第三个span节点</span>
</div>
</div>
// 下一个需要执行的单元
let nextUnitOfWork = null
// 是否需要让出进程
let shouldYield = false
const vDom = myJsxFn('div', {
id: 'first',
className: '123',
children: [
myJsxFn('span', {
id: '我是第一个span元素',
children: '我是一个dom元素'
}),
'我是一个普通文本节点',
myJsxFn('span', {
children: '我是第三个span节点'
}),
],
})
const updateVDom = myJsxFn('div', {
id: 'first33333',
className: '123',
children: [
myJsxFn('span', {
id: '我是更新之后的span元素',
children: '我是一个dom元素11111'
}),
'我是一个普通文本节点12',
myJsxFn('span', {
children: '我是第三个span节点哈哈哈'
}),
],
})
let fiberRoot = null
// 当前内存中正在构建的fiber树
let workInProgressRootFiber = null
const Render = (jsx, container) => {
// 赋值全局唯一的fiberRoot对象
if (fiberRoot === null) {
fiberRoot = {
type: 'FiberRoot',
current: null,
}
}
const rootFiber = {
type: jsx.type,
props: jsx.props,
// 复用容器或旧 DOM
stateNode: fiberRoot.current ? fiberRoot.current.stateNode : null,
// 🔗 建立双向连接的关键!
alternate: fiberRoot.current,
container: container,
return: null
}
workInProgressRootFiber = rootFiber
// 初始化首次需要执行的单元
nextUnitOfWork = rootFiber
}
Render(vDom, document.getElementById('app'))
const workLoop = (deadline) => {
shouldYield = false
while (!shouldYield && nextUnitOfWork) {
performUnitOfWork(nextUnitOfWork)
if (deadline.timeRemaining < 1) {
shouldYield = true
}
}
if (workInProgressRootFiber && !nextUnitOfWork) {
commitWork(workInProgressRootFiber)
}
requestIdleCallback(workLoop)
}
const beginWork = (current, wipFiber) => {
return reconCileChildren(current, wipFiber, wipFiber.props.children)
}
const createTextNode = (text, returnFiber, alternate = null) => {
return {
type: "Text",
nodeValue: text,
return: returnFiber,
alternate: alternate,
props: {
children: null
},
flag: alternate ? 'UPDATE' : null
}
}
const reconCileChildren = (current, wipFiber, nextChildren) => {
let preChid = null
if (current === null) {
// 挂载时
if (typeof nextChildren === 'string') {
// 文本节点
const textNode = createTextNode(nextChildren, wipFiber)
wipFiber.child = textNode;
return wipFiber.child
}
if (wipFiber.type === 'Text') {
return null
}
nextChildren.forEach((item, index) => {
if (typeof item === 'string') {
item = createTextNode(item, wipFiber)
}
item.alternate = null;
if (index === 0) {
// 如果是第一个子节点
wipFiber.child = item;
} else {
preChid.siblings = item;
}
item.return = wipFiber;
preChid = item;
})
} else {
// 更新时
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
if (typeof nextChildren === 'string') {
const isSameType = oldFiber.type === 'Text' && oldFiber.nodeValue === nextChildren;
// 文本节点
if (isSameType) {
const newFiber = {
...oldFiber,
alternate: oldFiber,
flag: 'UPDATE',
return: wipFiber,
}
wipFiber.child = newFiber
return newFiber
} else {
return createTextNode(nextChildren, wipFiber, oldFiber)
}
}
if (wipFiber.type === 'Text') {
return null
}
nextChildren.forEach((item, index) => {
const isSameType = oldFiber && item && item.type === oldFiber.type;
let newFiber = null;
if (isSameType) {
newFiber = item
newFiber.alternate = oldFiber
oldFiber.alternate = newFiber
newFiber.flag = 'UPDATE'
} else {
if (typeof item === 'string') {
newFiber = createTextNode(item, wipFiber, oldFiber);
} else {
// 挂载 or 新节点(span->div)
newFiber = {
type: item.type,
}
}
}
if (index === 0) {
// 如果是第一个子节点
wipFiber.child = newFiber;
} else {
preChid.siblings = newFiber;
}
// 移动oldFiber指针
oldFiber = oldFiber.siblings;
newFiber.return = wipFiber;
preChid = newFiber;
})
}
return wipFiber.child
}
const createDom = (currentFiber) => {
if (currentFiber.type === 'Text') {
const TextNode = document.createTextNode(currentFiber.nodeValue)
return TextNode
}
const domNode = document.createElement(currentFiber.type)
Object.entries(currentFiber.props).forEach(([key, value]) => {
if (key === 'children') {
// 由于props中也含有children属性,但是children不需要作为props挂载到dom属性上
return;
}
domNode.setAttribute(key, value)
})
return domNode
}
const appendAllChildren = (currentStateNode, currentCompleteWork) => {
// 完成对子节点dom的挂载
let fiberChild = currentCompleteWork.child;
while (fiberChild) {
if (fiberChild.flag === 'UPDATE') {
// 如果fiberChild的flag为'update',则直接复用dom
updateDomProperties(fiberChild.stateNode, fiberChild)
}
currentStateNode.appendChild(fiberChild.stateNode)
// 通过链表append所有的child
fiberChild = fiberChild.siblings;
}
}
const updateDomProperties = (currentStateNode, currentCompleteWork) => {
Object.entries(currentCompleteWork.props).forEach(([key, value]) => {
if (key === 'children') {
// 由于props中也含有children属性,但是children不需要作为props挂载到dom属性上
return;
}
currentStateNode.setAttribute(key, value)
})
}
const completeUnitOfWork = () => {
// 源码中叫completedWork,容易跟函数名混淆
let currentCompleteWork = nextUnitOfWork;
// completeWork会给当前fiber创建好dom对象
while (currentCompleteWork !== null) {
if (currentCompleteWork.flag === 'UPDATE') {
// tag为'update'的节点有stateNode可以直接复用,不需要创建dom了
if (currentCompleteWork?.alternate?.stateNode) {
if (currentCompleteWork.type === 'Text') {
currentCompleteWork.alternate.stateNode.nodeValue = currentCompleteWork.nodeValue
}
currentCompleteWork.stateNode = currentCompleteWork.alternate.stateNode
}
}
if (currentCompleteWork.flag !== 'UPDATE') {
// 不是"update"的节点我们执行更新流程
currentCompleteWork.stateNode = createDom(currentCompleteWork)
}
appendAllChildren(currentCompleteWork.stateNode, currentCompleteWork)
if (currentCompleteWork.siblings) {
// 如果当前节点有兄弟节点 拿兄弟节点执行beginWork
nextUnitOfWork = currentCompleteWork.siblings;
return
}
currentCompleteWork = currentCompleteWork.return;
}
nextUnitOfWork = null;
}
const performUnitOfWork = (unitOfWork) => {
const current = unitOfWork.alternate;
let next;
next = beginWork(current, unitOfWork)
if (next === null) {
completeUnitOfWork(unitOfWork)
} else {
nextUnitOfWork = next
}
}
requestIdleCallback(workLoop)
const commitWork = () => {
const targetDom = workInProgressRootFiber.container
const renderTemplate = workInProgressRootFiber.stateNode
targetDom.appendChild(renderTemplate)
// 更新fiberRoot的current指向当前页面展示的fiber树
fiberRoot.current = workInProgressRootFiber
workInProgressRootFiber = null
}
setTimeout(() => {
// 开始更新react
Render(updateVDom, document.getElementById('app'))
}, 3000);
</script>
TODOLIST
对diff算法以及schedule的描述这里我们都用简单的方式带过了,caus这两章节的知识值得用单独的文章去学习