前言Hooks是React中比较流行的一个概念,用过的朋友都说不错。但是,在使用Hooks的时候,我们可能会有很多疑惑:为什么useEffect的第二个参数是一个空数组,相当于ComponentDidMount只会执行一次?为什么Hook只能在函数的最外层调用,在循环、条件判断、子函数中不能调用?一个自定义的Hook是如何影响使用它的函数组件的?捕获价值功能是如何产生的?...今天的文章不会讲解Hooks的概念和用法,而是带你从零开始实现一个小小的hooks,并知其所以然。useState最简单的用法是:functionCounter(){var[count,setCount]=useState(0);return(
{count}
{setCount(count+1);}}>点击);}demo1:https://codesandbox.io/s/v0nq...基于useState的用法,我们尝试自己实现一个useState实现一个useState:functionuseState(initialValue){varstate=initialValue;函数setState(newState){state=newState;使成为();}return[state,setState];}demo2:https://codesandbox.io/s/myy5...这时候我们发现点击Button的时候,count没有变化。为什么?我们不存储状态,每次渲染Counter组件时,都会重置状态。自然地,我们可以想到将状态提取出来,存储在useState之外。demo3:https://codesandbox.io/s/q9wq...var_state;//将状态存储到外部functionuseState(initialValue){_state=_state||初始值;//如果没有_state,表示先Execute一次,复制initialValue给它functionsetState(newState){_state=newState;使成为();}return[_state,setState];}到目前为止,我们已经实现了一个可用的useState,至少现在它没有问题。接下来我们看看useEffect是如何实现的。useEffectuseEffect是另一个基本的Hook,用来处理副作用,最简单的用法是这样的:demo4:https://codesandbox.io/s/93jp...useEffect(()=>{console.log(count);},[数数]);我们知道useEffect有几个特点:有两个参数callback和dependencies数组,如果dependencies不存在,每次render都会执行callback,如果dependencies存在,只有当它发生变化时,callback才会执行,实现一个useEffectdemo5:https://codesandbox.io/s/3kv3...让_deps;//_deps记录了useEffectfunctionuseEffect(callback,depArray)最后的依赖关系{consthasNoDeps=!depArray;//如果依赖不存在consthasChangedDeps=_deps?!depArray.every((el,i)=>el===_deps[i])//两次的依赖是否完全相等:true;/*如果依赖不存在,或者依赖已经改变*/if(hasNoDeps||hasChangedDeps){callback();_deps=depArray;}}至此,我们已经实现了一个可以工作的useEffect,看起来并不那么困难。至此我们应该可以回答一个问题了:为什么第二个参数是空数组,相当于componentDidMount?因为依赖没有改变,回调不会执行两次。不是Magic,只是Arrays到目前为止,我们已经实现了useState和useEffect的工作。但是有一个很大的问题:它们都只能使用一次,因为只有一个_state和一个_deps。例如,const[count,setCount]=useState(0);const[用户名,setUsername]=useState('fan');count和username总是相等的,因为它们共享一个_state,没有地方可以存储两个值。我们需要能够存储多个_states和_deps。正如《React Hooks: not magic, just arrays》所写,我们可以使用数组来解决Hooks的复用问题。demo6:https://codesandbox.io/s/50ww...代码关键在于:第一次渲染时,按照useState和useEffect的先后顺序,stuffstate,deps等到memoizedState数组中为了。更新时,依次从memoizedState中取出最后记录的值。如果你还不确定,你可以看下图。让memoizedState=[];//钩子存储在这个数组中letcursor=0;//当前memoizedState下标functionuseState(initialValue){memoizedState[cursor]=memoizedState[cursor]||初始值;constcurrentCursor=cursor;函数setState(newState){memoizedState[currentCursor]=newState;使成为();}返回[memoizedState[cursor++],setState];//返回当前状态,并将光标加1}functionuseEffect(callback,depArray){consthasNoDeps=!depArray;constdeps=memoizedState[光标];consthasChangedDeps=deps?!depArray.every((el,i)=>el===deps[i]):true;如果(hasNoDeps||hasChangedDeps){回调();memoizedState[cursor]=depArray;}cursor++;}我们用图来描述改变memoizedState和cursor的过程。1.初始化2.初始渲染3.事件触发4.重新渲染在这里,我们实现了一个可以任意复用的useState和useEffect。同时也可以回答几个问题:Q:为什么Hook只能在函数的最外层调用?为什么不在循环、条件判断或者子函数中调用呢?A:memoizedState数组按照钩子定义的顺序放置数据。如果hooks的顺序发生变化,memoizedState是不会感知到的。Q:自定义Hook对使用它的函数组件有什么影响?A:共享同一个memoizedState,共享同一个订单。Q:“捕捉价值”功能是怎么来的?A:每次ReRender都会重新执行函数组件,不会对之前执行过的函数组件进行任何操作。真正的React实现虽然我们已经基本用数组实现了一个可用的Hooks,也了解了Hooks的原理,但是在React中,实现的方式还是有些区别的。在React中,数组被替换为类似单向链表的形式。源码地址:https://github.com/facebook/react/blob/5f06576f51ece88d846d01abd2ddd575827c6127/packages/react-reconciler/src/ReactFiberHooks.js#L336通过next依次连接所有hooks。https://github.com/facebook/react/blob/5f06576f51ece88d846d01abd2ddd575827c6127/packages/react-reconciler/src/ReactFiberHooks.js#L477typeHooks={memoizedState:any,//指向当前渲染节点FiberbaseState:any,//初始化initialState,每次调度newStatebaseUpdate:Update
|null,//当前需要更新的Update,每次更新后,都会为之前的更新赋值,使得React濒临渲染错误,数据回溯到队列:UpdateQueue|null,//UpdateQueue通过next:Hook|null,//链接到下一个钩子,通过next串联每个钩子}typeEffect={tag:HookEffectTag,//effectTag标记当前钩子作用于生命周期创建的哪个阶段:()=>mixed,//初始化回调destroy:(()=>mixed)|null,//卸载回调deps:Array|null,next:Effect,//同上};MemoizedState,游标存在于何处?如何与各个功能组件一一对应?我们知道react会生成一个组件树(或者Fiber单链表),树中的每个节点对应一个组件,hooks的数据作为组件的一条信息存储在这些节点上,born和die与组件一起。结束语以上我们了解了Hooks的工作原理和简单实现。Hooks的其他一些细节可以看我的另一篇文章,希望能给你一些启发。参考文章https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/https://medium.com/@ryardley/react-hooks-不是魔术只是数组-cd4f1857236e