简介
在本篇文章中,我们将一起探索如何从零构建一个迷你版的React框架。通过这个过程,不仅能够加深对React内部机制的理解,还能学习到虚拟DOM、组件生命周期和状态管理等核心概念的实际应用。这篇文章旨在深入浅出地理解React工作原理。
问题清单:
在上一节中,我们完成了分块渲染和批量提交的功能。接下来,我们将进行事件绑定和数据更新。
事件绑定
Q1:怎么处理事件绑定?
首先,让我们更新测试用例,加入一个新的事件。
jsx
// App.jsx
function Foo({ num }) {
function handleClick() {
console.log('click')
}
return (
<div>
{num}
<div>
<button onClick={handleClick}>click</button>
</div>
</div>
)
}
function App() {
return (
<div>
<Foo num={10} />
</div>
)
}
到目前为止,我们还未处理该事件。我们将在updateProps中添加事件处理逻辑。如你所见,我们已经完成了事件的绑定。
jsx
// React.js
// 省略...
function updateProps(dom, props) {
const isEvent = (key) => key.startsWith('on')
const isProperty = (key) => key !== 'children' && !isEvent(key)
Object.keys(props)
.filter(isProperty)
.forEach((name) => {
dom[name] = props[name]
})
Object.keys(props)
.filter(isEvent)
.forEach((name) => {
const eventType = name.toLowerCase().substring(2)
dom.addEventListener(eventType, props[name])
})
}
数据更新
数据更新时,如何知道哪些元素需要更新呢?
这时候就需要diff
来对新旧两个Fiber
树进行差异比较。
我们需要做到的是:
Q2:如何实现React
的数据更新?
jsx
// React.js
function commitRoot() {
commitWork(workInProcessRoot.child)
// 记录当前的fiber树 oldFiber
currentRoot = workInProcessRoot
workInProcessRoot = null
}
// 增加alternate属性,用于保存上一次的fiber树
function update() {
workInProcessRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot
}
nextUnitOfWork = workInProcessRoot
}
// 执行diff
function reconcileChildren(wipFiber, children) {
let prevSibling = null
let oldFiber = wipFiber.alternate && wipFiber.alternate.child
let index = 0
while (index < children.length) {
// 对比旧的fiber和新的children
// effectTag用于标记fiber节点的操作类型
const element = children[index]
const isSameType = oldFiber && element && element.type === oldFiber.type
let newFiber = null
if (isSameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
parent: wipFiber,
dom: oldFiber.dom,
alternate: oldFiber,
effectTag: 'UPDATE'
}
} else {
newFiber = {
type: element.type,
props: element.props,
parent: wipFiber,
dom: null,
alternate: null,
effectTag: 'PLACEMENT'
}
}
if (oldFiber) {
// 下一次循环中找到oldFiber的兄弟节点
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
// commitWork来处理更新还是创建
function commitWork(fiber) {
if (!fiber) {
return
}
let fiberParent = fiber.parent
while (!fiberParent.dom) {
fiberParent = fiberParent.parent
}
if (fiber.dom) {
if (fiber.effectTag === 'PLACEMENT') {
fiberParent.dom.appendChild(fiber.dom)
} else if (fiber.effectTag === 'UPDATE') {
// 将新旧节点的props传入
updateProps(fiber.dom, fiber.alternate.props, fiber.props)
}
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function updateProps(dom, prevProps, nextProps) {
const isEvent = (key) => key.startsWith('on')
const isProperty = (key) => key !== 'children' && !isEvent(key)
// 判断属性是否更新
const isNew = (prev, next) => (key) => prev[key] !== next[key]
// 判断属性是否被删除
const isGone = (next) => (key) => !(key in next)
Object.keys(prevProps)
.filter(isEvent)
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2)
dom.removeEventListener(eventType, prevProps[name])
})
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(nextProps))
.forEach((name) => {
// 为什么不是 dom.removeAttribute(name)?
// 因为removeAttribute会移除所有属性,包括value,checked等
dom[name] = ''
})
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
dom[name] = nextProps[name]
})
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2)
dom.addEventListener(eventType, nextProps[name])
})
}
到此为止,我们已经完成了数据更新。但实际上,在使用过程中,我们并不是这样定义变量的。如果采用类似React的方式,应该如何实现呢?
useState
当然,在日常使用中,我们通常不是用update来更新变量,而是这样创建变量的。
jsx
import React from './core/React'
function Foo({ num }) {
const [count, setCount] = React.useState(0)
const [foo, setFoo] = React.useState(111)
function handleClick() {
setCount((c) => c + 1)
setFoo(222)
}
return (
<div>
<div>Num: {num}</div>
<div>Foo: {foo}</div>
<div>Count: {count}</div>
<div>
<button onClick={handleClick}>click</button>
</div>
</div>
)
}
function App() {
return (
<div>
<Foo num={10} />
</div>
)
}
export default App
Q3:如何实现useState
?
需求分析:
- useState 是一个函数,它接收状态的初始值作为参数,返回一个数组。
- 返回的数组包含两个元素:当前的状态值和一个更新该状态的函数。
- 更新函数(例如上面的 setCount)可以被用来修改状态值,当状态值被修改时,组件将重新渲染。
- useState 可以在一个组件中多次被调用,以创建多个状态变量。
jsx
// React.js
// 保存state的hook
let stateHooks = null
// 保存当前的hook索引
let currentHookIndex = 0
function useState(initial) {
let currentFiber = workInProcessRoot
// 查找是否有上一次的fiber树
const oldHook = currentFiber.alternate?.stateHooks[currentHookIndex]
const stateHook = {
state: oldHook ? oldHook.state : initial
}
currentHookIndex++
stateHooks.push(stateHook)
currentFiber.stateHooks = stateHooks
function setState(newState) {
// 处理 setState(newState) 和 setState((state) => newState) 两种情况
const isFunction = typeof newState === 'function'
stateHook.state = isFunction ? newState(stateHook.state) : newState
update()
}
return [stateHook.state, setState]
}
function commitRoot() {
commitWork(workInProcessRoot.child)
currentRoot = workInProcessRoot
workInProcessRoot = null
// 重置hook
stateHooks = null
currentHookIndex = 0
}
总结
我们成功实现了事件绑定和数据更新机制。通过这一过程,我们深入理解了React框架下如何将事件监听器附加到DOM元素上,并且在Fiber架构中引入了一个专门的变量用于存储组件状态。同时,我们也提供了一个setState函数,它能够有效触发并管理状态的更新。这些成果不仅展示了React的交互性特点,还强调了状态管理对于构建响应式用户界面的重要性。