持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情
这一节我们实现 useReducer 和 useState,让函数组件支持数据更新。
👉 仓库地址,跪求您帮忙点个 🌟🌟,谢谢啦~ PS:本节代码在 v0.0.3 分支
使用 useReducer
在此之前,需要在demo/which-react.js中将useReducer从react中引入,并将ReactDOM更换为从react-dom引入
import { Component, Fragment, useReducer } from 'react';
import ReactDOM from 'react-dom/client';
// import { Component, Fragment } from '../src/react';
// import ReactDOM from '../src/react-dom';
export {
Component,
Fragment,
useReducer,
ReactDOM
}
在demo/src/main.jsx中的 FunctionComponent 中添加useReducer
function FunctionComponent(props) {
const [state, dispatch] = useReducer(x => x + 1, 0)
return (
<div className='function'>
<p>{props.name}</p>
<div>{state}</div>
<button onClick={dispatch} >+1</button>
</div>
)
}
那么每次点击button都会将state + 1,有了我们想要的效果,接下来我们去实现它
实现 useReducer
将 React 的引入重新设定为自己写的(修改which-react.js),创建src/ReactFiberHooks.ts
export function useReducer(reducer, initalState) {
const dispatch = () => {
console.log('useReducer dispatch log')
}
// 暂时直接返回
return [initalState, dispatch]
}
接着会发现点击 button 没有触发 dispatch 里的 console,这是因为我们还没有实现 React 事件,暂时只是将事件作为属性挂在dom上
// src/utils.ts
export function updateNode(node, nextVal) {
Object.keys(nextVal).forEach(key => {
if (key === 'children') {
if (isStringOrNumber(nextVal[key])) {
node.textContent = nextVal[key]
}
} else {
node[key] = nextVal[key] // 这里直接当作属性放到 dom 上了
}
})
}
这里我们先简单处理一下,能让事件能响应(注意这里并不是真正的 React 事件实现方式)
export function updateNode(node: HTMLElement, nextVal) {
Object.keys(nextVal).forEach(key => {
if (key === 'children') {
if (isStringOrNumber(nextVal[key])) {
node.textContent = nextVal[key]
}
} else if (key.slice(0, 2) === 'on') {
// 简单处理一下事件响应(并不是真正的React 事件)
const eventName = key.slice(2).toLocaleLowerCase()
node.addEventListener(eventName, nextVal[key])
} else {
node[key] = nextVal[key]
}
})
}
点击按钮能让console.log('useReducer dispatch log')执行出来了
实现 mount 时的 useReducer
import { Fiber } from "./ReactFiber"
interface Hook {
memoizedState: any, // state
next: null | Hook // 下一个 hook
}
// 当前正在渲染的 fiber
let currentlyRenderingFiber: Fiber | null = null
// 没什么特别的意义,就是想返回一个 Fiber 类型 不让 ts 报错
function getCurrentlyRenderingFiber() {
return currentlyRenderingFiber as Fiber
}
// 当前正在处理的 hook
let workInProgressHook: Hook | null = null
export function renderWithHooks(workInProgress: Fiber) {
currentlyRenderingFiber = workInProgress
currentlyRenderingFiber.memoizedState = null
workInProgressHook = null
}
function updateWorkInProgressHook() {
currentlyRenderingFiber = getCurrentlyRenderingFiber()
let hook
const current = currentlyRenderingFiber.alternate
// current 存在说明是 update,否则就是 mount
if (current) {
// 复用之前的 hook
currentlyRenderingFiber.memoizedState = current.memoizedState
// 看是否是第一个 hook
if (workInProgressHook) {
// 不是,则拿到下一个 hook,同时更新 workInProgressHook
workInProgressHook = hook = workInProgressHook.next
} else {
// 是第一个 hook ,拿到第一个hook
workInProgressHook = hook = currentlyRenderingFiber.memoizedState
}
} else {
// mount 时需要新建hook
hook = {
memoizedState: null, // state
next: null // 下一个 hook
}
if (workInProgressHook) {
workInProgressHook = workInProgressHook.next = hook
} else {
// 第一个 hook,将 hook 放到 fiber 的 state 上,同时更新 workInProgressHook
workInProgressHook = currentlyRenderingFiber.memoizedState = hook
}
}
// 最终返回 hook(也就是 workInProgressHook)
return hook
}
export function useReducer(reducer, initalState) {
currentlyRenderingFiber = getCurrentlyRenderingFiber()
const hook = updateWorkInProgressHook()
if (!currentlyRenderingFiber.alternate) {
// 初次渲染时将默认数据放到 hook.memoizedState 上即可
hook.memoizedState = initalState
}
const dispatch = () => {
console.log('useReducer dispatch log')
}
// 返回 state
return [hook.memoizedState, dispatch]
}
在处理 FunctionComponent 时更新正在处理的 fiber(也就是当前的这个函数)
// ReactFiberReconciler.ts
export function updateFunctionComponent(workInProgress: Fiber) {
// 更新正在处理的 fiber
renderWithHooks(workInProgress)
const { type, props } = workInProgress
const children = type(props)
reconcileChildren(workInProgress, children)
}
实现 update 时的 useReducer
现在页面在浏览器中渲染正常,没有报错,点击按钮还只是console,接下来我们处理 update 时。
update 时需要让数据更新(页面更新),之前是通过调scheduleUpdateOnFiber进行更新,这里也需要使用
export function useReducer(reducer, initalState) {
const hook = updateWorkInProgressHook()
if (!currentlyRenderingFiber?.alternate) {
// 初次渲染
hook.memoizedState = initalState
}
const dispatch = () => {
// 修改状态值(将旧的state传给使用者,然后返回新的state给 hook)
hook.memoizedState = reducer(hook.memoizedState); // 后面有圆括号,需要加分号
// 更新之前将 currentlyRenderingFiber 设置为自己的 alternate
(currentlyRenderingFiber as Fiber).alternate = { ...currentlyRenderingFiber as Fiber }
// 更新
scheduleUpdateOnFiber(currentlyRenderingFiber as Fiber)
console.log('useReducer dispatch log')
}
return [hook.memoizedState, dispatch]
}
因为我们写的 FragmentComponent 也是 FunctionComponent所以在 mount 完以后 currentlyReeringFiber 就指向了这个 FragmentComponent,这里先保证写的最后一个 FunctionComponent 是包含有刚刚写的 useReducer 的组件,将其他组件都注视掉,有其他问题后面再处理。
刷新页面,点击以后页面从这个 FunctionComponent 往后的组件都会再出现一遍,同时 state 是最新的值,这是因为我们在 reconcileChildren 时创建的Fiber的 flags 是 Placement,每次都会重新创建新的 dom
实现节点的复用
因为diff比较复杂,我们这节的重点是实现useReducer,所以只会实现一个简单的diff(sameNode 函数判断能否复用)
function reconcileChildren(workInProgress: Fiber, children) {
if (isStringOrNumber(children)) {
return
}
// 这里先将子节点都当作数组来处理
const newChildren: any[] = isArray(children) ? children : [children]
// oldFiber 的头节点
let oldFiber = workInProgress.alternate?.child
// 用于保存上个 fiber 节点
let previousNewFiber: Fiber | null = null
for (let i = 0; i < newChildren.length; i++) {
const newChild = newChildren[i]
if (newChild === null) {
// 会遇到 null 的节点,直接忽略即可
continue
}
const newFiber = createFiber(newChild, workInProgress)
// 能否复用
const same = sameNode(newFiber, oldFiber)
if (same) {
// 能复用
Object.assign(newFiber, {
stateNode: (oldFiber as Fiber).stateNode,
alternate: oldFiber as Fiber,
flags: Update
})
}
if (oldFiber) {
// 处于for 中,oldFiber 也需要更新到下一个 fiber
oldFiber = oldFiber.sibling
}
if (previousNewFiber === null) {
// 第一个子节点直接保存到 workInProgress 上
workInProgress.child = newFiber
} else {
// 后续都保存到上一个节点的 sibling 上
previousNewFiber.sibling = newFiber
}
// 更新
previousNewFiber = newFiber
}
}
// 节点复用条件
// 1. 同层级
// 2. type 相同
// 3. key 相同
function sameNode(a, b) {
return a && b && a.type === b.type && a.key === b.key
}
接着在 commit 时需要处理节点更新时的情况
// ReactFIberWorkLoop.ts
function commitWorker(workInProgress: Fiber | null) {
// ......
if (flags & Update && stateNode) {
// 更新属性
updateNode(
stateNode,
(workInProgress.alternate as Fiber).props,
workInProgress.props
)
}
// ......
}
在updateNode函数中需要将旧的属性和原本监听的事件都移除掉,重新赋值新的属性和监听事件
// src/utils.ts
export function updateNode(node: HTMLElement, prevVal, nextVal) {
// 遍历老的props,将原本的事件以及新props不存在的属性移除
Object.keys(prevVal)
.forEach((key) => {
if (key === "children") {
// 有可能是文本,直接将其清除
if (isStringOrNumber(prevVal[key])) {
node.textContent = "";
}
} else if (key.slice(0, 2) === "on") {
// 事件需要移除掉
const eventName = key.slice(2).toLocaleLowerCase();
node.removeEventListener(eventName, prevVal[key]);
} else {
// 对于老的key存在oldProps,新的上面不存在的需要将其处理掉(remove)
if (!(key in nextVal)) {
node[key] = "";
}
}
});
// 更新 props
Object.keys(nextVal)
.forEach((key) => {
if (key === "children") {
// 有可能是文本
if (isStringOrNumber(nextVal[key])) {
node.textContent = nextVal[key] + "";
}
} else if (key.slice(0, 2) === "on") {
const eventName = key.slice(2).toLocaleLowerCase();
node.addEventListener(eventName, nextVal[key]);
} else {
node[key] = nextVal[key];
}
});
}
函数组件的 useReducer 就实现了更新。但还有一些需要解决的bug。
我们发现,如果存在多个FunctionComponent时,会因为 currntkyRendingFiber 是一个全局变量,导致最终指向的是最后一个 FunctionComponent,使得我们的 state,无法被更新,反而出现最后一个组件重新渲染一次问题。
所以我们需要保证在使用 dispatch 时内部的 fiber 是当前组件的 fiber。那么我们可以利用 bind 可以存储住参数的特性,将 currentlyRenderingFiber 作为 bind 时的预制参数,使得我们在调用 dispatch 时拿到的 fiber 是使用这个 hook 的 fiber。
// src/ReactFiberHooks.ts
export function useReducer(reducer, initalState) {
const hook = updateWorkInProgressHook()
if (!(currentlyRenderingFiber as Fiber).alternate) {
// 初次渲染
hook.memoizedState = initalState
}
// 因为 currentlyRenderingFiber 是全局变量,可能会导致存储的 fiber 不是需要更新 state 的 fiber
// 所以需要通过 bind 在 dispatchReducerAction 这个函数内存储住相关信息(fiber 等),在调用时能获取需要更新的 fiber
const dispatch = dispatchReducerAction.bind(
null,
currentlyRenderingFiber,
hook,
reducer
)
return [hook.memoizedState, dispatch]
}
function dispatchReducerAction(fiber: Fiber, hook: Hook, reducer, action) {
hook.memoizedState = reducer(hook.memoizedState)
// 更新之前将 currentlyRenderingFiber 设置为自己的 alternate
fiber.alternate = { ...fiber }
// 更新
scheduleUpdateOnFiber(fiber)
}
state 不更新的问题解决了,还有组件重新渲染的问题。这个其实也很简单。因为我们只需要当前组件更新,其他组件不需要动,所以我们在 scheduleUpdateOnFiber 时只需要提交当前 fiber 即可
function dispatchReducerAction(fiber: Fiber, hook: Hook, reducer, action) {
hook.memoizedState = reducer(hook.memoizedState)
fiber.alternate = { ...fiber }
// 因为我们只更新这一个 fiber 组件,不能影响其他组件,所以需要将 sibling 设置为 null,避免后续组件被重复渲染
fiber.sibling = null
// 更新当前 fiber
scheduleUpdateOnFiber(fiber)
}
至此,我们完成了 useReducer 的功能了。在这个功能中,我们实现了 fiber、dom 的复用和更新
实现 useState
实现完 useReducer,会发现 useState 的实现是差不多的,区别就在于 useState 没有 reducer 参数,并在使用 dispatch 时会将新的值传递过来,我们对 useReducer 稍作修改后兼容实现 useState
export function useState(initalState) {
// 因为我们没有 reduce ,所以传 null
return useReducer(null, initalState)
}
function dispatchReducerAction(fiber: Fiber, hook: Hook, reducer, action) {
// 通过判断是否有 reducer,来决定 state 的值是 reducer 执行的结果,来还是根据传进来的参数
// 其实就是判断是 useReducer 还是 useState
hook.memoizedState = reducer ? reducer(hook.memoizedState) : action
fiber.alternate = { ...fiber }
fiber.sibling = null
scheduleUpdateOnFiber(fiber)
}
优化 useReducer
在我们实际使用 useReducer 时,可能是下面这样使用的,在调用 dispatch 时也需要支持参数。
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function FunctionComponent(props) {
const [data, setData] = useReducer(reducer, { count: 0 })
return (
<div className='function'>
<div>{ data.count }</div>
<button onClick={ () => setData({ type: 'increment' }) }>data.count + 1</button>
<button onClick={ () => setData({ type: 'decrement' }) }>data.count - 1</button>
</div>
)
}
我们只需要将 dispatch 中的 action 传入到reducer中即可
function dispatchReducerAction(fiber: Fiber, hook: Hook, reducer, action) {
// 通过判断是否有 reducer,来决定 state 的值是 reducer 执行的结果,来还是根据传进来的参数
// 其实就是判断是 useReducer 还是 useState
hook.memoizedState = reducer ? reducer(hook.memoizedState, action) : action
fiber.alternate = { ...fiber }
fiber.sibling = null
scheduleUpdateOnFiber(fiber)
}
至此,我们实现了useReducer和useState,提供了更新页面数据的能力。
👉 仓库地址,跪求您帮忙点个 🌟🌟,谢谢啦~ PS:本节代码在 v0.0.3 分支