这是我参与更文挑战的第24天,活动详情查看: 更文挑战
前言
大家好我是小村儿,上一节我们实现了创建任务队列,添加任务,使用requestIdelCallback
api利用浏览器空闲时间实现任务的调度逻辑。接下来我们用代码构建一个Fiber对象。
- 构建根节点Fiber对象
- 构建子级节点Fiber对象
- 完善Fiber对象的stateNode和tag属性
- 构建左侧节点数中的剩余子节点Fiber对象
- 构建剩余子节点的fiber对象
构建根节点Fiber对象
接上一章,我们最后完成任务调度之后需要去执行任务了,这个任务是什么样的任务呢?这个任务就是为每个根据节点VirtualDOM对象构建Fiber对象。
这个任务应该被怎样去执行呢?或者说我们要从哪一个节点开始构建?节点构建顺序又是怎样的?看下图:
我们是从最外层去开始构建的,也就是VirtualDOM树的根节点,构建完成后之后接下来就开始接下来的两个子级节点,构建完之后就会去指定这三个组件之间的关系,只有从左第一个子级才是父级的子级(child),从左往右算第二个就算第一个子级的下一个兄弟节点这样来构建。确定关系之后,再去找第一个子级节点的子节点,还是最左边的去构建这个节点的Fiber对象,构建完成后之后,在构建该子级的两个子级,然后确定他们之间的关系,确定关系之后发现没了子级,就会去找子节点的同级,按照深度遍历顺序去构建。
我们来尝试构建根节点的Fiber对象,也就是id为root的节点,他的子节点就是我们jsx里面最外层div包着的就是子节点。
<div>
<p>Hello React</p>
</div>
复制代码
我们先回顾一下任务调度的流程,当调用render方法的时候,我们给任务队列添加了一个对象,有两个属性一个是dom
,目前根节点则是id为root的根节点 一个是props
.值是一个对象,这个对象children属性,他的值就是父级的子级节点。在浏览器空闲的时候呢就会去执行performTask这个代码
requestIdelCallback(performTask)
.在执行performTask方法的时候回去调用workLoop这个方法,这个方法就开始去执行任务了,首先回去查看一下subTask是否有值,在第一次执行的时候subTask值为空,这时候就会去调用getFirstTask方法获取任务。
getFirstTask这个方法我们之前只做了一个函数初始化,我们现在来完成他。getFirstTask就是获取任务队列中的第一个排在最前面的小任务,通过第一个小任务对象,构建根节点的fiber对象.
// fiber对象基本结构
{
type 节点类型(元素,文本,组件)(具体的类型)
props 加点属性
stateNode 节点DOM对象 | 组件实例对象
tag 节点标记(对具体类型的分类 hostRoot || hostComponent || classComponent || functionComponent)
effects 数组,存储需要更改的fiber对象
effectTag 当前Fiber要被执行的操作(新增,删除,修改)
parent 当前Fiber的下一个父级Fiber
child 当前Fiber的下一个子级Fiber
sibling 当前Fiber的下一个兄弟Fiber
alternate Fiber 备份 fiber比对时使用
}
复制代码
对于根节点fiber对象不需要指定type属性,我们直接构建props属性,值为task.props.stateNode属性存储的是当前节点DOM对象,tag是一个标记,根节点值为”host_root“,接下来是effects数组,这个数组我们先不去获取暂时给一个空数组为值,暂时还用不到。根节点也不需要用effectTag属性,因为不需要新增删除和修改。也没有parent因为这是根节点,最后还需要配置上一个child子级节点属性,值当前没有先指定为null,关于alternate在对比时候再用,现在暂时不指定。
const getFirstTask = () => {
/**
* 从任务队列中获取任务
*/
const task = taskQueue.pop()
/**
* 返回最外层节点的fiber对象
*/
return {
props: task.props,
stateNode: task.dom,
tag: "host_root",
effects: [],
child: null
}
}
复制代码
这个对象返回之后就赋值给subTask
if(!subTask) {
subTask = getFirstTask()
console.log(subTask)
}
复制代码
效果:
一个根节点fiber对象构建完成!!!
拿到Fiber对象之后且浏览器有空闲时间的时候,就执行workLoop方法里面while循环,就会把根节点fiber对象传递给方法executeTask方法,这样我们整个任务调度和fiber对象构建就算运转起来了
const workLoop = deadline => {
if(!subTask) {
// 这里的subTask就是一个fiber对象
subTask = getFirstTask()
}
while(subTask && deadline.timeRemaining() > 1) {
subTask = executeTask(subTask)
}
}
复制代码
现在就知道executeTask的形参被命名为fiber了。因为得到的就是根节点的fiber对象
构建子级节点Fiber对象
子级节点FIber对象的构建在executeTask方法中构建完成,executeTask会调用reconcileChildren方法,第一个参数为fiber(父级fiber对象),第二个参数为子级VitrualDOM对象通过fiber.props.children获取,
1. 实现reconcileChildren方法
关于这个方法第二个参数为children,有可能是一个对象有可能是一个数组,当我们调用render方法的时候children,我们传element是一个对象那就是一个对象,如果不是我们传的是createElement返回的那么children就是一个数组
export const render = (element, dom) => {
taskQueue.push({
dom,
props: {children: element} // 这个element是对象
})
createElement.js
// children是一个数组
export default function createElement(type, props, ...children) {
const childElements = [].concat(...children).reduce((result, child) => {
if (child !== false && child !== true && child !== null) {
if (child instanceof Object) {
result.push(child)
} else {
result.push(createElement("text", { textContent: child }))
}
}
return result
}, [])
return {
type,
props: Object.assign({ children: childElements }, props),
}
}
复制代码
思路:
因为有可能为数组又有可能为对象,这样的参数传过来特别影响代码之后的操作,所以我们定义一个方法在这个方法判断这个参数是否是数组,如果是数组就直接返回,如果是对象包裹在一个数组中然后返回出来.
实现:
我们在杂项Misc这个目录里面创建一个文件夹Arrified文件夹,在这个文件夹想创建index.js这里去实现将children对象转化为数组
const arrified = arg => Array.isArray(arg) ? arg : [arg]
export default arrified
const reconcileChildren = (fiber, children) => {
/**
* children 可能是对象,也有可能是数组
* 将children转换成数组
*/
console.log(children)
const arrifiedChildren = arrified(children);
console.log(arrifiedChildren)
}
复制代码
接下来将arrifiedChildren数组中VitrualDOM转化为Fiber对象。我们需要一个循环去将数组中的VirtualDOM构建出一个fiber对象
const reconcileChildren = (fiber, children) => {
/**
* children 可能是对象,也有可能是数组
* 将children转换成数组
*/
const arrifiedChildren = arrified(children);
let index = 0;
let numberOfElements = arrifiedChildren.length;
let element = null;
let newFiber = null
while(index < numberOfElements) {
element = arrifiedChildren[index];
newFiber = {
type: element.type,
props: element.props,
tag: "host_component",
effects: [],
effectTag: "placement",
stateName: null,
parent: fiber
}
fiber.child = newFiber
index++
}
}
复制代码
这时候就完成了每个DOM对象构建成Fiber的工作,type和props直接从element获取,我们现在还没处理组件节点,根节点只有一个之前已经生成,说明现在就是普通节点tag就是”host_component“, effects我们还是先定义为空数组,后面用到。effectTag是操作的标识而已成为”placement“,stateNode就是当前节点DOM对象,等下我们来完善这个值暂且定义为null。到这里差不多结束了一个子节点的初步构建,但是还是要添加节点关系属性,谁是谁的父级,谁是谁的子集,很明显当前传进来的fiber就是这个节点fiber对象的父级。这时候我们还要为fiber添加子级(child)值为newFiber。
到这里还有一些小问题,如果子节点有多个,第一个子节点才是父节点的子节点,其他子节点都是彼此都是兄弟节点,第二个是第一个兄弟节点,但三个是第二个兄弟节点。所以是第一个子节点才去设置子节点child属性,其他的都需要设置兄弟节点
const reconcileChildren = (fiber, children) => {
/**
* children 可能是对象,也有可能是数组
* 将children转换成数组
*/
const arrifiedChildren = arrified(children);
let index = 0;
let numberOfElements = arrifiedChildren.length;
let element = null;
let newFiber = null
let prevFiber = null
while(index < numberOfElements) {
element = arrifiedChildren[index];
newFiber = {
type: element.type,
···
}
// 为0是子节点,其他为彼此的兄弟节点
if(index === 0) {
fiber.child = newFiber;
} else {
prevFiber.sibling = newFiber
}
// 处理完之后最后将前一个fiber对象存储起来
prevFiber = newFiber;
index++
复制代码
2. 设置stateNode属性
stateNode属性我们使用createStateNode函数调用去获取。需要参数是当前节点fiber对象newFiber。createStateNode函数里当判断fiber对象tag为”host_component“时,则去生成DOM元素对象。如果是组件的话就应该存储的是组件实例对象。
createDOMElement去生成对应的普通DOM元素对象。
// react/reconciliation/index.js
newFiber.stateNode = createStateNode(newFiber)
// src/react/Misc/createStateNode/index.js
import { createDOMElement } from '../../DOM'
const createStateNode = fiber => {
if(fiber.tag === "host_component") {
return createDOMElement(fiber)
}
}
export default createStateNode
复制代码
在文件夹Misc中创建文件夹为DOM,专门对于DOM元素的一些操作都在这里createDOMElement.js
// src/react/DOM/createDOMElement.js
import updateNodeElement from "./updateNodeElement"
export default function createDOMElement(virtualDOM) {
let newElement = null
if (virtualDOM.type === "text") {
// 文本节点
newElement = document.createTextNode(virtualDOM.props.textContent)
} else {
// 元素节点
newElement = document.createElement(virtualDOM.type)
updateNodeElement(newElement, virtualDOM)
}
return newElement
}
复制代码
updateNodeElement为元素节点的更新操作,这里就不贴代码,这个和tinyreact一样,查看详情可以github/tinyReact查看或者翻到前面tinyreact文章也可以查看。
图中可以看到stateNode已经生成。
3. 设置tag属性
我们在处理子节点时,每一个子节点的类型都是不一样的。我们通过一个方法去判定节点的类型,通过不同类型给tag设置值.
// src/react/Misc/getTag/index.js
const getTag = vdom => {
if(typeof vdom.type === 'string') {
return "host_component"
}
}
export default getTag
复制代码
4. 构建左侧节点数中的剩余子节点Fiber对象
以上我们已经把根节点,和他的第一层子节点和第一层子节点的同级兄弟节点都已经构建fiber对象并且关联了起来,接下来我们要继续往下查找子节点继续构建fiber对象。
按照前面构建节点的顺序,应该找到第二层最左侧的子节点开始构建fiber对象,这个节点我们应该怎么去构建呢?在代码当中,我们使用最小规律查找子级原则,找到一个节点(我们可以以根节点为例),查看他是否有子级,如果有就将这个子级和这个子级的children对象传递给 reconcileChildren(fiber, fiber.props.children)进行创建fiber对象。以此类推,将这个子级的子级作为父级,他的children传递给reconcileChildren继续创建。
const executeTask = fiber => {
reconcileChildren(fiber, fiber.props.children)
// 判断当前fiber.child是否有值,有值则返回fiber.child
if(fiber.child) {
return fiber.child
}
console.log(fiber)
}
const workLoop = deadline => {
···
while(subTask && deadline.timeRemaining() > 1) {
subTask = executeTask(subTask)
}
}
复制代码
虽然只是加了一个判断 返回了fiber.child,这个代码执行还是蛮不好理解的。当执行完reconcileChildren,fiber.child有值的话,executeTask返回一个新的fiber对象,workLoop方法里subTask重新被赋值继续执行executeTask,直到executeTask子节点为空,结束循环。这样左侧子节点树DOM构建Fiber对象。
5. 构建剩余子节点的fiber对象
上面已经构建完了所有左侧的子节点,现在我们要构建所有剩余节点的fiber对象。当左侧节点构建完成之后我们定位的应该是最后一个子节点,就根据这个最后一个子节点去查找剩余节点,如果当前节点有同级就去构建该节点。如果没有就退回他的父级,查看他的父级有没有同级,一直退回查找构建fiber对象,将所有剩余子节点的构建fiber对象
// 多添加一个p节点
import React, {render} from "./react"
const root = document.getElementById("root")
const jsx = (<div>
<p>Hello React</p>
<p>我是同级子节点</p>
</div>)
console.log(jsx)
render(jsx, root)
let currentExecutedFiber = fiber
while(currentExecutedFiber.parent) {
// 有同级返回同级
if(currentExecutedFiber.sibling){
return currentExecutedFiber.sibling
}
// 退到父级
currentExecutedFiber = currentExecutedFiber.parent
}
复制代码
这样我们就完成了所有子节点的fiber对象的构建!!!
总结
今天完成了根节点和所有子节点的fiber对象的构建,获取根节点的vittualDOM之后根据之前提供fiber对象模型去创建根fiber对象。接着创建子级的fiber对象,第一层子级直接第一个参数传fiber,第二个传递fiber.props.children创建当前子节点对象。后面的子节点继续之前逻辑去生成fiber对象,当子级属性有值,则返回friber.child. 创建完左侧子节点之后,代码运行轨迹到了最后一个子节点,如果这个子节点有兄弟节点,则生成该节点fiber,知道同级子节点为空。继续退回上级执行此操作完成所有fiber对象的创建
好了!!!今天到此为止,下一节我们学习构建effects数组,进入fiber第二阶段commit,实现初始化渲染,敬请期待!!!