当前位置:网站首页>03|实现 useReducer 和 useState
03|实现 useReducer 和 useState
2022-07-26 16:37:00 【东方睡衣】
使用 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, // statenext: 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 = workInProgresscurrentlyRenderingFiber.memoizedState = nullworkInProgressHook = null
}
function updateWorkInProgressHook() {currentlyRenderingFiber = getCurrentlyRenderingFiber()let hookconst current = currentlyRenderingFiber.alternate// current 存在说明是 update,否则就是 mountif (current) {// 复用之前的 hookcurrentlyRenderingFiber.memoizedState = current.memoizedState// 看是否是第一个 hookif (workInProgressHook) {// 不是,则拿到下一个 hook,同时更新 workInProgressHookworkInProgressHook = hook = workInProgressHook.next} else {// 是第一个 hook ,拿到第一个hookworkInProgressHook = hook = currentlyRenderingFiber.memoizedState}} else {// mount 时需要新建hookhook = {memoizedState: null, // statenext: null // 下一个 hook}if (workInProgressHook) {workInProgressHook = workInProgressHook.next = hook} else {// 第一个 hook,将 hook 放到 fiber 的 state 上,同时更新 workInProgressHookworkInProgressHook = 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')}// 返回 statereturn [hook.memoizedState, dispatch]
}
在处理 FunctionComponent 时更新正在处理的 fiber(也就是当前的这个函数)
// ReactFiberReconciler.ts
export function updateFunctionComponent(workInProgress: Fiber) {// 更新正在处理的 fiberrenderWithHooks(workInProgress)const { type, props } = workInProgressconst 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 = nullfor (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] = "";}}});// 更新 propsObject.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 等),在调用时能获取需要更新的 fiberconst 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// 更新当前 fiberscheduleUpdateOnFiber(fiber)
}
至此,我们完成了 useReducer 的功能了。在这个功能中,我们实现了 fiber、dom 的复用和更新
实现 useState
实现完 useReducer,会发现 useState 的实现是差不多的,区别就在于 useState 没有 reducer 参数,并在使用 dispatch 时会将新的值传递过来,我们对 useReducer 稍作修改后兼容实现 useState
export function useState(initalState) {// 因为我们没有 reduce ,所以传 nullreturn useReducer(null, initalState)
}
function dispatchReducerAction(fiber: Fiber, hook: Hook, reducer, action) {// 通过判断是否有 reducer,来决定 state 的值是 reducer 执行的结果,来还是根据传进来的参数// 其实就是判断是 useReducer 还是 useStatehook.memoizedState = reducer ? reducer(hook.memoizedState) : actionfiber.alternate = { ...fiber }fiber.sibling = nullscheduleUpdateOnFiber(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 还是 useStatehook.memoizedState = reducer ? reducer(hook.memoizedState, action) : actionfiber.alternate = { ...fiber }fiber.sibling = nullscheduleUpdateOnFiber(fiber)
}
至此,我们实现了useReducer和useState,提供了更新页面数据的能力。
仓库地址,跪求您帮忙点个 ,谢谢啦~ PS:本节代码在 v0.0.3 分支
边栏推荐
- Recurrence of historical loopholes in ThinkPHP
- Alibaba side: analysis of ten classic interview questions
- 【飞控开发基础教程2】疯壳·开源编队无人机-定时器(LED 航情灯、指示灯闪烁)
- regular expression
- How does win11 reinstall the system?
- 搭建typora图床
- 37.【重载运算符的类别】
- 2022 software testing skills postman+newman+jenkins continuous integration practical tutorial
- Tcpdump命令详解
- [Luogu p8063] shortest paths (graph theory)
猜你喜欢

PXE efficient batch network installation

Marketing guide | several common micro blog marketing methods

After Oracle creates a table partition, the partition is not given during the query, but the value specified for the partition field will be automatically queried according to the partition?

How to implement Devops with automation tools | including low code and Devops application practice
It turns out that cappuccino information security association does this. Let's have a look.

How can win11 system be reinstalled with one click?

Realizing DDD based on ABP -- related concepts of DDD

How does the data link layer transmit data
![Sharing of 40 completed projects of high-quality information management specialty [source code + Thesis] (VI)](/img/b9/629449d3c946b017075ed42eaa81bf.png)
Sharing of 40 completed projects of high-quality information management specialty [source code + Thesis] (VI)

Matlab paper illustration drawing template issue 40 - pie chart with offset sector
随机推荐
It turns out that cappuccino information security association does this. Let's have a look.
6种方法帮你搞定SimpleDateFormat类不是线程安全的问题
About the difference between BigDecimal conversion string toengineeringstring, toplainstring and toString
Batch normalization batch_ normalization
Pass-19,20
2022-2023 topic recommendation of information management graduation project
2022软件测试技能 Postman+newman+jenkins 持续集成 实战教程
"Green is better than blue". Why is TPC the last white lotus to earn interest with money
Alibaba side: analysis of ten classic interview questions
Definition and relationship of derivative, differential, partial derivative, total derivative, directional derivative and gradient
第一章概述-------第一节--1.3互联网的组成
TensorFlow Lite源码解析
How to connect tdengine with idea database tool?
40个高质量信息管理专业毕设项目分享【源码+论文】(六)
2022-2023 信息管理毕业设计选题题目推荐
Create MySQL function: access denied; you need (at least one of) the SUPER privilege(s) for this operation
Realizing DDD based on ABP -- related concepts of DDD
浅谈云原生边缘计算框架演进
Add 5g and AI, oppo announced to invest 10billion R & D funds next year!
Pyqt5 rapid development and practice 3.4 signal and slot correlation