在上一节中,介绍了React为什么采用Fiber替换原有的虚拟Dom对比更新方式及一些Fiber基础概念,本节将模拟实现首次渲染时如何构建Fiber树及实现节点挂载。
实现workLoop
接着上一节,performTask方法会利用浏览器空闲时间不断的执行workLoop方法,在该方法中,会首先判断全局对象subTask是否存在,如果不存在就创建根Fiber对象,然后利用while循环构建其余的Fiber对象,当所有Fiber对象构建完成之后,就执行commit操作。
// 子任务
let subTask = null
// commit操作标志
let pendingCommit = null
const workLoop = deadline => {
// 1. 构建根对象
if (!subTask) {
subTask = getFirstTask()
}
// 2. 通过while循环构建其余对象
while (subTask && deadline.timeRemaining() > 1) {
subTask = executeTask(subTask)
}
// 3. 执行commit操作,实现Dom挂载
if (pendingCommit) {
commitAllWork(pendingCommit)
}
}
构建根节点Fiber对象
声明getFirstTask方法用于构建根节点Fiber对象:
const getFirstTask = () => {
// 获取任务队列中的任务
const task = taskQueue.pop()
// 返回Fiber对象
return {
props: task.props,
stateNode: task.dom,
// 代表虚拟Dom挂载的节点
tag: "host_root",
effects: [],
child: null
}
}
构建其余Fiber对象
当构建完根Fiber节点之后,通过while循环构建其余fiber节点,executeTask方法分成两个大的部分,一部分是从上到下遍历VDom树,另一部分是从下到上遍历VDom树,并为父节点收集所有子节点信息。
const executeTask = fiber => {
// 构建子fiber对象
if (fiber.tag === "class_component") {
reconcileChildren(fiber, fiber.stateNode.render())
} else if (fiber.tag === "function_component") {
reconcileChildren(fiber, fiber.stateNode(fiber.props))
} else {
reconcileChildren(fiber, fiber.props.children)
}
// 如果子级存在 返回子级,将这个子级当做父级 构建这个父级下的子级
if (fiber.child) {
// return之后,workLoop方法会将返回值赋值给subTask,然后继续执行本方法
return fiber.child
}
// 当执行此段代码时,说明第一次从上到下的遍历已经完成,需要从下到上构建剩余的fiber对象,思路就是判断是否存在同级,如果存在,则构建同级的子级,如果没有同级,则查看父级是否存在同级。
let currentExecutelyFiber = fiber
while (currentExecutelyFiber.parent) {
// 收集所有子节点信息
currentExecutelyFiber.parent.effects = currentExecutelyFiber.parent.effects.concat(
currentExecutelyFiber.effects.concat([currentExecutelyFiber])
)
if (currentExecutelyFiber.sibling) {
return currentExecutelyFiber.sibling
}
currentExecutelyFiber = currentExecutelyFiber.parent
}
pendingCommit = currentExecutelyFiber
}
在excuteTask方法中会调用reconcileChildren方法创建子fiber对象,在children数组中分为两类:第一个和其余,第一个将添加到父节点的child属性上,其余的将添加到前一个子节点的sibling属性中,这样就构成了节点之间的关系。
// 确保children是一个数组
const arrified = arg => (Array.isArray(arg) ? arg : [arg])
const reconcileChildren = (fiber, children) => {
// 将children 转换成数组
const arrifiedChildren = arrified(children)
let index = 0
let numberOfElements = arrifiedChildren.length
// 循环过程中的循环项 就是子节点的 virtualDOM 对象
let element = null
// 子级 fiber 对象
let newFiber = null
// 上一个兄弟 fiber 对象
let prevFiber = null
while (index < numberOfElements) {
element = arrifiedChildren[index]
newFiber = {
type: element.type,
props: element.props,
tag: getTag(element),
effects: [],
effectTag: "placement",
parent: fiber
}
// 为fiber节点添加DOM对象或组件实例对象
newFiber.stateNode = createStateNode(newFiber)
// 构建关系
if (index === 0) {
fiber.child = newFiber
} else if (element) {
prevFiber.sibling = newFiber
}
// 更新上一个fiber对象
prevFiber = newFiber
index++
}
}
createStateNode方法用于为fiber对象构建createStateNode属性,fiber对象可以大致分为两类:原生的Dom元素和自定义类或者函数。
const createReactInstance = fiber => {
let instance = null
if (fiber.tag === "class_component") {
// 创建实例
instance = new fiber.type(fiber.props)
} else {
// 函数没有实例,直接返回函数本身
instance = fiber.type
}
return instance
}
const createStateNode = fiber => {
if (fiber.tag === "host_component") {
// 此处复用了实现React虚拟Dom中的源码
return createDOMElement(fiber)
} else {
return createReactInstance(fiber)
}
}
通过上述代码,完成除根节点之外其余fiber对象的构建。
节点挂载
当所有fiber对象遍历构建完成之后,将执行commit挂载操作。此时全局对象pendingCommit存储的是根fiber对象,而根fiber对象的effects属性中存储着fiber树中所有的其他fiber对象,此时只需要遍历effects,然后通过appendChild方法插入到Dom树中即可。
const commitAllWork = fiber => {
// 循环 effets 数组 构建 DOM 节点树
fiber.effects.forEach(item => {
if (item.tag === "class_component") {
item.stateNode.__fiber = item
}
if (item.effectTag === "placement") {
let fiber = item
let parentFiber = item.parent
// 找到普通节点父级 排除组件父级,因为组件父级是不能直接追加真实DOM节点的
while (
parentFiber.tag === "class_component" ||
parentFiber.tag === "function_component"
) {
parentFiber = parentFiber.parent
}
// 如果子节点是普通节点 找到父级 将子节点追加到父级中
if (fiber.tag === "host_component") {
parentFiber.stateNode.appendChild(fiber.stateNode)
}
}
})
}
此时,完成模拟实现Fiber首次渲染。下一节将继续模拟实现对比更新功能。