当前位置:网站首页>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 分支
边栏推荐
- [flight control development basic tutorial 3] crazy shell · open source formation UAV - serial port (basic transceiver)
- Detailed explanation of tcpdump command
- Response object - response character data
- IDEA 阿里云多模块部署
- Movable view component (it can be dragged up, down, left and right)
- 导数、微分、偏导数、全微分、方向导数、梯度的定义与关系
- Use verdaccio to build your own NPM private library
- [Luogu p8063] shortest paths (graph theory)
- Digital currency of quantitative transactions - merge transaction by transaction data through timestamp and direction (large order consolidation)
- Implementing DDD based on ABP -- aggregation and aggregation root practice
猜你喜欢

Redis persistence - detailed analysis of RDB source code | nanny level analysis! The most complete network

UPC 2022 summer personal training game 07 (part)

Win11 auto delete file setting method

"Green is better than blue". Why is TPC the last white lotus to earn interest with money

Review the past and know the new MySQL isolation level

My SQL is OK. Why is it still so slow? MySQL locking rules

Merge multiple row headers based on apache.poi operation

【飞控开发基础教程3】疯壳·开源编队无人机-串口(基础收发)

Oracle创建表分区后,查询的时候不给出partition,但是会给分区字段指定的值,会不会自动按照分区查询?

Pass-19,20
随机推荐
[ctfshow web] deserialization
Idea Alibaba cloud multi module deployment
What does it mean to lock financial products regularly? Can financial products be redeemed during the lock-in period?
Using MySQL master-slave replication delay to save erroneously deleted data
[Luogu cf643f] bears and juice (conclusion)
FIR filter design
About the difference between BigDecimal conversion string toengineeringstring, toplainstring and toString
The first case in Guangdong! A company in Guangzhou was punished by the police for failing to fulfill its obligation of data security protection
如何借助自动化工具落地DevOps|含低代码与DevOps应用实践
【飞控开发基础教程2】疯壳·开源编队无人机-定时器(LED 航情灯、指示灯闪烁)
How does win11 automatically clean the recycle bin?
Movable view component (it can be dragged up, down, left and right)
OA项目之我的会议(会议排座&送审)
Response object - response character data
Matlab论文插图绘制模板第40期—带偏移扇区的饼图
“青出于蓝胜于蓝”,为何藏宝计划(TPC)是持币生息最后的一朵白莲花
"Green is better than blue". Why is TPC the last white lotus to earn interest with money
mysql锁机制(举例说明)
Quickly learn to configure local and network sources of yum, and learn to use yum
[development tutorial 8] crazy shell · open source Bluetooth heart rate waterproof sports Bracelet - triaxial meter pace