蜕变之始,useEffect 最后一种用法

2024年 1月 2日 101.9k 0

在 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 仅会在组件首次渲染完成时执行。

useEffect(effect, [])

只要我们确保当前组件在程序运行过程中相对稳定,不会随时被删除,那么我们就可以在 effect 中获取原生 DOM 节点,并添加绑定事件,回归到原生 DOM 开发思路中去,你甚至可以在这里继续使用 jQuery。

我们也可以在这里放心使用百度地图 javaScript sdk,从而完整的在 react 项目中嵌入百度地图。

function App() {
  // ...
  useEffect(() => {
    var mp = new BMapGL.Map('map'); 
    mp.centerAndZoom(new BMapGL.Point(121.491, 31.233), 11); 
  }, [])

  // ...
}

或者与 echarts 结合使用。

function App() {
  const main = useRef(null)

  useEffect(() => {
    var myChart = echarts.init(main.current);

    // 指定图表的配置项和数据
    var option = {
      title: {
        text: 'ECharts 入门示例'
      },
      tooltip: {},
      legend: {
        data: ['销量']
      },
      xAxis: {
        data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
      },
      yAxis: {},
      series: [
        {
          name: '销量',
          type: 'bar',
          data: [5, 20, 36, 10, 10, 20]
        }
      ]
    };

    // 使用刚指定的配置项和数据显示图表。
    myChart.setOption(option);
  }, [])

  return (
    
  )
}

其他的例子还有很多,我们这里使用一个案例来进一步感受 React 与原生 DOM 开发结合的方式。

二、需求

在长页面滚动的过程中,我们常常会在页面的顶部或者旁边,放一个标识组件来告诉用户页面已经滚动到什么位置了。

在滚动的过程中,当前选中状态会自动变化到对应的位置。接下来我们思考一下这样的功能应该如何实现。

本案例具体要实现的效果如图:

三、实现

实现原理比较简单,我们只需要判断每个元素什么时候应该出现在屏幕中即可。对于原生 DOM 而言,我们可以使用 getBoundingClientRect 来获取元素对象在可视区域中的位置信息。

本案例中的判断规则非常简单粗暴,因此当同屏出现两个目标元素时会存在规则冲突,实践中的规则设计会更细致一些,判断逻辑也会更复杂。

React 提供了 useRef 来获取真实 DOM 对象。

const n1 = useRef(null)

... 

本案例中的主要内容为我们前面章节中搜索的 demo,因此每个 demo 都是使用 Block 组件来包裹布局。


  
     setParam(e.target.value)}
    />
  
   (
      {item}
    )}
  />

但是因为 Block 并没有针对 ref 进行支持,因此我们可以在外面额外套一个 div 作为目标元素。

import Normal from './search/Normal'

... 


  

在页面滚动的过程中,目标元素相对于可视区域的位置会随时发生变化。因此我们可以在 effect 中添加 document 的滚动事件监听。

useEffect(() => {
  document.addEventListener('scroll', () => {
    ...
  })
}, [])

设计一个 state 状态用来对应选中状态。

const [current, setCurrent] = useState(0)

...

案例一

这样,我们只需要在滚动过程中,不停的判断每个目标元素和视口的相对位置,当符合条件的目标元素出现在视口时,就设置 current 为对应的值,功能就实现了。

useEffect(() => {
  document.addEventListener('scroll', () => {
    const n1y = n1.current?.getBoundingClientRect().y
    const n2y = n2.current?.getBoundingClientRect().y
    const n3y = n3.current?.getBoundingClientRect().y
    const n4y = n4.current?.getBoundingClientRect().y
    const n5y = n5.current?.getBoundingClientRect().y

    if (n1y > 0 && n1y  0 && n2y  0 && n3y  0 && n4y  0 && n5y < window.innerHeight) {
      setCurrent(4)
    }
  })
}, [])

当我们使用 ref 获取真实 DOM 时,ref 可能会为 null,但是由于 effect 在组件渲染完成之后执行,此时必定能获取到真实 DOM,因此我们使用 ?. 跳过了为 null 的判断。

const n1y = 
n1.current?.getBoundingClientRect().y

完整代码如下:

import { useEffect, useRef, useState } from 'react'
import Normal from './search/Normal'
import Normal2 from './search/Normal2'
import Normal3 from './search/Normal3'
import Normal4 from './search/Normal4'
import Normal5 from './search/Normal5'

import s from './style.module.scss'

export default function EffectDemo() {
  const [current, setCurrent] = useState(0)
  const n1 = useRef(null)
  const n2 = useRef(null)
  const n3 = useRef(null)
  const n4 = useRef(null)
  const n5 = useRef(null)

  useEffect(() => {
    document.addEventListener('scroll', () => {
      const n1y = n1.current?.getBoundingClientRect().y as number
      const n2y = n2.current?.getBoundingClientRect().y as number
      const n3y = n3.current?.getBoundingClientRect().y as number
      const n4y = n4.current?.getBoundingClientRect().y as number
      const n5y = n5.current?.getBoundingClientRect().y as number

      if (n1y > 0 && n1y  0 && n2y  0 && n3y  0 && n4y  0 && n5y < window.innerHeight) {
        setCurrent(4)
      }
    })
  }, [])

  return (
    
      
        案例一
        案例二
        案例三
        案例四
        案例五
      
      
        
      
      
        
      
      
      
        
      

      
        
      
      
      
        
      
      
    
  )
}

四、总结

大量的初级框架开发者被困在「试图让框架解决所有问题」的思想牢笼里,认为学会了框架似乎就完事了。然而事实上,灵活运用 React 的 escape hatch 特性是成为 React 高手的标志之一,他在指引学习者不要把学习目标局限在 React 中,这是我们蜕变的开始。

不要试图让 React 解决所有问题,让他做擅长的事情。

接下来我们思考两个问题,一个问题是,在上面的案例中,我并没有移除事件绑定,这样的行为是否会造成内存泄露?第二个问题,我们期望封装一个图片组件,该组件需要支持懒加载的优化特性:只有当图片内容进入到可视区域时,图片才开始加载,这样的图片组件应该如何封装?

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论