一. 基本概念
1.1 jsx
语法
jsx
是react
提供的语法糖,经过babel
转换处理之后会变成React.createElement
方法调用,例如<App />
会转成React.createElement(function App() {}, null)
,所以转换后的代码本质是调用了React.createElement
方法执行相关的代码逻辑。
1.2 Fiber
架构
在React
中每个组件或元素标签都对应一个FiberNode
节点,节点间会建立父子,兄弟关联关系,构建成FiberNode Tree
,即虚拟DOM
树。
当触发更新逻辑时,会构建一棵新的虚拟DOM
树,与旧的虚拟DOM
树进行比对,找出新增/删除/变更FiberNode
节点,在更新DOM
树阶段,会递归遍历虚拟DOM
树,找到需要处理的FiberNode
节点,例如需要删除该FiberNode
节点,那么会将该FiberNode
节点对应的DOM
节点从DOM
树中移除,完成DOM
树的更新。
通过React
的Fiber
架构,我们可以精确找到真实DOM
树需要更新的节点,进行相应逻辑处理完成更新操作
二. 核心对象
2.1 ReactElement
当调用React.createElement
方法时会创建ReactElement
对象,这个对象会记录组件方法或元素标签类型以及传入的props
2.2 FiberRootNode
这个对象会记录根DOM
节点和最新构建的FiberNode Tree
根节点
2.3 FiberNode
每个ReactElement
对象都会创建对应的FiberNode
节点,节点之间建立父子,兄弟关联关系,由此构建成虚拟DOM
树,同时节点会有stateNode
属性记录对应的真实DOM
节点
三. 实现mini React
3.1 测试代码
注意测试项目要支持解析
jsx
语法且添加了@babel/preset-react
这个预设
目标是跑通这段测试代码,逻辑很简单,渲染App
组件的内容,当点击hello react
这个h1
标签时要切换展示HelloWorld
组件
import React, { createRoot, useState } from './mini-react'
function HelloWorld() {
return <h1>are you ok?</h1>
}
function App() {
const [visible, setVisible] = useState(true)
return (
<div>
<h1 onClick={() => setVisible(!visible)}>hello react</h1>
{visible && <HelloWorld />}
</div>
)
}
const root = createRoot(document.getElementById('app'))
root.render(<App />)
3.2 目录结构
mini-react
|- hooks
|-- useState.js
|- ReactElement.js
|- FiberNode.js
|- FiberRootNode.js
|- ReactDOMRoot.js
|- ReactFiberReconciler.js
|- index.js
3.3 将组件方法或元素标签转成ReactElement
对象
3.3.1 定义ReactElement
对象原型
/**
* @param {*} type 组件方法或元素标签类型,例如function App() {}或h1
* @param {*} key 组件方法或元素标签的标识符,例如<App key='app' />的key属性值
* @param {*} props 入参属性,例如<h1 onClick={() => {}} />的onClick属性
*/
function ReactElement(type, key, props) {
this.type = type
this.key = key
this.props = props
}
3.3.2 提供createElement
方法创建ReactElement
对象
/**
* @param {*} type 组件方法或元素标签类型,例如function App() {}或h1
* @param {*} config ReactElement的key和props
* @param {*} childrens child ReactElement对象数组
*/
function createElement(type, config, ...childrens) {
const { key = null, ...props } = config || {}
if (childrens.length) {
props.children = childrens.length > 1 ? childrens : childrens[0]
}
return new ReactElement(type, key, props)
}
export { createElement }
3.4 创建FiberRootNode
对象,记录FiberNode Tree
根节点和根DOM
节点
3.4.1 定义FiberNode
对象原型
这里解析下flags
和substreeFlags
两个属性,flags
是当前节点状态,例如插入,删除,更新等状态,而substreeFlags
是child FiberNode
节点状态,在更新DOM
阶段,会遍历FiberNode Tree
节点,通过substreeFlags
属性值可以判断child FiberNode
节点是否需要更新,如果值为NoFlags
,说明无需处理,那么就可以不用继续递归遍历其child FiberNode
节点
// 根FiberNode节点对应类型
export const HostRoot = 3
// 组件方法FiberNode节点对应类型
export const FunctionComponent = 0
// 元素标签FiberNode节点对应类型
export const HostComponent = 5
// 纯文本FiberNode节点对应类型
export const HostText = 6
// 表示该FiberNode节点无需处理
export const NoFlags = 0
// 表示该FiberNode节点是新增节点,需要插入DOM树中
export const Placement = 2
// 表示该FiberNode节点需要更新处理
export const Update = 4
// 表示该FiberNode节点有子节点要删除
export const ChildDeletion = 16
function FiberNode(tag, pendingProps) {
this.tag = tag // FiberNode节点类型
this.key = null // 对应ReactElement的key属性
this.elementType = null // 对应ReactElement的type属性
this.return = null // 父FiberNode节点
this.child = null // 子FiberNode节点
this.sibling = null // 兄弟FiberNode节点
this.alternate = null // FiberNode副本节点
this.deletions = null // 删除的FiberNode节点
this.flags = NoFlags // FiberNode节点状态
this.subtreeFlags = NoFlags // 子树FiberNode节点状态
this.stateNode = null // FiberNode节点对应的DOM节点
this.pendingProps = pendingProps // 对应ReactElement的props属性
}
3.4.2 定义FiberRootNode
对象原型
/**
* @param {*} element DOM节点
*/
function FiberRootNode(element) {
// 记录根DOM节点
this.containerInfo = element
// 创建根FiberNode节点
const fiber = new FiberNode(HostRoot, null)
fiber.stateNode = this
// 记录根FiberNode节点
this.current = fiber
}
3.4.3 提供createRoot
方法创建FiberRootNode
对象
/**
* @param {*} root FiberRootNode对象
*/
function ReactDOMRoot(root) {
this._internalRoot = root
}
/**
* @param {*} children ReactElement对象
*/
ReactDOMRoot.prototype.render = function (children) {
// 获取FiberRootNode对象
const root = this._internalRoot
performWorkOnRoot(root, children)
}
/**
* @param {*} element DOM节点
*/
function createRoot(element) {
// 添加全局click监听事件
element.addEventListener('click', (event) => {
if (event.target.onClick) {
event.target.onClick()
}
})
const root = new FiberRootNode(element)
return new ReactDOMRoot(root)
}
export { createRoot }
3.5 首次构建FiberNode Tree
首次构建FiberNode Tree
结构如图所示,先对FiberNode
节点之间关联关系,FiberNode
节点和DOM
节点关联关系有一个初步的了解,下面会逐步讲解从零到一构建FiberNode Tree
流程
3.5.1 提供createWorkInProgress
方法创建FiberNode
副本节点
如果已存在副本节点,则复制旧FiberNode
节点属性值
/**
* @param {*} current FiberNode节点
* @param {*} pendingProps 对应ReactElement对象的props
*/
function createWorkInProgress(current, pendingProps) {
let workInProgress = current.alternate
if (workInProgress !== null) {
workInProgress.flags = NoFlags
workInProgress.subtreeFlags = NoFlags
workInProgress.deletions = null
workInProgress.pendingProps = pendingProps
} else {
workInProgress = new FiberNode(current.tag, pendingProps)
workInProgress.alternate = current
current.alternate = workInProgress
}
workInProgress.key = current.key
workInProgress.elementType = current.elementType
workInProgress.child = current.child
workInProgress.sibling = current.sibling
workInProgress.stateNode = current.stateNode
workInProgress.memoizedState = current.memoizedState
return workInProgress
}
3.5.2 递归遍历FiberNode
节点,创建ReactElement
对象对应的FiberNode
节点
- 调用
createWorkInProgress
方法创建根FiberNode
副本节点,赋值给workInProgress
- 调用
performUnitOfWork
方法递归遍历FiberNode
节点,创建ReactElement
对应的FiberNode
节点,建立关联关系,构建FiberNode Tree
- 当递归遍历到当前分支叶子节点时会调用
completeUnitOfWork
方法进行DOM
树构建和收集子树FiberNode
节点副作用
function performUnitOfWork(unitOfWork) {
let nextFiber = beginWork(unitOfWork)
// next为null,说明已经遍历到当前分支叶子FiberNode节点,则调用completeWork方法构建DOM树和收集子树FiberNode节点副作用
if (nextFiber === null) {
completeUnitOfWork(unitOfWork)
} else {
workInProgress = nextFiber
}
}
/**
* @param {*} root FiberRootNode对象
* @param {*} children ReactElement对象
*/
function renderRootSync(root, children) {
const current = root.current
// 创建根FiberNode副本节点,赋值给workInProgress
workInProgress = createWorkInProgress(current, null)
workInProgress.pendingProps = { children }
// 递归遍历FiberNode节点,创建ReactElement对应的FiberNode节点,建立关联关系,构建FiberNode Tree
while (workInProgress !== null) {
performUnitOfWork(workInProgress)
}
}
需要注意这里采用深度优先遍历算法,即递归遍历到第一个分支叶子节点后返回遍历下一个分支,例如下面这个示例,A
和B
组件平级,A
嵌套C
组件,那会先递归遍历A -> C
这个分支,然后返回遍历B
,可以先判断下控制台输出结果会是什么
function C() {
console.log('C')
return <h1>hello C</h1>
}
function A() {
console.log('A')
return (
<div>
<h1>hello A</h1>
<C />
</div>
)
}
function B() {
console.log('B')
return <h1>hello B</h1>
}
function App() {
return (
<div>
<A />
<B />
</div>
)
}
// 控制台输出结果是A -> C -> B
3.5.3 创建ReactElement
对象对应的FiberNode
节点,建立关联关系
FiberNode
节点tag
属性代表节点类型,不同节点类型有对应处理逻辑
/**
* @param {*} workInProgress FiberNode节点
*/
function beginWork(workInProgress) {
// 获取旧FiberNode节点
const current = workInProgress.alternate
switch (workInProgress.tag) {
case HostRoot: {
const { children: nextChildren } = workInProgress.pendingProps
// 创建child ReactElement对象对应的FiberNode节点
return reconcileChildren(current, workInProgress, nextChildren)
}
case FunctionComponent: {
// 获取组件方法
const Component = workInProgress.elementType
// 调用组件方法获取child ReactElement对象
const nextChildren = Component(workInProgress.pendingProps)
// 创建child ReactElement对应的FiberNode节点
return reconcileChildren(current, workInProgress, nextChildren)
}
case HostComponent: {
let { children: nextChildren } = workInProgress.pendingProps
// 判断nextChildren是否是纯文本,是则不需要创建FiberNode节点,将nextChildren赋值为null
if (typeof nextChildren === 'string') {
nextChildren = null
}
// 创建child ReactElement对象对应的FiberNode节点
return reconcileChildren(current, workInProgress, nextChildren)
}
case HostText: {
return null
}
}
}
3.5.4 解析reconcileChildren
方法
当current
为null
,调用mountChildFibers
方法,即不需要处理child FiberNode
节点副作用,变更flags
属性值(flags
属性作用参考3.4.1
小节),因为current
为null
说明旧FiberNode Tree
中没有当前节点对应的旧FiberNode
节点,属于新增节点,我们只需要将当前节点的flags
属性值赋值为Placement
即可,不需要处理child FiberNode
节点flags
属性值。
当current
不为null
,调用reconcileChildFibers
方法,即需要处理child FiberNode
节点副作用。
mountChildFibers
和reconcileChildFibers
都是调用了createChildReconciler
方法,获取返回的闭包函数。
createChildReconciler
方法接收一个boolean
类型入参,用于判断是否处理child FiberNode
节点副作用,采用闭包方式,返回创建child FiberNode
节点方法。同时createChildReconciler
方法内部封装了许多处理child FiberNode
节点的逻辑方法,下面会依次说明
function createChildReconciler(shouldTrackSideEffects) {
function reconcileChildFibers(workInProgress, currentFirstChild, nextChildren) {}
return reconcileChildFibers
}
const mountChildFibers = createChildReconciler(false)
const reconcileChildFibers = createChildReconciler(true)
/**
* @param {*} current 旧FiberNode节点
* @param {*} workInProgress FiberNode节点
* @param {*} nextChildren child ReactElement对象
*/
function reconcileChildren(current, workInProgress, nextChildren) {
if (current === null) {
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren)
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
)
}
return workInProgress.child
}
3.5.5 解析首次渲染reconcileChildFibers
方法逻辑
在首次渲染,需要从零到一构建FiberNode Tree
,只有根FiberNode
节点有对应的旧FiberNode
节点,意味着创建App Compoent
对应的FiberNode
节点时shouldTrackSideEffects
会为true
,同时因为是首次构建FibreNode Tree
,所以App Component
对应的FiberNode
节点不存在对应的旧FiberNode
节点,即该节点是新增节点,将flags
属性值赋值为Placement
在后续递归创建child FiberNode
节点时,shouldTrackSideEffects
会为false
,不需要变更flags
属性值
function createChildReconciler(shouldTrackSideEffects) {
function placeChild(newFiber) {
// 当需要处理副作用时,且旧FiberNode Tree中没有新节点对应的旧节点时,说明是新增节点,将其flags属性值赋值为Placement
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement
}
return newFiber
}
// 创建ReactElement对应的FiberNode节点
function createFiberFormElement(element) {
let fiber
if (typeof element.type === 'function') {
fiber = new FiberNode(FunctionComponent, element.props)
} else {
fiber = new FiberNode(HostComponent, element.props)
}
fiber.key = element.key
fiber.elementType = element.type
return fiber
}
// 创建文本对应的FiberNode节点
function creaetFiberFromText(text) {
const fiber = new FiberNode(HostText, text)
return fiber
}
function reconcileSingleTextNode(returnFiber, currentFirstChild, text) {
const fiber = creaetFiberFromText(text)
fiber.return = returnFiber
return placeChild(fiber)
}
// 创建单个ReactElement对象对应的FiberNode节点
function reconcileSingleElement(returnFiber, currentFirstChild, newChild) {
const fiber = createFiberFormElement(newChild)
fiber.return = returnFiber
return placeChild(fiber)
}
// 创建多个ReactElement对象对应的FiberNode节点
function reconcileChildrenArray(returnFiber, oldFiber, newChild) {
let firstChildFiber = null
let prevChildFiber = null
for (let newIdx = 0; newIdx < newChild.length; newIdx++) {
let nextFiber = null
if (typeof newChild[newIdx] === 'object' && newChild[newIdx] !== null) {
nextFiber = createFiberFormElement(newChild[newIdx])
} else if (typeof newChild[newIdx] === 'string') {
nextFiber = creaetFiberFromText(newChild[newIdx])
}
if (nextFiber) {
nextFiber.return = returnFiber
// 记录第一个child FiberNode节点
if (firstChildFiber === null) {
firstChildFiber = nextFiber
}
// 建立兄弟FiberNode节点关联关系
if (prevChildFiber === null) {
prevChildFiber = nextFiber
} else {
prevChildFiber.sibling = nextFiber
prevChildFiber = nextFiber
}
}
}
return firstChildFiber
}
/**
* @param {*} returnFiber 父FiberNode节点
* @param {*} currentFirstChild 旧FiberNode节点的子节点
* @param {*} newChild ReactElement对象
*/
function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
if (typeof newChild === 'object' && newChild !== null) {
if (Array.isArray(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild)
}
const firstChildFiber = reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
)
return firstChildFiber
}
if (typeof newChild === 'string') {
const firstChildFiber = reconcileSingleTextNode(
returnFiber,
currentFirstChild,
newChild,
)
return firstChildFiber
}
return null
}
return reconcileChildFibers
}
3.6 构建DOM
树
3.6.1 递归遍历FiberNode
节点,构建FiberNode
节点对应的DOM
树和收集子树FiberNode
节点副作用
当构建FiberNode Tree
,递归遍历到分支叶子节点时会调用completeUnitOfWork
创建构建该节点对应DOM
节点,递归父节点构建DOM
树,采用自底向上的递归方式
/**
* @param {*} unitOfWork FiberNode节点
*/
function completeUnitOfWork(unitOfWork) {
while (unitOfWork) {
let nextFiber = unitOfWork
// 构建FiberNode节点对应的DOM树和收集子树FiberNode节点副作用
completeWork(nextFiber)
// 获取兄弟节点
nextFiber = nextFiber.sibling
// 如果有兄弟节点,则调用performUnitOfWork方法继续构建FiberNode Tree
if (nextFiber !== null) {
workInProgress = nextFiber
break
}
// 递归父FiberNode节点
unitOfWork = unitOfWork.return
workInProgress = unitOfWork
}
}
3.6.2 收集子树FiberNode
节点副作用
遍历child FiberNode
,收集副作用
function bubbleProperties(workInProgress) {
let subtreeFlags = NoFlags
let child = workInProgress.child
while (child) {
subtreeFlags |= child.flags
subtreeFlags |= child.subtreeFlags
child = child.sibling
}
workInProgress.subtreeFlags |= subtreeFlags
}
3.6.3 构建FiberNode
节点对应的DOM
树
// 设置DOM属性值
function setProp(el, key, value) {
switch (key) {
case 'children': {
if (typeof value === 'string') {
el.textContent = value
}
break
}
case 'onClick': {
el.onClick = value
break
}
}
}
function appendAllChildren(el, workInProgress) {
let nextChild = workInProgress.child
while (nextChild) {
if (nextChild.tag === HostComponent || nextChild.tag === HostText) {
el.appendChild(nextChild.stateNode)
} else {
appendAllChildren(el, nextChild)
}
nextChild = nextChild.sibling
}
}
function completeWork(workInProgress) {
switch (workInProgress.tag) {
// HostRoot和FunctionComponent类型的FiberNode节点没有对应的DOM节点,没有构建DOM树逻辑,只需要收集子树FiberNode节点副作用即可
case HostRoot:
case FunctionComponent:
bubbleProperties(workInProgress)
return null
case HostComponent: {
// 创建DOM节点
const { elementType, pendingProps } = workInProgress
const instance = document.createElement(elementType)
// 将DOM节点赋值给FiberNode的stateNode属性
workInProgress.stateNode = instance
// 设置DOM节点属性
for (const propKey in pendingProps) {
const prpoValue = pendingProps[propKey]
setProp(instance, propKey, prpoValue)
}
// 递归遍历FiberNode节点将对应的DOM节点添加到父DOM节点中,构建DOM树
appendAllChildren(instance, workInProgress)
// 收集子树FiberNode节点副作用
bubbleProperties(workInProgress)
return null
}
case HostText: {
const { pendingProps } = workInProgress
const instance = document.createTextNode(pendingProps)
workInProgress.stateNode = instance
bubbleProperties(workInProgress)
return null
}
}
}
3.7 首次更新DOM
3.7.1 递归遍历FiberNode
节点执行副作用处理逻辑
采用深度优先遍历递归算法,优先处理叶子节点副作用逻辑
在首次渲染更新DOM
阶段,只需要将App Component
组件的child FiberNode
节点对应的DOM
插入指定节点即可完成DOM
更新
// 获取父DOM节点
function getParentNode(finishWork) {
let parentFiber = finishWork.return
while (parentFiber !== null) {
if (parentFiber.tag === FunctionComponent) {
parentFiber = parentFiber.return
continue
}
break
}
return parentFiber.tag === HostRoot
? parentFiber.stateNode.containerInfo
: parentFiber.stateNode
}
function recursivelyTraverseMutationEffects(finishWork) {
// 为true说明子树FiberNode节点有副作用需要处理,递归遍历child FiberNode
if (finishWork.subtreeFlags & (Placement | Update | ChildDeletion)) {
let child = finishWork.child
while (child) {
commitMutationEffectsOnFiber(child)
child = child.sibling
}
}
}
function commitMutationEffectsOnFiber(finishWork) {
// 采用深度优先遍历算法进行DOM更新
switch (finishWork.tag) {
case HostRoot: {
recursivelyTraverseMutationEffects(finishWork)
break
}
case FunctionComponent: {
recursivelyTraverseMutationEffects(finishWork)
// 为true则需要将对应DOM节点插入到父节点DOM中
if (finishWork.flags & Placement) {
// 获取父节点DOM
const parentNode = getParentNode(finishWork)
// 获取子节点对应的DOM
appendAllChildren(parentNode, finishWork)
}
break
}
case HostComponent: {
break
}
case HostText: {
break
}
}
}
function commitRoot(root) {
const finishWork = root.current.alternate
// 递归遍历FiberNode节点,执行副作用处理逻辑
commitMutationEffectsOnFiber(finishWork)
// 将FiberRootNode对象current属性指向新的FiiberNode Tree根节点
root.current = finishWork
}
3.8 实现简单版本useState
3.8.1 修改FiberNode
对象原型定义
新增memoizedState
属性用于记录useState
数据
function FiberNode(tag) {
this.memoizedState = null // 记录useState数据
}
3.8.2 useState
方法实现
每次调用useState
方法都会创建一个hook
,用来保存state
数据,以及收集更新state
的方法。每个hook
通过next
指针进行索引,构成一个单链表结构。
当首次调用组件方法时,调用mountState
方法处理逻辑,即创建hook
,记录初始值,将hook
赋值给当前节点的memoizedState
属性,返回初始值和dispatch
方法
当触发更新重新调用组件方法时,调用updateState
方法处理,即创建hook
,获取旧的state
值,执行更新state
的方法时作为入参,获取新的state
值赋值给hook
,将hook
赋值给当前节点的memoizedState
属性,返回初始值和dispatch
方法
dispatch
方法会收集更新state
的方法,触发更新重新渲染
// 记录当前FiberNode节点
let currentlyRenderingFiber = null
// 记录旧state hook
let currentHook = null
// 记录新state hook
let workInProgressHook = null
// 组件方法调用装饰器,在调用组件方法前后做一些逻辑处理
export function renderWithHooks(current, workInProgress, Component, props) {
currentlyRenderingFiber = workInProgress
if (current !== null) {
currentHook = current.memoizedState
}
workInProgressHook = null
const children = Component(props)
currentlyRenderingFiber = null
currentHook = null
return children
}
// 获取FiberRootNode对象
function getRootForUpdatedFiber(fiber) {
while (fiber.tag !== HostRoot) {
fiber = fiber.return
}
return fiber.stateNode
}
// 触发更新方法
function dispatchSetState(fiber, hook, action) {
// 获取FiberRootNode对象
const root = getRootForUpdatedFiber(fiber)
const executor = typeof action === 'function' ? action : () => action
// 收集更新state的方法,在创建新FiberNode节点时执行,将新的state赋值给节点的memoizedState属性
hook.queue.push((state) => executor(state))
// 触发更新
performWorkOnRoot(root, null)
}
// 创建state hook
function mountWorkInProgressHook() {
const hook = {
memoizedState: null, // 记录更新state值
queue: [], // 更新state方法队列
next: null, // 记录下一个hook
}
return hook
}
// 首次调用组件方法处理逻辑
function mountState(initialState) {
const hook = mountWorkInProgressHook()
hook.memoizedState = initialState
// 建立hook链表
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook
} else {
workInProgressHook.next = hook
workInProgressHook = hook
}
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, hook)
return [hook.memoizedState, dispatch]
}
// 触发更新再次调用函数组件处理逻辑
function updateState() {
const hook = mountWorkInProgressHook()
// 调用更新state方法获取新的state
hook.memoizedState = currentHook.queue.reduce(
(state, action) => action(state),
currentHook.memoizedState,
)
// 建立hook链表
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook
} else {
workInProgressHook.next = hook
workInProgressHook = hook
}
currentHook = currentHook.next
const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, hook)
return [hook.memoizedState, dispatch]
}
function useState(initialState) {
const current = currentlyRenderingFiber.alternate
if (current === null) {
return mountState(initialState)
} else {
return updateState()
}
}
export { useState }
3.8.3 组件调用添加装饰器
修改组件方法调用逻辑,作为renderWithHooks
方法入参内部调用
function beginWork(workInProgress) {
const current = workInProgress.alternate
switch (workInProgress.tag) {
case FunctionComponent: {
// 获取组件方法
const Component = workInProgress.elementType
const nextChildren = renderWithHooks(
current,
workInProgress,
Component,
workInProgress.pendingProps,
)
}
}
}
3.9 触发更新构建新FiberNode Tree
新FiberNode Tree
结构如图所示,可以看到这时候已经有两课FiberNode Tree
,FiberNodeRoot
对象的current
指向最新的FiberNode Tree
根节点,当触发更新时,会进行diff
逻辑处理,判断旧FiberNode
节点能否复用,旧FiberNode
是否要删除等
3.9.1 复用旧根FiberNode
节点child FiberNode
function beginWork(workInProgress) {
switch (workInProgress.tag) {
case HostRoot: {
if (current.child) {
// 复用旧child FiberNode节点
const newChild = createWorkInProgress(
current.child,
current.pendingProps,
)
newChild.return = workInProgress
workInProgress.child = newChild
return workInProgress.child
}
const { children: nextChildren } = workInProgress.pendingProps
// 创建child ReactElement对象对应的FiberNode节点
return reconcileChildren(current, workInProgress, nextChildren)
}
}
}
3.9.2 补充reconcileChildFibers
方法diff
逻辑
3.9.2.1 新增deleteChild
方法
用于收集要删除的旧Fiber
节点,以及变更父节点的flags
属性值为ChildDeletion
function createChildReconciler(shouldTrackSideEffects) {
// 遍历旧FiberNode节点及其兄弟节点添加到父节点的deletions属性中,将父节点的flags赋值为ChildDeletion
function deleteChild(returnFiber, oldFiber, traverse = true) {
if (oldFiber === null) {
return null
}
returnFiber.flags |= ChildDeletion
returnFiber.deletions = returnFiber.deletions || []
returnFiber.deletions.push(oldFiber)
oldFiber = oldFiber.sibling
while (oldFiber && traverse) {
returnFiber.deletions.push(oldFiber)
oldFiber = oldFiber.sibling
}
return null
}
}
3.9.2.2 补充reconcileSingleTextNode
方法diff
逻辑
- 旧节点类型不是
HostText
,那么删除该节点及其兄弟节点 - 旧节点类型是
HostText
,复用该旧节点,由于新节点没有兄弟节点,需要删除旧节点的兄弟节点
function createChildReconciler(shouldTrackSideEffects) {
function useFiber(fiber, pendingProps) {
const clone = createWorkInProgress(fiber, pendingProps)
clone.sibling = null
return clone
}
function reconcileSingleTextNode(returnFiber, oldFiber, text) {
if (oldFiber !== null && oldFiber.tag === HostText) {
deleteChild(returnFiber, oldFiber.sibling)
const existing = useFiber(oldFiber, text)
existing.return = returnFiber
return existing
}
deleteChild(returnFiber, oldFiber)
const fiber = new FiberNode(HostText, text)
fiber.return = returnFiber
return placeChild(fiber)
}
}
3.9.2.3 补充reconcileSingleElement
方法diff
逻辑
- 如果旧节点和
newChild
的key
和elementType
都相同,复用旧节点,由于新节点没有兄弟节点,所以需要删除旧节点的兄弟节点 key
和elementType
不相同则删除旧节点及其兄弟节点,创建新节点
function createChildReconciler(shouldTrackSideEffects) {
function reconcileSingleElement(returnFiber, oldFiber, newChild) {
if (
oldFiber !== null &&
oldFiber.key === newChild.key &&
oldFiber.elementType === newChild.type
) {
deleteChild(returnFiber, oldFiber.sibling)
const fiber = useFiber(oldFiber, newChild.props)
fiber.return = returnFiber
return fiber
}
deleteChild(returnFiber, oldFiber)
const fiber = createFiberFormElement(newChild)
fiber.return = returnFiber
return placeChild(fiber)
}
}
3.9.2.4 补充reconcileChildrenArray
方法diff
逻辑
采用线性比对的思想,即判断当前新旧节点是否相同,相同则复用,不同则创建新节点,将旧节点标记删除,然后继续比对下一个节点
function createChildReconciler(shouldTrackSideEffects) {
function reconcileChildrenArray(returnFiber, oldFiber, newChild) {
let firstChildFiber = null
let prevChildFiber = null
for (let newIdx = 0; newIdx < newChild.length; newIdx++) {
let nextFiber = null
if (typeof newChild[newIdx] === 'object' && newChild[newIdx] !== null) {
const { key, type, props } = newChild[newIdx]
if (
oldFiber !== null &&
oldFiber.key === key &&
oldFiber.elementType === type
) {
nextFiber = placeChild(useFiber(oldFiber, props))
} else {
deleteChild(returnFiber, oldFiber, false)
nextFiber = placeChild(createFiberFormElement(newChild[newIdx]))
}
} else if (typeof newChild[newIdx] === 'string') {
if (oldFiber !== null && oldFiber.tag === HostText) {
nextFiber = placeChild(useFiber(oldFiber, newChild[newIdx]))
} else {
deleteChild(returnFiber, oldFiber, false)
nextFiber = placeChild(creaetFiberFromText(newChild[newIdx]))
}
} else {
if (oldFiber) deleteChild(returnFiber, oldFiber, false)
}
if (oldFiber) oldFiber = oldFiber.sibling
if (nextFiber !== null) {
nextFiber.return = returnFiber
// 记录第一个child FiberNode节点
if (firstChildFiber === null) {
firstChildFiber = nextFiber
}
// 建立兄弟FiberNode节点关联关系
if (prevChildFiber === null) {
prevChildFiber = nextFiber
} else {
prevChildFiber.sibling = nextFiber
prevChildFiber = nextFiber
}
}
}
return firstChildFiber
}
3.9.2.5 修改reconcileChildFibers
方法
如果nextChild
既不是对象也不是字符串,则将旧节点删除掉
function createChildReconciler(shouldTrackSideEffects) {
function reconcileChildFibers(workInProgress, currentFirstChild, nextChildren) {
if (typeof newChild === 'object' && newChild !== null) {
...
}
if (typeof newChild === 'string') {
...
}
return deleteChild(returnFiber, currentFirstChild)
}
}
3.10 构建DOM
树
3.10.1 补充completeWork
方法代码逻辑
对于HostComponent
和HostText
类型的FiberNode
节点添加current
不为null
时对应的处理逻辑,current
不为null
说明在旧FiberNode Tree
中存在新FiberNode
节点对应的旧FiberNode
节点,且已有DOM
节点,所以不需要在走构建DOM
树逻辑,只需要在更新DOM
阶段,遍历FiberNode
节点,执行对应的副作用处理逻辑即可
function completeWork(workInProgress) {
const current = workInProgress.alternate
switch (workInProgress.tag) {
case HostComponent: {
if (current != null) {
if (workInProgress.pendingProps) {
workInProgress.flags |= Update
}
bubbleProperties(workInProgress)
return
}
// 创建DOM节点
const { elementType, pendingProps } = workInProgress
const instance = document.createElement(elementType)
// 将DOM节点赋值给FiberNode的stateNode属性
workInProgress.stateNode = instance
// 设置DOM节点属性
for (const propKey in pendingProps) {
const prpoValue = pendingProps[propKey]
setProp(instance, propKey, prpoValue)
}
// 递归遍历FiberNode节点将对应的DOM节点添加到父DOM节点中,构建DOM树
appendAllChildren(instance, workInProgress)
// 收集子树FiberNode节点副作用
bubbleProperties(workInProgress)
return null
}
case HostText: {
if (current !== null) {
if (workInProgress.pendingProps !== current.pendingProps) {
workInProgress.flags |= Update
}
bubbleProperties(workInProgress)
return null
}
const { pendingProps } = workInProgress
const instance = document.createTextNode(pendingProps)
workInProgress.stateNode = instance
bubbleProperties(workInProgress)
return null
}
}
}
3.11 更新DOM
3.11.1 添加删除逻辑
遍历FiberNode
节点的deletions
属性中的节点,从DOM
树中移除相应DOM
节点
// 获取子DOM节点
function getChildNode(finishWork) {
let childFiber = finishWork.child
while (childFiber !== null) {
if (childFiber.tag === FunctionComponent) {
childFiber = childFiber.child
continue
}
break
}
return childFiber.stateNode
}
function recursivelyTraverseMutationEffects(finishWork) {
if (finishWork.deletions !== null) {
const parentNode =
finishWork.tag === HostComponent
? finishWork.stateNode
: getParentNode(finishWork)
// 将旧节点对应的DOM节点从DOM树移除
finishWork.deletions.forEach((fiber) => {
const childNode = getChildNode(fiber)
parentNode.removeChild(childNode)
})
}
// 为true说明子树FiberNode节点有副作用需要处理,递归遍历child FiberNode
if (finishWork.subtreeFlags & (Placement | Update | ChildDeletion)) {
let child = finishWork.child
while (child) {
commitMutationEffectsOnFiber(child)
child = child.sibling
}
}
}
3.11.2 添加插入DOM
节点逻辑
function commitHostPlacement(finishWork) {
const parentNode = getParentNode(finishWork)
parentNode.appendChild(finishWork.stateNode)
}
function commitReconciliationEffects(finishWork) {
// 为true说明需要执行插入DOM节点逻辑
if (finishWork.flags & Placement) {
commitHostPlacement(finishWork)
}
}
function commitMutationEffectsOnFiber(finishWork) {
switch (finishWork.tag) {
case HostComponent: {
recursivelyTraverseMutationEffects(finishWork)
commitReconciliationEffects(finishWork)
break
}
case HostText: {
commitReconciliationEffects(finishWork)
break
}
}
}
3.11.3 添加更新逻辑
function commitMutationEffectsOnFiber(finishWork) {
switch (finishWork.tag) {
case HostComponent: {
recursivelyTraverseMutationEffects(finishWork)
commitReconciliationEffects(finishWork)
// 更新DOM属性
if (finishWork.flags & Update) {
const { pendingProps, stateNode } = finishWork
for (const propKey in pendingProps) {
const prpoValue = pendingProps[propKey]
setProp(stateNode, propKey, prpoValue)
}
}
break
}
case HostText: {
commitReconciliationEffects(finishWork)
// 修改文本内容
if (finishWork.flags & Update) {
finishWork.stateNode.textContent = finishWork.pendingProps
}
break
}
}
}
四. 总结
React每次更新渲染都会构建虚拟DOM
树,与旧虚拟DOM
树进行比对,获取节点需要处理的副作用,在更新DOM
阶段,遍历虚拟DOM
树节点,执行副作用处理逻辑,完成DOM
树的更新。代码仓库