当前位置: 首页 > 科技观察

useState的一切

时间:2023-03-12 23:00:02 科技观察

作为一个React开发者,你能回答下面两个问题吗:1、对于下面的函数组件:functionApp(){const[num,updateNum]=useState(0);window.updateNum=updateNum;returnnum;}调用window.updateNum(1)可以将视图中的0更新为1吗?2、对于以下函数组件:functionApp(){const[num,updateNum]=useState(0);functionincrement(){setTimeout(()=>{updateNum(num+1);},1000);}return{num}

;}1秒内快速点击p5次,view上显示了多少?👉向右滑动显示答案1.是2.显示为1其实这两个问题本质上是在问:useState是如何保存状态的?useState是如何更新状态的?本文将结合源码对以上两个问题进行说明。这些就是你需要了解的关于useState的全部内容。hook如何保存数据FunctionComponent本身的render只是一个函数调用。那么render内部调用的hook是如何获取到相应数据的呢?例如:useState获取状态useRef获取refuseMemo获取缓存数据答案是:每个组件都有对应的fiber节点(可以理解为虚拟DOM)来存储组件相关信息。FunctionComponent每次渲染时,全局变量currentlyRenderingFiber都会被赋值为FunctionComponent对应的fiber节点。因此,在hook内部,状态信息实际上是从currentlyRenderingFiber中获取的。多个钩子如何获取数据我们知道一个FunctionComponent中可能有多个钩子,例如:functionApp(){//hookAconst[a,updateA]=useState(0);//hookBconst[b,updateB]=useState(0);//hookCconstref=useRef(0);return

;}那么多个hooks如何获取自己的数据呢?答案是:currentlyRenderingFiber.memoizedState存储了一个hook对应数据的单向链表。对于上面的例子,可以理解为:consthookA={//hook保存的数据memoizedState:null,//指向下一个hooknext:hookB//...省略其他字段};hookB.next=hookC;currentlyRenderingFiber。memoizedState=hookA;FunctionComponent渲染时,每执行一个hook,currentlyRenderingFiber.memoizedState链表的指针就会后移一次,指向当前hook对应的数据。这就是为什么React要求hooks的调用顺序不能改变(hooks不能用在条件语句中)——每次执行render时,从一个固定顺序的链表中获取hooks对应的数据。useState执行过程我们知道useState返回值数组的第二个参数是改变状态的方法。在源码中,他叫做dispatchAction。每当调用dispatchAction时,都会创建一个表示更新的对象update:constupdate={//updateddataaction:action,//指向下一个更新next:null};对于以下示例functionApp(){const[num,updateNum]=useState(0);functionincrement(){updateNum(num+1);}return{num}

;}调用updateNum(num+1),willcreate:constupdate={//更新数据action:1,//指向下一次更新next:null//...省略其他字段};如果多次调用dispatchAction,例如:functionincrement(){//生成update1updateNum(num+1);//生成update2updateNum(num+2);//生成update3updateNum(num+3);}那么update就会形成循环链表。update3--next-->update1^||update2|______next_______|这个链表保存在哪里?既然这个更新链表是由一个useState的dispatchAction生成的,那么这个链表显然是属于useStatehook的。我们继续补充钩子的数据结构。consthook={//hook保存的数据memoizedState:null,//指向下一个hooknext:hookForB//本次更新根据baseState:null计算新的statebaseState,//本次更新开始时已有的更新队列baseQueue:null,//本次更新队列需要加入的更新队列:null,};其中,队列中保存本次更新的链表。在计算状态的时候,队列的环链表会被切割挂载到baseQueue的尾部,baseQueue会根据baseState计算出一个新的状态。计算状态后,新状态变为memoizedState。为什么更新不是基于memoizedState而是baseState是因为state的计算过程需要考虑优先级,有些更新可能没有足够的优先级被跳过。所以memoizedState不一定和baseState一样。更详细的解释见React技术揭秘[1]回到我们开头的第一个问题:functionApp(){const[num,updateNum]=useState(0);window.updateNum=updateNum;returnnum;}调用窗口。updateNum(1)视图中的0可以更新为1吗?这里需要看一下updateNum方法的具体实现:updateNum===dispatchAction.bind(null,currentlyRenderingFiber,queue);可以看出updateNum方法就是绑定了currentlyRenderingFiber和queue(即hook.queue)的dispatchAction。上面说了,调用dispatchAction的目的是为了产生一个update,插入到hook.queue链表中。由于队列已经作为预设参数绑定到dispatchAction,所以调用dispatchAction仅限于FunctionComponent内部。第二题update动作functionApp(){const[num,updateNum]=useState(0);functionincrement(){setTimeout(()=>{updateNum(num+1);},1000);}return{num}

;}1秒内快速点击p5次,view上显示了多少?我们知道调用updateNum会产生update,传参就会变成update.action。1秒内点击5次。第五次点击时,第一次点击创建的update还没有进入update流程,所以hook.baseState还没有变化。那么这5次点击产生的updates都是基于同一个baseState来计算新的state,num变量没有变化(即5次update.actions(即num+1)的值相同)。那么,最终渲染出来的结果就是1.useState和useReducer那么,如何在5次点击中让视图从1逐渐变为5呢?从上面的知识,我们知道需要改变baseState或者action。baseState由React的更新过程决定,我们无法控制。但是我们可以控制动作。Actions不仅可以传递值,还可以传递函数。//action是值updateNum(num+1);//action是函数updateNum(num=>num+1);在根据baseState和updatelist生成新状态的过程中:letnewState=baseState;letfirstUpdate=hook.baseQueue。next;letupdate=firstUpdate;//遍历baseQueue中的每一次更新do{if(typeofupdate.action==='function'){newState=update.action(newState);}else{newState=action;}}while(update!==firstUpdate)可以看出,在传值的时候,由于我们5个action是同一个值,所以最终计算出来的newState也是同一个值。传递一个函数时,newState会根据action函数计算5次,最后得到累加的结果。如果我们在这个例子中使用useReducer而不是useState,因为useReducer的动作总是一个函数,所以我们不会在我们的例子中遇到问题。事实上,useState本身就是一个useReducer,预设了以下reducer。functionbasicStateReducer(state,action){returntypeofaction==='function'?action(state):action;}总结通过这篇文章,我们了解了useState的完整执行流程。