构建Fiber对象第二阶段
接下来我们要做的就是将所有 Fiber 对象存储在数组中,为什么要存在数组中呢?
- 因为 Fiber 算法的第二阶段,我们要循环这个数组统一获取 fiber 对象从而构建真实 DOM 对象,并且要将构建起来的真实 DOM 挂在到页面当中
我们要怎么构建这个数组呢?
- 所有 fiber 对象都有 effects 数组,最外层 fiber 对象的 effects 数组存放所有的 fiber 对象
- 其他 effects 数组负责协助收集 fiber 对象,最终我们将所有收集的 fiber 对象汇总收集到最外层 fiber 对象的 effects 数组中
构建 Effect 数组
思路:
- 当左侧节点树种的节点全部构建完成以后,我们开启一个 while 循环去构建其他节点过程时,我们会找到每一个节点父级 fiber 对象,这样我们就可以为每一个节点 effects 数组添加 fiber 对象了
const executeTask = fiber => {
console.log(fiber)
let currentExecuteFiber = fiber
while (currentExecuteFiber.parent) {
currentExecuteFiber.parent.effects = currentExecuteFiber.parent.effects.concat(
currentExecuteFiber.effects.concat([currentExecuteFiber])
)
}
console.log(currentExecuteFiber)
}
实现初始化渲染
在 fiber 第二个阶段当中我们要做真实 dom 操作,我们要去构建 dom 节点之间的关系,在构建完成后我们要把真实 dom 添加到页面当中
将 currentExecutedFiber 提升为全局变量 因为在调用第二阶段渲染函数的时候,需要 currentExecutedFiber 这个变量传递给第二阶段的方法
jsxlet pendingCommit = null const commitAllWork = fiber => { console.log(fiber.effects) } const executeTask = fiber => { // ... pendingCommit = currentExecuteFiber } const workLoop = deadline => { // ... if (pendingCommit) { commitAllWork(pendingCommit) } } const jsx = (<div> <p>Hello React</p> <p>我是同级子节点</p> </div>)
将 currentExecutedFiber 在 commitAllWork 遍历,操作 fiber 对象
第一个是文本节点,所以我们是倒序获取 fiber 对象的,也就是从左侧最后一个子节点开始收集的,我们先确定每个 fiber 对象的 effectTag 是什么类型再对其进行相应的操作,如果是 placement 则进行节点追加操作
jsconst commitAllWork = fiber => { fiber.effects.forEach(item => { if (item.effectTag === 'placement') { item.parent.stateNode.appendChild(item.stateNode) } }) }
类组件处理
准备类组件
首先我们准备好一个类型组件,类组件需要继承 Component 类,并新建一个文件夹 Component 在里面创建 index.js 存放 Component 类
import React, { render, Component } from './react'
class Greeting extends Component {
constructor(props) {
super(props)
}
render() {
return <p>Hello React</p>
}
}
render(<Greeting />, root)
// Component/index.js
export class Component {
constructor(props) {
this.props = props
}
}
构建组件 tag 属性
这样处理后,打印 newFiber,得到的 tag 是 undefined,因为 getTag 方法中我们只处理了 type -> string 的情况
import { Component } from '../../Component'
const getTag = vdom => {
if (typeof vdom.type === 'string') {
return 'host_component'
} else if (Object.getPrototypeOf(vdom.type) === Component) {
return 'class_component'
} else {
return 'function_component'
}
}
构建组件 stateNode 属性
stateNode 还是 undefined,如果这个 DOM 节点是一个普通的元素则存储的是一个普通的 DOM 实例对象,一共处理两种组件:函数、类组件
// Misc/CreateReactInstance/index.js
export const createReactInstance = fiber => {
let instance = null
if (fiber.tag === 'class_component') {
instance = new fiber.type(fiber.props)
} else {
instance = fiber.type
}
return instance
}
// Misc/CreateStateNode/index.js
import { createDOMElement } from '../../DOM'
import { createReactInstance } from '../CreateReactInstance'
const createStateNode = fiber => {
if (fiber.tag === 'host_component') {
return createDOMElement(fiber)
} else {
return createReactInstance(fiber)
}
}
优化 executeTask 方法
如果是组件,他的子级就不是直接 fiber.props.children 了,而是类组件里面 render 方法返回的内容,所以调用 reconcileChildren 方法的时候需要帕努单是组件还是普通元素,是组件的话调用 render 方法
const executeTask = fiber => {
if (fiber.tag === 'class_component') {
reconcileChildren(fiber, fiber.stateNode.render())
} else {
reconcileChildren(fiber, fiber.props.children)
}
// ...
}
这样就拿到组件子元素了
commitAllWork 处理
因为类组件本身也是一个节点,但是类组件本身的节点是不可以追加元素的。所以我们要往父级查找,直到父级不是组件而是普通元素就开始做元素追加操作
const commitAllWork = fiber => {
fiber.effects.forEach(item => {
if (item.effectTag === 'placement') {
let fiber = item
let parentFiber = item.parent
while (parentFiber.tag === 'class_component') {
parentFiber = parentFiber.parent
}
if (fiber.tag === 'host_component') {
parentFiber.stateNode.appendChild(fiber.stateNode)
}
item.parent.stateNode.appendChild(item.stateNode)
}
})
}
函数组件处理
准备一个函数式组件
function FnComponent(){
return <div>FnComponent</div>
}
render(<FnComponent />, root)
executeTask 处理函数组件
const executeTask = 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)
}
}
处理函数组件内容渲染到页面
const commitAllWork = fiber => {
fiber.effects.forEach(item => {
if (item.effectTag === 'placement') {
let fiber = item
let parentFiber = item.parent
while (
parentFiber.tag === 'class_component' ||
parentFiber.tag === 'function_component'
) {
parentFiber = parentFiber.parent
}
if (fiber.tag === 'host_component') {
parentFiber.stateNode.appendChild(fiber.stateNode)
}
item.parent.stateNode.appendChild(item.stateNode)
}
})
}
更新节点 oldFiber/newFiber
这两个JSX只是从 Hello React 替换成 奥利给 操作,当 dom 初始化完成渲染之后,我们去备份旧的 fiber 对象,在两秒钟之后我们又调用了 render 方法,就会去创建新的 fiber 对象了
创建 fiber 对象的时候我们会看一下 fiber 节点存不存在,如果旧的 fiber 对象存在则说明当前我们要执行更新操作
const jsx = (
<div>
<p>Hello React</p>
<p>Hi Fiber</p>
</div>
)
render(jsx, root)
setTimeout(() => {
const jsx = (
<div>
<p>奥利给</p>
<p>Hi Fiber</p>
</div>
)
render(jsx, root)
}, 2000)
commitAllWork 方法,执行的就是 dom 操作
const commitAllWork = fiber => {
// ...
// 备份旧的 fiber 节点对象
fiber.stateNode.__rootFiberContainer = fiber
}
在创建新 fiber 对象的时候,将旧 fiber 对象存储到 alternate 中,在构建更新 fiber 对象的时候会用
const getFirstTask = () => {
// ...
return {
// ...
alternate: task.dom.__rootFiberContainer
}
}
reconcileChildren
这个方法我们要判断 fiber 对象需要进行什么样的操作,去构建不同操作类型的 fiber 对象
先获取备份节点
定义一个 alternate 变量,接收备份节点,如果 fiber.alternate 有值则说明有备份节点,则获取备份节点的子节点
如果都有值将 fiber.alternate.child 赋值给 alternate
jslet alternate = null if (fiber.alternate && fiber.alternate.child) { alternate = fiber.alternate.child }
更新各个子节点的 alternate,当方法里面不循环不执行的时候,第一次循环就是第一个子节点,第二次是找到第二个子节点...
jsif (alternate && alternate.sibling) { alternate = alternate.sibling } else { alternate = null }
确定操作类型,我们是做初渲染?还是更新?还是删除?
如果 element 存在,alternate 不存在,做的是初始渲染操作
如果 element 存在,alternate 存在,做的是更新操作
jsconst reconcileChildren = (fiber, children) => { // ... while (index < numberOfElements) { /** * 子级 virtualDOM 对象 */ element = arrifiedChildren[index] if (element && alternate) { // 更新操作 newFiber = { type: element.type, props: element.props, tag: getTag(element), effects: [], effectTag: 'update', parent: fiber, alternate } if (element.type === alternate.type) { // 类型相同 newFiber.stateNode = alternate.stateNode } else { // 类型不同 newFiber.stateNode = createStateNode(newFiber) } } else if (element && !alternate) { // 初始渲染 /** * 子级 fiber 对象 */ newFiber = { type: element.type, props: element.props, tag: getTag(element), effects: [], effectTag: 'placement', parent: fiber } /** * 为 fiber 节点添加 DOM 对象或组件实例对象 */ newFiber.stateNode = createStateNode(newFiber) console.log(newFiber) // 为父级 fiber 添加子级 fiber if (index === 0) { fiber.child = newFiber } else { // 为 fiber 添加下一个兄弟 fiber prevFiber.sibling = newFiber } if (alternate && alternate.sibling) { alternate = alternate.sibling } else { alternate = null } prevFiber = newFiber index++ } } }
执行 DOM 操作
在 commitAllWork 中判断 effectTag 是 update 时对 dom 进行更新
jsconst commitAllWork = fiber => { fiber.effects.forEach(item => { if (item.effectTag === 'update') { if (item.type === item.alternate.type) { // 节点类型相同 updateNodeElement(item.stateNode, item, item.alternate) } else { // 节点类型不同 item.parent.stateNode.replaceChild(item.stateNode, item.alternate.stateNode) } } }) }
实现节点删除操作
优化 reconcileChildren 方法
const reconcileChildren = (fiber, children) => {
// ...
while (index < numberOfElements || alternate) {
// ...
if (!element && alternate) {
// 删除操作
alternate.effectTag = 'delete'
fiber.effects.push(alternate)
}
}
}
完成删除 DOM 操作
const commitAllWork = fiber => {
fiber.effects.forEach(item => {
if (item.effectTag === 'delete') {
item.parent.stateNode.removeChild(item.stateNode)
}
}
}