简介
在本篇文章中,我们将一起探索如何从零构建一个迷你版的React框架。通过这个过程,不仅能够加深对React内部机制的理解,还能学习到虚拟DOM、组件生命周期和状态管理等核心概念的实际应用。这篇文章旨在深入浅出地理解React工作原理。
通过前面的章节,我们已经实现了数据在页面上的渲染,并定义及更新了数据变量。接下来,我们将优化现有的更新逻辑。
问题清单:
当前的逻辑是从根节点开始遍历进行更新,如果节点较深,则会耗费大量时间。那么,我们应该如何优化呢?
Q1:React中怎么优化更新数据状态更新的?
- 记录当前需更新的节点
- 收集待更新操作,统一进行更新
jsx
// 现在
function update() {
workInProcessRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot
}
nextUnitOfWork = workInProcessRoot
}
所以,我们需要一个变量来存储当前需要改变的节点和需要更新的action
。
jsx
function useState(initial) {
let currentFiber = workInProcessFiber
const oldHook = currentFiber.alternate?.stateHooks[currentHookIndex]
const stateHook = {
state: oldHook ? oldHook.state : initial,
// 保存setState的action
queue: oldHook ? oldHook.queue : []
}
// 当组件更新时,将queue中的action依次执行
stateHook.queue.forEach((action) => {
stateHook.state = action(stateHook.state)
})
stateHook.queue = []
currentHookIndex++
stateHooks.push(stateHook)
currentFiber.stateHooks = stateHooks
function setState(action) {
const eagerState =
typeof action === 'function' ? action(stateHook.state) : action
if (eagerState === stateHook.state) {
return
}
// 当setState被调用时,将新的state放入queue中
stateHook.queue.push(typeof action === 'function' ? action : () => action)
workInProcessRoot = {
// 更新时只更新当前节点
...currentFiber,
alternate: currentFiber
}
nextUnitOfWork = workInProcessRoot
}
return [stateHook.state, setState]
}
function updateFunctionComponent(fiber) {
workInProcessFiber = fiber
// 重置状态
workInProcessFiber.stateHooks = []
currentHookIndex = 0
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
useEffect
我们来看看官网的用法:
jsx
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
Q2:怎么实现useEffect
?
分析:
- 触发时机是在组件渲染完成后,即收集的回调函数将在组件渲染完毕后执行。
- 第一个参数是执行函数,第二个参数是监听的变量
- 返回一个副作用函数,在更新后执行
jsx
function updateFunctionComponent(fiber) {
workInProcessFiber = fiber
workInProcessFiber.stateHooks = []
// 重置
workInProcessFiber.effectHooks = []
currentHookIndex = 0
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function commitRoot() {
commitWork(workInProcessRoot.child)
// 在渲染完成后,触发effect hooks
commitEffectHooks()
currentRoot = workInProcessRoot
workInProcessRoot = null
currentHookIndex = 0
}
function commitEffectHooks() {
function runEffects(fiber) {
if (!fiber) return
// 如果是新创建的fiber节点,那么就是mount,否则就是update
const isMount = !fiber.alternate
fiber.effectHooks?.forEach((effectHook, index) => {
if (isMount) {
effectHook.cleanup = effectHook.callback()
return
}
// handle deps change
const oldEffectHook = fiber.alternate?.effectHooks[index]
const depsChanged = haveDepsChanged(oldEffectHook?.deps, effectHook.deps)
if (depsChanged) {
oldEffectHook.cleanup?.()
effectHook.cleanup = effectHook.callback()
}
})
}
function haveDepsChanged(oldDeps = [], newDeps = []) {
return oldDeps.some((dep, i) => dep !== newDeps[i])
}
function traverseFiber(fiber) {
if (!fiber) return
// 处理当前fiber节点的副作用
runEffects(fiber)
traverseFiber(fiber.child)
traverseFiber(fiber.sibling)
}
// 从根fiber节点开始遍历和处理
traverseFiber(workInProcessFiber)
}
注意
在前面的章节中,我们没有处理一个边界情况:当fiber树长度不一致时,未对多余的节点进行删除。现在我们将补充这个边界情况的处理。
jsx
// 创建全局变量
let deletions = []
function commitRoot() {
deletions.forEach(commitDeletion)
// 省略代码...
deletions = []
}
function commitDeletion(fiber) {
if (fiber.dom) {
let fiberParent = fiber.parent
while (!fiberParent.dom) {
fiberParent = fiberParent.parent
}
fiberParent.dom.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child)
}
}
function reconcileChildren(wipFiber, children) {
while (index < children.length) {
// 省略代码...
if (isSameType) {
// 省略代码...
} else {
// 省略代码...
if (oldFiber) {
deletions.push(oldFiber)
}
}
// 省略代码...
}
while (oldFiber) {
deletions.push(oldFiber)
oldFiber = oldFiber.sibling
}
}
至此,我们已基本实现了数据渲染、更新以及useState和useEffect的功能。