蛻變之始,useEffect 最后一種用法
在 React 官方文檔中,對于 useEffect 有這樣一句描述:Effects are an escape hatch from the React paradigm。
這句話怎么理解呢?我們要結(jié)合前面的哲學部分進行思考。React 開發(fā)指導思想是數(shù)據(jù)驅(qū)動 UI,因此在 React 程序中,我們總是會思考如何設(shè)計與 UI 保持一致的數(shù)據(jù),把解決問題的重心放在數(shù)據(jù)邏輯上。
但是這樣的思路并不能應(yīng)對所有場景。在一些特殊的場景里,我們需要跳出數(shù)據(jù)驅(qū)動 UI 的解題思路,例如為了避免出現(xiàn)性能瓶頸,在高頻率的事件監(jiān)聽中,我們會選擇直接使用原生 DOM 節(jié)點來解決問題。
意思就是說,如果你想要跳出 React 的環(huán)境使用其他的方式開發(fā),可以把 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 第二個參數(shù)為一個數(shù)組,當我們傳入的參數(shù)為一個空數(shù)組時,表示 effect 僅會在組件首次渲染完成時執(zhí)行。
useEffect(effect, [])
只要我們確保當前組件在程序運行過程中相對穩(wěn)定,不會隨時被刪除,那么我們就可以在 effect 中獲取原生 DOM 節(jié)點,并添加綁定事件,回歸到原生 DOM 開發(fā)思路中去,你甚至可以在這里繼續(xù)使用 jQuery。
我們也可以在這里放心使用百度地圖 javaScript sdk,從而完整的在 react 項目中嵌入百度地圖。
function App() {
// ...
useEffect(() => {
var mp = new BMapGL.Map('map');
mp.centerAndZoom(new BMapGL.Point(121.491, 31.233), 11);
}, [])
// ...
}
或者與 echarts 結(jié)合使用。
function App() {
const main = useRef(null)
useEffect(() => {
var myChart = echarts.init(main.current);
// 指定圖表的配置項和數(shù)據(jù)
var option = {
title: {
text: 'ECharts 入門示例'
},
tooltip: {},
legend: {
data: ['銷量']
},
xAxis: {
data: ['襯衫', '羊毛衫', '雪紡衫', '褲子', '高跟鞋', '襪子']
},
yAxis: {},
series: [
{
name: '銷量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};
// 使用剛指定的配置項和數(shù)據(jù)顯示圖表。
myChart.setOption(option);
}, [])
return (
<div id="main" ref={main}></div>
)
}
其他的例子還有很多,我們這里使用一個案例來進一步感受 React 與原生 DOM 開發(fā)結(jié)合的方式。
二、需求
在長頁面滾動的過程中,我們常常會在頁面的頂部或者旁邊,放一個標識組件來告訴用戶頁面已經(jīng)滾動到什么位置了。
在滾動的過程中,當前選中狀態(tài)會自動變化到對應(yīng)的位置。接下來我們思考一下這樣的功能應(yīng)該如何實現(xiàn)。
本案例具體要實現(xiàn)的效果如圖:
三、實現(xiàn)
實現(xiàn)原理比較簡單,我們只需要判斷每個元素什么時候應(yīng)該出現(xiàn)在屏幕中即可。對于原生 DOM 而言,我們可以使用 getBoundingClientRect 來獲取元素對象在可視區(qū)域中的位置信息。
本案例中的判斷規(guī)則非常簡單粗暴,因此當同屏出現(xiàn)兩個目標元素時會存在規(guī)則沖突,實踐中的規(guī)則設(shè)計會更細致一些,判斷邏輯也會更復雜。
React 提供了 useRef 來獲取真實 DOM 對象。
const n1 = useRef<HTMLDivElement>(null)
...
<div ref={n1}></div>
本案例中的主要內(nèi)容為我們前面章節(jié)中搜索的 demo,因此每個 demo 都是使用 Block 組件來包裹布局。
<Block className={s.container} title={td.title} desc={td.desc}>
<div className={r.flex}>
<input
className={s.input}
placeholder="請輸入您要搜索的內(nèi)容"
onChange={(e) => setParam(e.target.value)}
/>
</div>
<List
list={list}
loading={loading}
error={error}
renderItem={(item) => (
<div key={item} className={s.item}>{item}</div>
)}
/>
</Block>
但是因為 Block 并沒有針對 ref 進行支持,因此我們可以在外面額外套一個 div 作為目標元素。
import Normal from './search/Normal'
...
<div ref={n1}>
<Normal />
</div>
在頁面滾動的過程中,目標元素相對于可視區(qū)域的位置會隨時發(fā)生變化。因此我們可以在 effect 中添加 document 的滾動事件監(jiān)聽。
useEffect(() => {
document.addEventListener('scroll', () => {
...
})
}, [])
設(shè)計一個 state 狀態(tài)用來對應(yīng)選中狀態(tài)。
const [current, setCurrent] = useState(0)
...
<div className={current == 0 ? `${s.item} ${s.active}` : s.item}>案例一</div>
這樣,我們只需要在滾動過程中,不停的判斷每個目標元素和視口的相對位置,當符合條件的目標元素出現(xiàn)在視口時,就設(shè)置 current 為對應(yīng)的值,功能就實現(xiàn)了。
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 < window.innerHeight) {
setCurrent(0)
}
if (n2y > 0 && n2y < window.innerHeight) {
setCurrent(1)
}
if (n3y > 0 && n3y < window.innerHeight) {
setCurrent(2)
}
if (n4y > 0 && n4y < window.innerHeight) {
setCurrent(3)
}
if (n5y > 0 && n5y < window.innerHeight) {
setCurrent(4)
}
})
}, [])
當我們使用 ref 獲取真實 DOM 時,ref 可能會為 null,但是由于 effect 在組件渲染完成之后執(zhí)行,此時必定能獲取到真實 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<HTMLDivElement>(null)
const n2 = useRef<HTMLDivElement>(null)
const n3 = useRef<HTMLDivElement>(null)
const n4 = useRef<HTMLDivElement>(null)
const n5 = useRef<HTMLDivElement>(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 < window.innerHeight) {
setCurrent(0)
}
if (n2y > 0 && n2y < window.innerHeight) {
setCurrent(1)
}
if (n3y > 0 && n3y < window.innerHeight) {
setCurrent(2)
}
if (n4y > 0 && n4y < window.innerHeight) {
setCurrent(3)
}
if (n5y > 0 && n5y < window.innerHeight) {
setCurrent(4)
}
})
}, [])
return (
<div>
<div className={s.nav}>
<div className={current == 0 ? `${s.item} ${s.active}` : s.item}>案例一</div>
<div className={current == 1 ? `${s.item} ${s.active}` : s.item}>案例二</div>
<div className={current == 2 ? `${s.item} ${s.active}` : s.item}>案例三</div>
<div className={current == 3 ? `${s.item} ${s.active}` : s.item}>案例四</div>
<div className={current == 4 ? `${s.item} ${s.active}` : s.item}>案例五</div>
</div>
<div ref={n1}>
<Normal />
</div>
<div ref={n2}>
<Normal2 />
</div>
<div ref={n3}>
<Normal3 />
</div>
<div ref={n4}>
<Normal4 />
</div>
<div ref={n5}>
<Normal5 />
</div>
</div>
)
}
四、總結(jié)
大量的初級框架開發(fā)者被困在「試圖讓框架解決所有問題」的思想牢籠里,認為學會了框架似乎就完事了。然而事實上,靈活運用 React 的 escape hatch 特性是成為 React 高手的標志之一,他在指引學習者不要把學習目標局限在 React 中,這是我們蛻變的開始。
不要試圖讓 React 解決所有問題,讓他做擅長的事情。
接下來我們思考兩個問題,一個問題是,在上面的案例中,我并沒有移除事件綁定,這樣的行為是否會造成內(nèi)存泄露?第二個問題,我們期望封裝一個圖片組件,該組件需要支持懶加載的優(yōu)化特性:只有當圖片內(nèi)容進入到可視區(qū)域時,圖片才開始加載,這樣的圖片組件應(yīng)該如何封裝?