在 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 ( <div id="main" ref={main}></div> )}
其他的例子還有很多,我們這里使用一個案例來進一步感受 React 與原生 DOM 開發結合的方式。
在長頁面滾動的過程中,我們常常會在頁面的頂部或者旁邊,放一個標識組件來告訴用戶頁面已經滾動到什么位置了。
在滾動的過程中,當前選中狀態會自動變化到對應的位置。接下來我們思考一下這樣的功能應該如何實現。
本案例具體要實現的效果如圖:
實現原理比較簡單,我們只需要判斷每個元素什么時候應該出現在屏幕中即可。對于原生 DOM 而言,我們可以使用 getBoundingClientRect 來獲取元素對象在可視區域中的位置信息。
本案例中的判斷規則非常簡單粗暴,因此當同屏出現兩個目標元素時會存在規則沖突,實踐中的規則設計會更細致一些,判斷邏輯也會更復雜。
React 提供了 useRef 來獲取真實 DOM 對象。
const n1 = useRef<HTMLDivElement>(null)... <div ref={n1}></div>
本案例中的主要內容為我們前面章節中搜索的 demo,因此每個 demo 都是使用 Block 組件來包裹布局。
<Block className={s.container} title={td.title} desc={td.desc}> <div className={r.flex}> <input className={s.input} placeholder="請輸入您要搜索的內容" 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>
在頁面滾動的過程中,目標元素相對于可視區域的位置會隨時發生變化。因此我們可以在 effect 中添加 document 的滾動事件監聽。
useEffect(() => { document.addEventListener('scroll', () => { ... })}, [])
設計一個 state 狀態用來對應選中狀態。
const [current, setCurrent] = useState(0)...<div className={current == 0 ? `${s.item} ${s.active}` : s.item}>案例一</div>
這樣,我們只需要在滾動過程中,不停的判斷每個目標元素和視口的相對位置,當符合條件的目標元素出現在視口時,就設置 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 < 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 在組件渲染完成之后執行,此時必定能獲取到真實 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> )}
大量的初級框架開發者被困在「試圖讓框架解決所有問題」的思想牢籠里,認為學會了框架似乎就完事了。然而事實上,靈活運用 React 的 escape hatch 特性是成為 React 高手的標志之一,他在指引學習者不要把學習目標局限在 React 中,這是我們蛻變的開始。
不要試圖讓 React 解決所有問題,讓他做擅長的事情。
接下來我們思考兩個問題,一個問題是,在上面的案例中,我并沒有移除事件綁定,這樣的行為是否會造成內存泄露?第二個問題,我們期望封裝一個圖片組件,該組件需要支持懶加載的優化特性:只有當圖片內容進入到可視區域時,圖片才開始加載,這樣的圖片組件應該如何封裝?
本文鏈接:http://www.www897cc.com/showinfo-26-55008-0.html蛻變之始,useEffect 最后一種用法
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: Go測試的20個實用建議,你采納了嗎?