一个关于React数据不可变的无聊问题

news/2025/2/23 5:05:05/

对于一个React的开发者来说不知道你有没有想过为什么React追求数据不可变这个范式;

一个月前我想过一个问题如果我在使用useState这个hooks的时候传入的是一个改变后的引用类型对象会发生什么?

例如:

import {useState} from "react"function App() {const [list,setList] = useState([0,1,2])const handleClick = ()=>{list.push(list.length)setList(list)}return (<div className="App"><button onClick={handleClick}>click me--conventionality</button>{list.map(item=><div key={item}>{item}</div>)}</div>);
}export default App; 

然后当我们点击按钮的时候会发生什么呢?答案是从我们的视觉感官来讲什么也没有发生!列表数据一直是012; 关于这个结果我相信百分之99的react开发者都是可以预料的!也肯定有百分之80以上的人会说因为你的新数据和老数据是同一个(newState===oldState)===true在这个问题上答案也确实是这个一个。那么newState与oldState是在哪里做的比较,又是在哪里做的拦截呢?我之前想的是会在render阶段update时的reconcileChildFibers中打上effectTag标记判断前做的判断,然而当我今天在给beginWork后我发现以上这个代码压根走不到beginWork (mount阶段),带着好奇我决定从源码出发去探索一下(答案可能会有点无聊);

我们知道useState这个hooks生成

const [list,setList] = useState([0,1,2]) 

dispatchAction这个方法

mountState阶段

而useState分为两种mountStateupdateState,因为setList是在mount时被创建的所以我们先去查看他是如何被创建的

function mountState(initialState) {var hook = mountWorkInProgressHook();if (typeof initialState === 'function') {// $FlowFixMe: Flow doesn't like mixed typesinitialState = initialState();}hook.memoizedState = hook.baseState = initialState;var queue = {pending: null,interleaved: null,lanes: NoLanes,dispatch: null,lastRenderedReducer: basicStateReducer,lastRenderedState: initialState};hook.queue = queue;//创建dispatch方法并保存到链式当中//dispatch是通过dispatchSetState这个方法创建的var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);//这一步return出链式当中的list与setListreturn [hook.memoizedState, dispatch];
} 

dispatch是通过dispatchSetState这个方法创建的,然后我们去dispatchSetState中去查看

function dispatchSetState(fiber, queue, action) {//此处打上console,可以正常输出,程序可以进行到此步console.log('dispatchSetState',fiber,queue,action){if (typeof arguments[3] === 'function') {error("State updates from the useState() and useReducer() Hooks don't support the " + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().');}}var lane = requestUpdateLane(fiber);var update = {lane: lane,action: action,hasEagerState: false,eagerState: null,next: null};//首屏更新走这里console.log(currentlyRenderingFiber$1===null)console.log(fiber.alternate===null)//trueif (isRenderPhaseUpdate(fiber)) {enqueueRenderPhaseUpdate(queue, update);} else {enqueueUpdate$1(fiber, queue, update);var alternate = fiber.alternate;//是否是首次更新判断(mount之后还未进入update)if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {// The queue is currently empty, which means we can eagerly compute the// next state before entering the render phase. If the new state is the// same as the current state, we may be able to bail out entirely.var lastRenderedReducer = queue.lastRenderedReducer;if (lastRenderedReducer !== null) {var prevDispatcher;{prevDispatcher = ReactCurrentDispatcher$1.current;ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;}try {//在这一步我们可以看到传入的值是已经改变的的//当前传入state(保存在链中)var currentState = queue.lastRenderedState;//第一次 [0,1,2,3]//state计算数据var eagerState = lastRenderedReducer(currentState, action); //第一次 [0,1,2,3]// Stash the eagerly computed state, and the reducer used to compute// it, on the update object. If the reducer hasn't changed by the// time we enter the render phase, then the eager state can be used// without calling the reducer again.update.hasEagerState = true;update.eagerState = eagerState;//判断newState与oldState做比较,第一次点击在这里终止if (objectIs(eagerState, currentState)) {// Fast path. We can bail out without scheduling React to re-render.// It's still possible that we'll need to rebase this update later,// if the component re-renders for a different reason and by that// time the reducer has changed.// console.log(222222,queue)return;}} catch (error) {// Suppress the error. It will throw again in the render phase.} finally {{ReactCurrentDispatcher$1.current = prevDispatcher;}}}}var eventTime = requestEventTime();var root = scheduleUpdateOnFiber(fiber, lane, eventTime);console.log('root',root)if (root !== null) {entangleTransitionUpdate(root, queue, lane);}}markUpdateInDevTools(fiber, lane);
} 

我们通过调试可以看到因为已经经过首屏更新所以走的是else内的部分,最终在else内进行当前值与计算值比较因为是同一个引用类型对象所以返回的是true

//判断newState与oldState做比较,第一次点击在这里终止
if (objectIs(eagerState, currentState)) {// Fast path. We can bail out without scheduling React to re-render.// It's still possible that we'll need to rebase this update later,// if the component re-renders for a different reason and by that// time the reducer has changed.// console.log(222222,queue)return;
} 

数据比较

function is(x, y) {return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y // eslint-disable-line no-self-compare;
}var objectIs = typeof Object.is === 'function' ? Object.is : is; 

最终mount阶段在dispatchSetState方法中就被拦截了,那么在update阶段又会怎么样呢?带着好奇我改写了一下demo

updateState

function App() {const [list,setList] = useState([0,1,2])//const handleClick = ()=>{list.push(3)setList(list)}const handleClick2 = ()=>{setList([...list,list.length])}return (<div className="App"><button onClick={handleClick}>click 1</button><button onClick={handleClick2}>click 2</button>{list.map(item=><div key={item}>{item}</div>)}</div>);
} 

我们先点击click2使其进入update状态,然后再点击click1,你会发现它进入了beginWork方法因为是Function组件,所以会在updateFunctionComponent 中执行,但是这这一步它停止了;原因是它在这里判断进入了bailoutOnAlreadyFinishedWork

//在这里进入bailoutOnAlreadyFinishedWork
//bailoutOnAlreadyFinishedWork 判断节点是否可复用
//当前为update阶段所以current不可能为空
//!didReceiveUpdate代表为update阶段
if (current !== null && !didReceiveUpdate) {bailoutHooks(current, workInProgress, renderLanes);console.log('bailoutOnAlreadyFinishedWork')return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} 

然后再让我们看看bailoutOnAlreadyFinishedWork 方法

function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {if (current !== null) {// Reuse previous dependenciesworkInProgress.dependencies = current.dependencies;}{// Don't update "base" render times for bailouts.stopProfilerTimerIfRunning();}markSkippedUpdateLanes(workInProgress.lanes); // Check if the children have any pending work.console.log(renderLanes, workInProgress.childLanes)if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {console.log("stop")// The children don't have any work either. We can skip them.// TODO: Once we add back resuming, we should check if the children are// a work-in-progress set. If so, we need to transfer their effects.{return null;}} // This fiber doesn't have work, but its subtree does. Clone the child// fibers and continue. 

最终本次render阶段会在这里被强制中断

//判断子节点有无需要进行的任务操作
//在这里停止原因是workInProgress.childLanes为0导致等式成立
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {console.log("stop")// The children don't have any work either. We can skip them.// TODO: Once we add back resuming, we should check if the children are// a work-in-progress set. If so, we need to transfer their effects.{return null;}
} // This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue. 

总结

不管是在mountState阶段可变数据会在dispatchSetState时就会因为数据比对而中断,因此进入不到beginWork,在updateState阶段,可变数据会进入beginWork并根据Fibertag类型判断进入的是updateFunctionComponent还是updateClassComponent但是最终都会在bailoutOnAlreadyFinishedWork函数中因为childLanes为0的缘故终止执行;也就是说在mountState阶段不会进入render阶段,但是在updateState阶段会进入render阶段并创建fiber,但是会被中断执行

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享


http://www.ppmy.cn/news/3307.html

相关文章

Python3.1 使用卡通头像网络模型生成卡通头像(基于GAN)

使用GAN网络生成卡通头像 提示&#xff1a;当前生成方法仅为实验场景 参考地址 使用GAN网络模型生成卡通头像使用GAN网络生成卡通头像前言一、下载模型二、编写生成卡通头像Python代码1.引入库2.加载模型3.生成卡通头像方法总结前言 随着人工智能的不断发展&#xff0c;机器学…

大数据面试之Spark基础常见题目

大数据面试之Spark基础常见题目 1.1 Spark解决什么问题 ​ Hadoop主要解决海量数据的存储和海量数据的计算。 ​ Spark主要解决海量数据的计算。存储还是交给HDFS。 1.2 Spark为什么会有自己的资源调度器 ​ Hadoop的YARN框架比Spark框架诞生的晚&#xff0c;因为YARN是在…

简洁而优美的结构 - 并查集 | 一文吃透 “带权并查集” 不同应用场景 | “手撕” 蓝桥杯A组J题 - 推导部分和

&#x1f49b;前情提要&#x1f49b; 本章节是每日一算法的并查集&带权并查集的相关知识~ 接下来我们即将进入一个全新的空间&#xff0c;对代码有一个全新的视角~ 以下的内容一定会让你对数据结构与算法有一个颠覆性的认识哦&#xff01;&#xff01;&#xff01; ❗以…

算法拾遗二十二之Dijstra算法优化+认识一些经典的递归过程

Dijstra算法优化 public static class NodeRecord {public Node node;public int distance;public NodeRecord(Node node, int distance) {this.node node;this.distance distance;}}public static class NodeHeap {private Node[] nodes; // 实际的堆结构// key 某一个node&…

Linux查看进程端口号的几种方法

1、lsof 命令 lsof 是 linux 下的一个非常实用的系统级的监控、诊断工具。它的意思是 List Open Files&#xff0c;它可以用来列出被各种进程打开的文件信息&#xff0c;linux 下 “一切皆文件”。 这个命令可能系统里没有自带&#xff0c;需要先安装一下&#xff0c;CentOS系…

C函数之memcpy( )函数用法

1 基本介绍 1.1 函数原型 void *memcpy(void*dest, const void *src, size_t n);函数原型 void *memcpy(void *dest, const void *src, int count) {void *ptr dest;if (NULL dest || NULL src || count < 0){return NULL;}while (count--)*(char *)dest *(char *)src…

C#语言实例源码系列-实现自己的进程管理器

专栏分享点击跳转>Unity3D特效百例点击跳转>案例项目实战源码点击跳转>游戏脚本-辅助自动化点击跳转>Android控件全解手册 &#x1f449;关于作者 众所周知&#xff0c;人生是一个漫长的流程&#xff0c;不断克服困难&#xff0c;不断反思前进的过程。在这个过程中…

[附源码]Nodejs计算机毕业设计基于JAVA语言的宠物寄养管理Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…