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

ReactHooks的实现一定要依赖Fiber吗?

时间:2023-03-13 03:49:34 科技观察

React的hooks是fiber之后出现的特性,所以很多人误以为hooks只能依赖fiber来实现。其实不然,两者没有必然联系。现在,hooks不仅在react中实现,在preact、reactssr、midway等框架中也有实现,而且它们的实现不依赖于fiber。下面我们来看看这些不同框架中的hooks是如何实现的:React是如何实现hooksReact是通过jsx来描述接口的,会被babel或者tsc等编译工具编译成render函数,然后执行生成vdom:render这里函数在React17之前是React.createElement:在React17之后更改为jsx:这个jsx-runtime将被自动引入,而不是像以前那样为每个组件保留一个React导入。执行render函数生成vdom:vdom的结构如下:在React16之前,这个vdom会递归渲染,增删改查真正的dom。React16引入fiber架构后,多了一步:先将vdom转成fiber,再渲染fiber。将vdom转换为fiber的过程称为reconcile,对realdom进行增删改查的过程称为commit。为什么需要进行这种转换?因为vdom只有子节点children的引用,没有父节点parent和其他兄弟节点siblings的引用,导致一次递归渲染所有vdom节点到dom,无法中断。如果中断了怎么办?因为没有记录父节点和兄弟节点,所以只能继续处理子节点,不能处理vdom的其他部分。所以React才引入了这种fiber结构,也就是有父节点return,子节点child,兄弟节点sibling等引用,可以打断,因为后面所有没有处理完的节点都可以找到在被打破和恢复之后。fiber节点的结构如下:这个过程可以中断,自然可以调度,也就是schdule的过程。因此,fiber架构分为三个阶段:schdule、reconcile(vdomtofiber)、commit(updatetodom)。在函数组件中可以使用hooks来访问一些值,这些值存储在fiber节点上。比如这个功能组件中使用了6个hooks:那么对应的fiber节点上有一个6个元素的memorizedState链表:通过next串联:不同的hooks在memorizedState链表的不同元素上访问值,这个是reacthooks原理。这个链表有创建阶段和更新阶段,所以你会发现useXxx的最终实现分为mountXxx和updateXxx:这里的mount阶段是创建hook节点,组装成链表:createdhooklinkedlist将链接到memorizedState属性上的fiber节点。更新的时候,自然可以从fiber节点中取出hook列表:这样在多次渲染时,useXxxAPI就可以在fiber节点上找到对应的memorizedState。这就是reacthooks的原理。你可以看到它存储了纤程节点上的钩子。那么preact有什么区别呢?preact是如何实现钩子的?Preact是一个与React代码兼容的轻量级框架。支持类组件和函数组件,也支持钩子等React特性。它虽然没有实现光纤架构。因为它主要考虑的是体积上的极致(只有3kb),而不是性能上的极致。刚才我们了解到react将hook链表存储在fiber节点上,那么preact将没有fiber节点的hook链表存储在哪里呢?其实很容易认为fiber只是为了提高性能修改了vdom。和vdom没有本质区别,把hook存放在vdom上就可以了?事实上,preact只是将hook列表放在vdom上。比如这个有4个hooks的功能组件:它的实现是访问vdom上相应的hooks:它并没有像react一样把??hook分成mount和update两个阶段,而是合并在一起处理。如果是这样,它将钩子存储在component.__hooks数组中并通过下标访问它们。这个component是vdom上的一个属性:即hooks的值存放在vnode._component._hooks数组中。比较react和preact实现hook的区别:react中hook列表保存在fiberNode.memorizedState属性中,preact中hook列表保存在vnode._component._hooks属性中。react中的hook链表是通过next进行拼接的,preact中的hook链表是一个数组,通过下标访问。React将hook链表的创建和更新分开,即useXxx会分成mountXxx和updateXxx来实现,而preact会将它们合并在一起进行处理。因此,hooks的实现并不依赖于fiber。它只是找个地方存放组件对应的钩子的数据。渲染的时候拿到就可以了,存放在哪都无所谓。因为vdom、fiber和componentrendering是强相关的,所以都存放在这些结构体中。像reactssr实现了hooks,在fiber或者vdom上都不存在:Howtoimplementhooksinreactssr其实react-dom包不仅可以作为csr,还可以作为ssr:csr的时候,使用react-dom的render方法:在ssr中使用react-dom/server的renderToString方法或者renderToStream方法:你觉得vdom到fiber的转换会在ssr中完成吗?当然不是,fiber是为了提高在浏览器中运行时的渲染性能。计算变得可中断,它是一种只有在空闲时间进行计算时才引入的结构。服务端渲染自然不需要纤程。如果不需要fiber,hooklist存放在哪里?是vdom吗?确实可以放在vdom里面,但是不可以。比如useRefhooks:是一个从firstWorkInProgressHook开始与next相连的链表。而firstWorkInProgressHook用createHook创建的第一个hook节点:并没有挂载到vdom上。为什么?因为ssr只需要渲染一次,不需要更新,所以没必要挂在vdom上。只要处理完各个组件的hook,hook列表就清空了:所以,在reactssr的时候,hook是存放在全局变量中的。对比一下reactcsr和ssr中hooks的实现原理的区别:csr会从vdom中创建fibers使渲染可中断,通过空闲调度提高性能,而ssr不会,是vdom直接渲染的。csr时,hooks保存在fiber节点上,ssr时,直接放在全局变量上,对每个组件进行处理。因为不会用第二次。csr会将hook的创建和更新分为mount和update两个阶段,而ssr只会处理一次,只有创建阶段。hooks的实现原理其实并不复杂。就是将一个链表存储在某个上下文中,然后hooksapi从链表的不同元素中访问相应的数据,完成各自的逻辑。这个上下文可以是vdom、fiber甚至是全局变量。然而,hooks的想法仍然很流行。淘宝的服务端框架midway介绍了hooks的思想:midway是如何实现hooks的。Midway是一个Node.js框架:服务端框架自然没有vdom、fiber这样的结构,但是hooks的思想并不依赖于这些。实现hooks的API,只需要在一定的上下文中放入一个链表即可。Midway实现了一个类似于reacthooks的API:hooklist在哪里,我还没有看到,但是我们已经掌握了hooks的实现原理。只要有存储钩子列表的上下文,它就可以在任何地方。总结一下,reacthooks是在reactfiber架构之后出现的特性。很多人误以为hooks必须用fiber来实现。我们查看了react、preact、reactssr和midway中hooks的实现,发现并不是这样的:react是将vdom转换成fiber,然后将hook列表存储在fiber.memorizedState属性上。Preact不会通过下一个系列实现纤程。它将挂钩列表放在vnode._component._hooks属性上,并实现数组。通过下标访问reactfiberssr不需要,但是hook列表没有挂在vdom上,而是直接放在一个全局变量上,因为只需要渲染一次,渲染一个组件后全局变量就清空了。midway是一个Node.jsFramework,它也实现了类似hooks的API。至于具体放在哪里,我们没有细说,只要有上下文可以存放钩子列表就可以了。那么,reacthooks是不是一定要依赖fiber来实现呢?显然不是,对于fiber、vdom、全局变量,甚至任何上下文。在框架中引入钩子的API并不难。