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

React Hooks 的原理,有的简单有的不简单

时间:2023-03-13 23:14:39 科技观察

ReactHooks的一些原理很简单,有些则不然。转载本文请联系神光编程秘籍公众号。React是一个实现组件的前端框架,它同时支持类组件和函数组件。类组件通过继承模板类(Component、PureComponent)来开发新的组件。这是类本身的一个特点。支持设置状态,状态改变后会重新渲染。父类的一些方法可以重写。这些方法会在React组件渲染的不同阶段被调用,称为生命周期函数。函数组件不能被继承,因为函数没有这个特性,所以提供了一些API给函数使用。这些API会将一些函数和值挂载到一个内部数据结构上,并执行相应的逻辑。通过这个方法实现了类似于类组件的状态和生命周期函数的功能。这种API称为钩子。钩子挂载数据的数据结构称为纤程。那么什么是纤维?我们知道React使用jsx来描述界面结构,将jsx编译成render函数,然后执行render函数生成vdom:在Reactv16之前,直接递归遍历vdom,通过domapi进行增删改查渲染修改dom。但是当vdom过大时,频繁调用domapi会很耗时,而且递归不能中断,所以存在性能问题。后来引入了fiber架构,先把vdom树转成fiber链表,再渲染fiber。vdom到fiber的转换过程称为reconcile,可以中断。React添加了一个调度机制来在空闲时调度协调。reconcile时会做diff,标记增删改查(effectTag),并创建对应的dom。然后就可以一次性把fiber渲染到dom上,也就是commit。这个调度、协调和提交过程就是纤程架构。当然对应的数据结构也叫纤程。Hooks是通过挂载数据到组件对应的fiber节点来实现的。fiber节点是一个对象,hooks把数据挂载到哪个属性上?我们可以用调试器检查一下。准备这样一个函数组件(代码没有具体含义,只是为了调试hook):functionApp(){const[name,setName]=useState("guang");useState('东');consthandler=useCallback((evt)=>{setName('dong');},[1]);useEffect(()=>{console.log(1);});使用参考(1);useMemo(()=>{return'guanganddong';})return({name}

);}在函数中打个断点,运行到这个组件就停止.我们看一下调用栈:最后一个函数是renderWithHooks,里面有个workingInProgress对象就是当前fiber节点:fiber节点的memorizedState就是保存hooks数据的地方。是一个通过next串联起来的链表。我们展开看看:链表一共有六个元素。这不就和我们在函数组件中写的钩子一样吗?这是挂钩访问数据的地方。执行时,各自在自己的memorizedState上存取数据,完成各种逻辑。这就是钩子的原理。这个memorizedState链表是什么时候创建的?问得好,确实有创建链表的过程,就是mountXxx。链表只需要创建一次,以后只需要更新。所以第一次调用useState会执行mountState,后续调用useState会执行updateState。让我们先着重了解mount。mountXxx是创建memorizedState链表的过程。每个hooksapi是这样的:它的实现也很容易想到,就是创建对应的memorizedState对象,然后用next进行拼接,就是这段代码:当然,创建这样的数据结构还是用来。每个hooksAPI使用这些memorizedState数据的逻辑都不一样,有的比较简单,比如useRef、useCallback、useMemo,有的就不那么简单,比如useState、useEffect。你为什么这么说?让我们看看它们的实现。先看这几个简单的:useRef的每个useXxxhooks都有mountXxx和updateXxx两个phase,比如ref就是mountRef和updateRef。它的代码最简单,只有几行代码:mountWorkInProgressHook,我们刚才看到的,就是创建并返回memorizedState链表。同样,更新下面的updateWorkInProgressHook。不用管这些,只要知道对应的memorizedState链表中的元素被修改即可。挂在memorizedState上的ref是什么?可以看出,传入的值是用一个具有current属性的对象包裹起来的,冻结一段时间,然后放到memorizedState属性上。后面更新的时候,不做任何处理,直接返回这个对象。因此,useRef的作用很容易猜到:useRef可以保存对数据的引用,它是不可变的。这个钩子是最简单的钩子。给我们一个存储数据的地方,我们就可以轻松实现useRefhook。再看一个稍微难点的:useCallbackuseCallback在memorizedState上放一个数组,第一个元素是传入的回调函数,第二个是传入的deps(deps按undefined处理)。update的时候,把之前的memorizedState取出来,和新传过来的deps进行比较。如果没有变化,则返回上一个回调函数,即prevState[0]。如果更改,则创建一个新数组,第一个元素是传入的回调函数,第二个元素是传入的deps。于是,useCallback的功能呼之欲出:useCallback可以实现函数的缓存,如果deps没有变化,则不会新建,否则返回新传过来的函数。这个逻辑其实不难,只是多了一个判断逻辑。我们看一个类似的东西:useMemouseMemo也在memorizedState上放了一个数组,第一个元素是传入函数的执行结果,第二个元素是deps(处理deps为undefined的情况)。update的时候也会把之前的memorizedState取出来和新传过来的deps进行比较。如果没有变化,则返回之前的值,即prevState[0]。如果改变了,在memorizedState中新建一个数组,第一个元素是新传入函数的执行结果,第二个元素是deps。所以大家可以猜到useMemo的作用:useMemo可以实现函数执行结果的缓存。如果deps没有变化,则直接取之前的,否则执行函数获取最新的结果并返回。实现逻辑与useCallback类似。这三个钩子难吗?给大家一个存储数据的对象并不难,大家都可以写。因为它们没有其他依赖性,所以它们只是缓存下一个值。useState和useEffect之类的比较复杂,主要是需要调度。useStatestate改变后,需要触发一个更新的schedule。React有自己的调度逻辑,也就是我们前面提到的fiberschedule,所以需要调度一个action。(不展开,简单看一下)这里不展开涉及调度的细节。和useEffect一样,effect传入的函数也是React调度的。当然这里的调度不是fiber调度,而是一个单独的effect调度:(不展开,随便看看)hooks负责把这些effect连接成一个updateQueue的链表,然后让React调度执行.因此,useState、useEffect等hook的实现与fiber的空闲调度和effect的调度紧密结合,实现较为复杂。这里不讨论,因为这篇文章的目的是澄清钩子的主要原理,而不是太过详细。您可能听说过自定义钩子的概念,那是什么?其实就是一个函数调用,没什么神奇的,我们可以把上面的钩子放到xxx函数中,然后在函数组件中调用,对应的钩子链表也是一样的。只是我们平时使用的是React提供的eslint插件。lint这些功能要从use开始,如果不需要也没关系。它们与普通的函数封装没有区别。综上所述,React同时支持类组件和函数组件。Class支持状态属性和生命周期方法,功能组件也通过hooksapi实现类似的功能。Fiber架构是React在16之后引入的,以前是jsx->renderfunction->vdom然后直接递归渲染vdom。现在将vdom转换为fiberreconcile是一个额外的步骤。在reconcile的过程中,创建dom并做diff并标记增删改effectTag,然后一次性commit。这个rec??oncile是可以中断和调度的,就是fiber的调度。hooks的实现是基于fiber的,fiber节点上会放一个链表。每个节点的memorizedState属性存储了对应的数据,然后不同的hooksAPI使用对应的数据完成不同的功能。链表自然有一个创建阶段,就是mountXxx,之后就不用挂载了,只需要update。所以每一个useXx的实现其实分为两部分,mountXxx和updateXxx。我们看过几个简单的hook:useRef、useCallback、useMemo,它们只是缓存值,逻辑比较纯粹,不依赖React的调度。虽然useState会触发纤程调度,但useEffect也有自己的调度逻辑。实现比较复杂,我们也没有深入。其实很简单,给我们一个访问数据的对象,实现useRef、useCallback、useMemo等hook,对于那些需要调度的,就比较复杂了。对于自定义钩子,那只是一个函数调用,没有任何区别。(如果不想遵循lint的规则,可以忽略。)所有的hooksAPI都是基于fiber节点上的memorizedState列表来访问数据,完成各自的逻辑。那么,hooks的原理简单吗?只能说有的简单有的不简单。