React的hooks是在fiber之后出现的特性,所以很多人误以为hooks是必须依赖fiber才能实现的,其实并不是,它们俩没啥必然联系。 现在,不止react中实现了hooks,在preact、reactssr、midway等框架中也实现了这个特性,它们的实现就是不依赖fiber的。 我们分别来看一下这些不同框架中的hooks都是怎么实现的: react如何实现hooks react是通过jsx描述界面的,它会被babel或tsc等编译工具编译成renderfunction,然后执行产生vdom: 图片 这里的renderfunction在React17之前是React。createElement: 图片 在React17之后换成了jsx: 图片 这个jsxruntime会自动引入,不用像之前那样每个组件都要保留一个React的import才行。 renderfunction执行产生vdom: 图片 vdom的结构是这样的: 图片 在React16之前,会递归渲染这个vdom,增删改真实dom。 图片 而在React16引入了fiber架构之后就多了一步:首先把vdom转成fiber,之后再渲染fiber。 图片 vdom转fiber的过程叫做reconcile,最后增删改真实dom的过程叫做commit。 为什么要做这样的转换呢? 因为vdom只有子节点children的引用,没有父节点parent和其他兄弟节点sibling的引用,这导致了要一次性递归把所有vdom节点渲染到dom才行,不可打断。 万一打断了会怎么样呢?因为没有记录父节点和兄弟节点,那只能继续处理子节点,却不能处理vdom的其他部分了。 所以React才引入了这种fiber的结构,也就是有父节点return、子节点child、兄弟节点sibling等引用,可以打断,因为断了再恢复也能找到后面所有没处理过的节点。 fiber节点的结构是这样的: 图片 这个过程可以打断,自然也就可以调度,也就是schdule的过程。 所以fiber架构就分为了schdule、reconcile(vdom转fiber)、commit(更新到dom)三个阶段。 函数组件内可以用hooks来存取一些值,这些值就是存在fiber节点上的。 比如这个函数组件内用到了6个hook: 图片 那么对应的fiber节点上就有个6个元素的memorizedState链表: 图片 通过next串联起来: 图片 不同的hook在memorizedState链表不同的元素上存取值,这就是reacthooks的原理。 这个链表有创建阶段和更新阶段,所以你会发现useXxx的最终实现都分为了mountXxx和updateXxx: 图片 这里的mount阶段就是创建hook节点并组装成链表的: 图片 会把创建好的hook链表挂到fiber节点的memorizedState属性上。 那更新的时候自然也就能从fiber节点上取出这个hook链表: 图片 这样在多次渲染中,useXxx的api都能在fiber节点上找到对应的memorizedState。 这就是reacthooks的原理,可以看到它是把hook存在fiber节点上的。 那preact有什么不同呢? preact如何实现hooks preact是兼容react代码的更轻量级的框架,它支持class组件和function组件,也支持了hooks等react特性。不过它没有实现fiber架构。 因为它主要考虑的是体积的极致(只有3kb),而不是性能的极致。 图片 刚才我们了解了react是把hook链表存放在fiber节点上的,那preact没有fiber节点,会把hook链表存在哪呢? 其实也很容易想到,fiber只是对vdom做了下改造用于提升性能的,和vdom没啥本质的区别,那就把hook存在vdom上不就行了? 确实,preact就是把hook链表放在了vdom上。 比如这个有4个hooks的函数组件: 图片 它的实现就是在vdom上存取对应的hook: 图片 图片 它没有像react那样把hook分为mount和update两个阶段,而是合并到一起处理了。 如图,它把hooks存在了component。hooks的数组上,通过下标访问。 这个component就是vdom上的一个属性: 图片 也就是把hooks的值存在了vnode。component。hooks的数组上。 对比下react和preact实现hooks的差异: react中是把hook链表存放在fiberNode。memorizedState属性上,preact中是把hook链表存放在vnode。component。hooks属性上 react中的hook链表通过next串联,preact中的hook链表就是个数组,通过下标访问 react把hook链表的创建和更新分离开,也就是useXxx会分为mountXxx和updateXxx来实现,而preact中合并在一起处理的 所以说,hooks的实现并不依赖fiber,它只不过是找个地方存放组件对应的hook的数据,渲染时能取到就行,存放在哪里是无所谓的。 因为vdom、fiber和组件渲染强相关,所以存放在了这些结构上。 像reactssr实现hooks,就既没有存在fiber上,也没有存在vdom上: reactssr如何实现hooks 其实reactdom包除了可以做csr外,也可以做ssr: csr时使用reactdom的render方法: 图片 ssr的时候使用reactdomserver的renderToString方法或renderToStream方法: 图片 大家觉得ssr的时候会做vdom到fiber的转换么? 肯定不会呀,fiber是为了提高在浏览器中运行时的渲染性能,把计算变成可打断的,在空闲时做计算,才引入的一种结构。 服务端渲染自然就不需要fiber。 不需要fiber的话,它把hook链表存放在哪里呢?vdom么? 确实可以放在vdom,但是其实并没有。 比如useRef这个hooks: 图片 它是从firstWorkInProgressHook开始的用next串联的一个链表。 图片 而firstWorkInProgressHook最开始用createHook创建的第一个hook节点: 图片 并没有挂载到vdom上。 为什么呢? 因为ssr只需要渲染一次呀,又不需要更新,自然没必要挂到vdom上。 只要每次处理完每个组件的hooks就清空一下这个hook链表就行: 图片 图片 图片 所以,reactssr时,hooks是存在全局变量上的。 对比下reactcsr和ssr时的hooks实现原理的区别: csr时会从vdom创建fiber,用于把渲染变成可打断的,通过空闲调度来提高性能,而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: 图片 图片 具体它这个hook链表存在哪我还没看,不过我们已经掌握hooks的实现原理了,只要有个上下文存放hook链表就行,在哪都可以。 总结 reacthooks是在reactfiber架构之后出现的特性,很多人误以为hooks必须配合fiber才能实现,我们分别看了react、preact、reactssr、midway中的hooks的实现,发现并不是这样的: react是把vdom转成fiber,然后把hook链表存放到了fiber。memorizedState属性上,通过next串联 preact没有实现fiber,它是把hook链表放到了vnode。component。hooks属性上,数组实现的,通过下标访问 reactssr时不需要fiber,但是也没有把hook链表挂到vdom上,而是直接放在了一个全局变量上,因为只需要渲染一次,渲染完一个组件就清空这个全局变量就行 midway是一个Node。js框架,它也实现了hooks类似的api,具体放在哪我们没深入,但是只要有个上下文存放hook链表就行 所以,reacthooks必须依赖fiber才能实现么? 明显不是,搭配fiber、搭配vdom、搭配全局变量,甚至任何一个上下文都可以。在框架中引入hooks的api并不难。