在 React 官方文档中,对于 useEffect 有这样一句描述:Effects are an escape hatch from the React paradigm。
这句话怎么理解呢?我们要结合前面的哲学部分进行思考。React 开发指导思想是数据驱动 UI,因此在 React 程序中,我们总是会思考如何设计与 UI 保持一致的数据,把解决问题的重心放在数据逻辑上。
但是这样的思路并不能应对所有场景。在一些特殊的场景里,我们需要跳出数据驱动 UI 的解题思路,例如为了避免出现性能瓶颈,在高频率的事件监听中,我们会选择直接使用原生 DOM 节点来解决问题。
意思就是说,如果你想要跳出 React 的环境使用其他的方式开发,可以把 effect 作为一个对外接口。
官方文档也对此有进一步的解释:
They let you “step outside” of React and synchronize your components with some external system like a non-React widget, network, or the browser DOM
这样的 escape hatch 给 React 带来了极大的灵活性。在一个复杂的项目中,我们可以使用 React 解决一部分逻辑,然后使用别的更合适的方案解决另外的问题,这样的灵活性提高了 React 项目的上限。这也是 React 能参与到类似于 Figma 这样庞大、复杂、对性能要求极高的项目中去的原因。
Figma
一、如何运用
useEffect 第二个参数为一个数组,当我们传入的参数为一个空数组时,表示 effect 仅会在组件首次渲染完成时执行。
只要我们确保当前组件在程序运行过程中相对稳定,不会随时被删除,那么我们就可以在 effect 中获取原生 DOM 节点,并添加绑定事件,回归到原生 DOM 开发思路中去,你甚至可以在这里继续使用 jQuery。
我们也可以在这里放心使用百度地图 javaScript sdk,从而完整的在 react 项目中嵌入百度地图。
或者与 echarts 结合使用。
其他的例子还有很多,我们这里使用一个案例来进一步感受 React 与原生 DOM 开发结合的方式。
二、需求
在长页面滚动的过程中,我们常常会在页面的顶部或者旁边,放一个标识组件来告诉用户页面已经滚动到什么位置了。
在滚动的过程中,当前选中状态会自动变化到对应的位置。接下来我们思考一下这样的功能应该如何实现。
本案例具体要实现的效果如图:
三、实现
实现原理比较简单,我们只需要判断每个元素什么时候应该出现在屏幕中即可。对于原生 DOM 而言,我们可以使用 getBoundingClientRect 来获取元素对象在可视区域中的位置信息。
本案例中的判断规则非常简单粗暴,因此当同屏出现两个目标元素时会存在规则冲突,实践中的规则设计会更细致一些,判断逻辑也会更复杂。
React 提供了 useRef 来获取真实 DOM 对象。
本案例中的主要内容为我们前面章节中搜索的 demo,因此每个 demo 都是使用 Block 组件来包裹布局。
但是因为 Block 并没有针对 ref 进行支持,因此我们可以在外面额外套一个 div 作为目标元素。
在页面滚动的过程中,目标元素相对于可视区域的位置会随时发生变化。因此我们可以在 effect 中添加 document 的滚动事件监听。
设计一个 state 状态用来对应选中状态。
这样,我们只需要在滚动过程中,不停的判断每个目标元素和视口的相对位置,当符合条件的目标元素出现在视口时,就设置 current 为对应的值,功能就实现了。
当我们使用 ref 获取真实 DOM 时,ref 可能会为 null,但是由于 effect 在组件渲染完成之后执行,此时必定能获取到真实 DOM,因此我们使用 ?. 跳过了为 null 的判断。
完整代码如下:
四、总结
大量的初级框架开发者被困在「试图让框架解决所有问题」的思想牢笼里,认为学会了框架似乎就完事了。然而事实上,灵活运用 React 的 escape hatch 特性是成为 React 高手的标志之一,他在指引学习者不要把学习目标局限在 React 中,这是我们蜕变的开始。
不要试图让 React 解决所有问题,让他做擅长的事情。
接下来我们思考两个问题,一个问题是,在上面的案例中,我并没有移除事件绑定,这样的行为是否会造成内存泄露?第二个问题,我们期望封装一个图片组件,该组件需要支持懒加载的优化特性:只有当图片内容进入到可视区域时,图片才开始加载,这样的图片组件应该如何封装?