渲染类组件
示例
// src/index.js
import React, { render, Component } from './react'
const root = document.getElementById('root')
// const jsx = (
// <div>
// <p>Hello React</p>
// <p>Hi Fiber</p>
// </div>
// )
// render(jsx, root)
class Greating extends Component {
constructor(props) {
super(props)
}
render() {
return <div>Hello Class Component</div>
}
}
render(<Greating />, root)
添加继承类 Component
// src\react\Component\index.js
export class Component {
constructor(props) {
this.props = props
}
}
// src/react/index.js
import createElement from './CreateElement'
export { render } from './Reconciliation'
export { Component } from './Component'
export default {
createElement
}
扩展 getTag
扩展getTag()
,支持组件类型:
// src\react\Misc\GetTag\index.js
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'
}
}
export default getTag
扩展 createStateNode
扩展createStateNode()
,支持组件:
- 类组件:返回组件实例对象
- 函数组件:返回定义组件的方法
// src\react\Misc\CreateStateNode\index.js
import { createDOMElement } from '../../DOM'
import { createReactInstance } from '../CreateReactInstance'
const createStateNode = fiber => {
if (fiber.tag === 'host_component') {
// 普通节点返回真实 DOM 对象
return createDOMElement(fiber)
} else {
// 组件节点返回组件实例对象
return createReactInstance(fiber)
}
}
export default createStateNode
// src\react\Misc\CreateReactInstance\index.js
/**
* 获取组件的 StateNode
* @param {*} fiber
*/
export const createReactInstance = fiber => {
let instance = null
if (fiber.tag === 'class_component') {
// 类组件
instance = new fiber.type(fiber.props)
} else {
// 函数组件
instance = fiber.type
}
return instance
}
获取组件的 children
React 在运行之前 Babel 会将 JSX 转换成 React.createElement()
的调用。
如果 JSX 是普通元素,则会将子元素传递给 React.createElement()
。
const jsx = (
<div>
<p>Hello React</p>
<p>Hi Fiber</p>
</div>
)
转换为:
const jsx = /*#__PURE__*/ React.createElement(
"div",
null,
/*#__PURE__*/ React.createElement("p", null, "Hello React"),
/*#__PURE__*/ React.createElement("p", null, "Hi Fiber")
);
但如果是组件:
class ClassComponent extends React.Component {
render() {
return <div>Hi Class</div>
}
}
const jsx_class = <ClassComponent />
function FunctionComponent() {
return <div>Hi Fcuntion</div>
}
const jsx_function = <FunctionComponent />
则直接将组件传递进去,组件的子级(即组件返回的 JSX 内容)需要通过调用组件的方法获取:
- 类组件调用
render()
方法 - 函数组件调用自身
class ClassComponent extends React.Component {
render() {
return /*#__PURE__*/ React.createElement("div", null, "Hi Class");
}
}
const jsx_class = /*#__PURE__*/ React.createElement(ClassComponent, null);
function FunctionComponent() {
return /*#__PURE__*/ React.createElement("div", null, "Hi Fcuntion");
}
const jsx_function = /*#__PURE__*/ React.createElement(FunctionComponent, null);
在 executeTask()
中向构建子级 Fiber 对象的方法 reconcileChildren
传递参数的时候,之前仅处理了普通节点。
const reconcileChildren = (fiber, children) => {
console.log(children);
/*...*/
}
const executeTask = fiber => {
// 构建子级 fiber 对象
reconcileChildren(fiber, fiber.props.children)
/*...*/
}
所以当前传递的组件的 fiber.props.children
为空 []
:
需要判断,当为组件节点的时候调用方法获取 children
:
const executeTask = fiber => {
// 构建子级 fiber 对象
if (fiber.tag === 'class_component') {
reconcileChildren(fiber, fiber.stateNode.render())
} else {
reconcileChildren(fiber, fiber.props.children)
}
/*...*/
}
组件节点本身也是一个节点。
构建组件的 Fiber 节点,组件的子级是组件返回的 JSX 内容,而不是 JSX 内容的子级。
第二阶段-追加节点
现在类组件的 Fiber 对象渲染完成,进入第二阶段。
我们对组件和组件返回的 JSX 的节点都构建了 Fiber 对象,组件节点本身也是一个节点,但是组件节点本身不能作为真实的 DOM 元素去操作:
- 被追加到页面中
- 追加真实 DOM 元素
所以要递归查找组件节点的普通节点父级(组件可能被包含在另一个组件中,所以要向上递归查找),这样才能去操作 DOM 元素的追加。
并且在追加时判断,仅当节点是普通节点类型时,执行追加操作。
const commitAllWork = fiber => {
fiber.effects.forEach(item => {
if (item.effectTag === 'placement') {
// 当前要追加的子节点的父级
let parentFiber = item.parent
/**
* 找到普通节点父级 排除组件父级
* 因为组件父级是不能直接追加真实 DOM 节点的
*/
while (parentFiber.tag === 'class_component') {
parentFiber = parentFiber.parent
}
// 如果子节点时普通节点 将子节点追加到父级中
if (item.tag === 'host_component') {
parentFiber.stateNode.appendChild(item.stateNode)
}
}
})
}
现在类组件就渲染完成,可以访问页面查看结果。
总结
- 设置类组件 Fiber 对象的
tag
属性为class_component
- 设置类组件 Fiber 对象的
stateNode
属性为组件实例对象 - 通过调用类组件实例对象的
render()
方法获取组件的子级:组件返回的 JSX - 追加组件内容:
- 类组件节点不能作为真实 DOM 节点去追加内容和被追加
- 需要向上循环递归查找它所属的普通节点类型的父级节点
- 在追加节点时判断,只有普通节点可以被追加到页面
渲染函数组件
示例:
// src/index.js
import React, { render, Component } from './react'
const root = document.getElementById('root')
function FnComponent(props) {
return <div>{props.title}</div>
}
render(<FnComponent title="Function Component" />, root)
函数组件和类组件几乎一样,区别:
tag
的不同- 类组件:
class_component
- 函数组件:
function_component
- 类组件:
stateNode
不同- 类组件:组件实例
- 函数组件:组件本身(函数方法)
- 获取子级的方式不同:
- 类组件通过调用实例对象的
render()
方法获取 - 函数组件通过调用组件本身的方法获取
- 类组件通过调用实例对象的
之前获取
tag
和stateNode
的修改已经支持了函数组件。
获取函数组件的子级:
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)
}
/*...*/
}
查找普通节点父级增加函数组件的判断:
const commitAllWork = fiber => {
fiber.effects.forEach(item => {
if (item.effectTag === 'placement') {
// 当前要追加的子节点的父级
let parentFiber = item.parent
/**
* 找到普通节点父级 排除组件父级
* 因为组件父级是不能直接追加真实 DOM 节点的
*/
while (parentFiber.tag === 'class_component' || parentFiber.tag === 'function_component') {
parentFiber = parentFiber.parent
}
// 如果子节点时普通节点 将子节点追加到父级中
if (item.tag === 'host_component') {
parentFiber.stateNode.appendChild(item.stateNode)
}
}
})
}
函数组件渲染完成。
实现节点更新
当前仅处理普通节点的更新
实现思路
- 当 DOM 初始化渲染完成之后,要备份旧的 Fiber 节点对象。
- 当再次调用
render()
方法更新 DOM 的时候,又再次创建 FIber 节点对象 - 当再次创建 Fiber 节点对象的时候要检查是否存在旧的 Fiber 节点对象
- 如果存在,则表示当前执行的是更新操作
- 此时就要创建执行更新操作的 Fiber 节点对象
- 否则就是初始化渲染
- 如果存在,则表示当前执行的是更新操作
示例
// src/index.js
import React, { render, Component } from './react'
const root = document.getElementById('root')
const jsx = (
<div>
<p>Hello React</p>
<p>Hi Fiber</p>
</div>
)
render(jsx, root)
setTimeout(() => {
const jsx = (
<div>
<p>你好 React</p>
<p>Hi Fiber</p>
</div>
)
render(jsx, root)
}, 2000)
备份旧的 Fiber 节点对象
初始化渲染完成就是指 DOM 操作完成之后,也就是 commitAllWork()
中的内容执行完成之后。
在该方法中备份旧的 Fiber 节点对象,只需将根节点对应的 Fiber 对象存储到根节点对应的真实 DOM 对象上即可。
// src/react/Reconciliation/index.js
import { createTaskQueue, arrified, createStateNode, getTag } from '../Misc'
/*...*/
// 存储根节点所对应的 Fiber 对象
let pendingCommit = null
const commitAllWork = fiber => {
// 循环 effects 数组 构建 DOM 节点树
fiber.effects.forEach(item => {
/*...*/
})
// 备份旧的 Fiber 节点对象
fiber.stateNode.__rootFiberContainer = fiber
}
/*...*/
根节点的 Fiber 对象中存储备份
Fiber 对象中的 alternate
属性存储旧 Fiber 对象的备份,用于新旧对比。
首先修改构建根节点 Fiber 对象的方法getFirstTask()
:
const getFirstTask = () => {
// 从任务队列中获取任务
const task = taskQueue.pop()
// 返回最外层节点的 Fiber 对象
return {
props: task.props,
stateNode: task.dom,
tag: 'host_root',
effects: [], // 暂不指定
child: null, // 在构建子节点的时候指定其与父节点的关系
alternate: task.dom.__rootFiberContainer // 旧的 Fiber 节点对象
}
}
子节点的 Fiber 对象中存储备份
然后找到构建子节点 Fiber 对象的方法reconcileChildren()
。
- 该方法中会循环构建节点的子节点
- 在循环之前判断节点是否备份了旧 Fiber 对象
- 如果有,则获取备份的Fiber对象中的子级(
child
:存储的是该节点下第一个子节点) - 然后进入构建子节点的循环中
- 构建Fiber对象的时候,判断是否有备份,如果有则将备份存储到
alternate
属性 - 然后判断该备份的 Fiber 对象中是否有兄弟节点(
sibling
) - 如果有,则获取这个兄弟节点,它就是下次循环构建的子节点
const reconcileChildren = (fiber, children) => {
// children 可能是对象,也可能是数组
// 将 children 转换成数组
const arrifiedChildren = arrified(children)
// 循环 children 使用的索引
let index = 0
// children 数组中元素的个数
let numberOfElements = arrifiedChildren.length
// 循环过程中的循环项 就是子节点的 virtualDOM 对象
let element = null
// 子级 fiber 对象
let newFiber = null
// 上一个兄弟 fiber 对象
let prevFiber = null
// 循环过程中节点对应的备份 fiber 对象
let alternate = null
if (fiber.alternate && fiber.alternate.child) {
alternate = fiber.alternate.child
}
while (index < numberOfElements) {
// 子级 virtualDOM 对象
element = arrifiedChildren[index]
// 子级 fiber 对象
newFiber = {
type: element.type,
props: element.props,
tag: getTag(element),
effects: [], // 暂不指定
effectTag: 'placement',
parent: fiber,
alternate
}
// 为 fiber 对象添加 DOM 对象或类组件实例对象或函数组件本身
newFiber.stateNode = createStateNode(newFiber)
// 指明父子关系、兄弟关系
if (index === 0) {
// 父节点的子节点只能是第一个子节点
fiber.child = newFiber
} else {
// 其它的节点作为上一个节点的兄弟节点
prevFiber.sibling = newFiber
}
if (alternate && alternate.sibling) {
// 获取下一个节点的备份
alternate = alternate.sibling
} else {
alternate = null
}
prevFiber = newFiber
index++
}
}
根据操作构建不同的 Fiber 对象
在构建子节点的时候,还要判断当前要执行什么操作,从而构建不同操作所对应的 Fiber 对象:
- 初始渲染
- 更新操作
const reconcileChildren = (fiber, children) => {
// children 可能是对象,也可能是数组
// 将 children 转换成数组
const arrifiedChildren = arrified(children)
// 循环 children 使用的索引
let index = 0
// children 数组中元素的个数
let numberOfElements = arrifiedChildren.length
// 循环过程中的循环项 就是子节点的 virtualDOM 对象
let element = null
// 子级 fiber 对象
let newFiber = null
// 上一个兄弟 fiber 对象
let prevFiber = null
// 循环过程中节点对应的备份 fiber 对象
let alternate = null
if (fiber.alternate && fiber.alternate.child) {
alternate = fiber.alternate.child
}
while (index < numberOfElements) {
// 子级 virtualDOM 对象
element = arrifiedChildren[index]
if (element && alternate) {
/* 更新操作 */
// 子级 fiber 对象
newFiber = {
type: element.type,
props: element.props,
tag: getTag(element),
effects: [], // 暂不指定
effectTag: 'update',
parent: fiber,
alternate
}
// 判断节点类型
if (element.type === alternate.type) {
/* 类型相同 */
// 只需将之前的 stateNode 赋值给新的 fiber 对象即可
newFiber.stateNode = alternate.stateNode
} else {
/* 类型不同 */
// 为 fiber 对象添加 DOM 对象或类组件实例对象或函数组件本身
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)
}
// 指明父子关系、兄弟关系
if (index === 0) {
// 父节点的子节点只能是第一个子节点
fiber.child = newFiber
} else {
// 其它的节点作为上一个节点的兄弟节点
prevFiber.sibling = newFiber
}
if (alternate && alternate.sibling) {
// 获取下一个节点的备份
alternate = alternate.sibling
} else {
alternate = null
}
prevFiber = newFiber
index++
}
}
执行 DOM 操作
执行 DOM 操作是在 commitAllWork()
方法中。
- 通过 Fiber 对象的
effectTag
属性判断执行的操作update
更新节点placement
追加节点
- 如果是更新节点,继续判断节点类型是否相同
- 节点类型不同,直接用新节点替换旧节点(调用父节点 DOM 的
replaceChild()
) - 节点类型相同,执行更新操作(调用
updateNodeElement()
)updateNodeElement()
方法接收的 VirtualDOM 就是 Fiber 对象,主要使用对象的props
属性
- 节点类型不同,直接用新节点替换旧节点(调用父节点 DOM 的
// src/react/Reconciliation/index.js
import { updateNodeElement } from '../DOM'
import { createTaskQueue, arrified, createStateNode, getTag } from '../Misc'
/*...*/
const commitAllWork = fiber => {
// 循环 effects 数组 构建 DOM 节点树
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)
}
} else if (item.effectTag === 'placement') {
/* 追加节点 */
/*...*/
}
})
// 备份旧的 Fiber 节点对象
fiber.stateNode.__rootFiberContainer = fiber
}
扩展更新节点的方法 - 更新文本节点
当前更新节点的方法 updateNodeElement
是参考之前的《模拟 React》 文章复制来的。
该文中更新文本节点是调用的另一个方法,所以 updateNodeElement()
中没有处理文本节点。
现在扩展这个方法,使其即能处理元素节点,也能处理文本节点:
// src\react\DOM\updateNodeElement.js
/**
* @param {*} newElement 要更新的 DOM 元素对象
* @param {*} virtualDOM 新的 Virtual DOM 对象
* @param {*} oldVirtualDOM 旧的 Virtual DOM 对象
*/
export default function updateNodeElement(newElement, virtualDOM = {}, oldVirtualDOM = {}) {
// 获取节点对应的属性对象
const newProps = virtualDOM.props
const oldProps = oldVirtualDOM.props || {}
// 文本节点更新操作
if (virtualDOM.type === 'text') {
if (newProps.textContent !== oldProps.textContent) {
virtualDOM.parent.stateNode.textContent = newProps.textContent
// 也可以使用替换节点的方式,但要判断父节点类型发生变化的情况
// if (virtualDOM.parent.type != oldVirtualDOM.parent.type) {
// virtualDOM.parent.stateNode.appendChild(document.createTextNode(newProps.textContent))
// } else {
// virtualDOM.parent.stateNode.replaceChild(
// document.createTextNode(newProps.textContent),
// oldVirtualDOM.stateNode
// )
// }
}
return
}
// 属性被修改或添加属性的情况
Object.keys(newProps).forEach(propName => {/*...*/})
// 判断属性被删除的情况
Object.keys(oldProps).forEach(propName => {/*...*/})
}
总结
- 在构建 Fiber 对象的时候要备份旧的 Fiber 对象
- 在初始渲染结束后(
commitAllWork()
)将根节点的 Fiber 对象存储在真实 DOM 上(__rootFiberContainer
) - 在构建根节点 Fiber 时(
getFirstTask()
)将旧的根节点 Fiber 对象备份到alternate
属性 - 在构建子节点时(
reconcileChildren()
)备份旧的子节点 Fiber,还要根据操作构建不同操作类型的 Fiber 节点对象- 首先判断父级是否有
alternate
- 如果有则获取
alternate
的子级(child
),它是循环的第一个子节点的备份 - 循环子级节点,判断节点是否有对应的备份
- 如果有则为更新节点操作
- 将备份存储到
alternate
- 判断节点类型是否相同
- 如果不同则需要重新获取
stateNode
- 如果相同则直接取
alternate
的stateNode
- 如果不同则需要重新获取
- 将备份存储到
- 如果没有,则不需要对
alternate
赋值
- 如果有则为更新节点操作
- 接着判断
alternate
是否有兄弟节点- 如果有则将兄弟节点作为下一轮循环的子节点的备份
- 首先判断父级是否有
- 在初始渲染结束后(
- 构建完 Fiber 后操作 DOM 对象,
commitAllWork()
中循环根节点的effects
,也就是所有的 Fiber 对象,判断它的操作类型(effectTag
):update
更新节点操作- 判断节点类型
- 相同节点,执行更新节点操作
- 文本节点更新文本内容
- 其它节点更新它们的属性
- 不同节点,直接用新节点替换就节点
- 相同节点,执行更新节点操作
- 判断节点类型
placement
追加节点操作
实现节点删除
当前仅处理普通节点的删除
示例
// src/index.js
import React, { render, Component } from './react'
const root = document.getElementById('root')
const jsx = (
<div>
<p>Hello React</p>
<p>Hi Fiber</p>
</div>
)
render(jsx, root)
setTimeout(() => {
const jsx = (
<div>
{/* <h1>你好 React</h1> */}
<p>Hi Fiber</p>
</div>
)
render(jsx, root)
}, 2000)
构建删除操作的 Fiber 节点对象
在 reconcileChildren()
中通过判断循环的判断当前如果是删除操作,就构建删除操作的 Fiber 节点对象。
- 根据当前循环的子节点对应的
alternate
是否存在, 判断节点是否被删除 - 当子节点被清空的时候,子节点的数量为0,无法进入循环,所以要为
while
循环增加一个判断条件,判断是否有子级的备份 - 并在进入循环后,判断当前子节点是否存在,以判断节点是否被删除
- 当为删除节点操作时,将当前节点的备份 Fiber 中的
effectTag
设置为delete
添加到effects
中,在最终执行 DOM 操作的时候会处理 - 在为上一个子节点设置兄弟节点的时候要判断当前节点是否存在,如果不存在则不设置兄弟节点
const reconcileChildren = (fiber, children) => {
/*...*/
while (index < numberOfElements || alternate) {
// 子级 virtualDOM 对象
element = arrifiedChildren[index]
if (!element && alternate) {
/* 删除操作 */
alternate.effectTag = 'delete'
fiber.effects.push(alternate)
} else if (element && alternate) {
/* 更新操作 */
/*...*/
} else if (element && !alternate) {
/* 初始渲染操作 */
/*...*/
}
// 指明父子关系、兄弟关系
if (index === 0) {
// 父节点的子节点只能是第一个子节点
fiber.child = newFiber
} else if (element) {
// 其它的节点作为上一个节点的兄弟节点
prevFiber.sibling = newFiber
}
/*...*/
}
}
执行 DOM 删除操作
在 commitAllWork()
中判断,如果是删除操作,直接调用父节点的 removeChild()
删除当前节点即可:
const commitAllWork = fiber => {
// 循环 effects 数组 构建 DOM 节点树
fiber.effects.forEach(item => {
if (item.effectTag === 'delete') {
/* 删除节点 */
item.parent.stateNode.removeChild(item.stateNode)
} else if (item.effectTag === 'update') {
/* 更新节点 */
/*...*/
} else if (item.effectTag === 'placement') {
/* 追加节点 */
/*...*/
}
})
/*...*/
}