react?定位組件源碼解析
正文
react組件庫(kù)系列耽擱了一些時(shí)間,繼續(xù)了!定位組件在b端里面實(shí)在太常見了。
首先介紹一下什么是定位組件,如下圖:
也就是上面的“這是一個(gè)彈窗的div”其實(shí)是絕對(duì)定位到按鈕上的,因?yàn)槭墙^對(duì)定位,所以可以定位到任意元素上。
整體源碼來自于t-design的popup組件
但是他們也是二次封裝了react-popper,react-popper又依賴@popperjs/core
,所以這篇文章主要解決的是@popperjs/core
的實(shí)現(xiàn)原理。
計(jì)劃:
- 本篇是
@popperjs/core
的實(shí)現(xiàn)核心原理 - 第二篇講
react-popper
實(shí)現(xiàn)核心原理 - 最后一篇是
t-design
的popup
實(shí)現(xiàn)原理。
講源碼之前,跟大家亮點(diǎn)好玩的知識(shí)點(diǎn):
里面有很多實(shí)用的工具函數(shù),說實(shí)話,原以為自己原生dom api掌握還挺熟練的,看了這些兼容性的代碼,我才知道自己有多無知,舉個(gè)例子,你們一個(gè)元素的position是absolute,那么它是相當(dāng)于誰(shuí)定位?例如:
<body> <div> 網(wǎng)二 </div> <div style="transform: translateX(2px);"> <span style="position: absolute; top: 0" >李四</span> </div> </body>
肯定有人說了,這個(gè)我熟啊,相當(dāng)于上面包含它的元素只要不是static定位的。這個(gè)沒錯(cuò),但是只答對(duì)一部分,還有一種可能,本身元素是static元素也會(huì)成為定位上下文,比如給它加一個(gè)transform屬性,你可以試試上面的代碼,李四是相對(duì)于transform屬性的div定位的。
不僅僅是transform屬性,下面的方式都可以成定位上下文元素(當(dāng)時(shí)看源碼這里我是怎么也不明白為啥要判斷下面這些)
- 有transform、perspective、filter屬性中任意一個(gè)不為none。
- 是will-change屬性值為以上任意一個(gè)屬性的祖先元素。
- 是contain屬性值包含paint、layout、strict或content的祖先元素。
正文開始!
如何創(chuàng)建基礎(chǔ)的popper
使用createPopper API,我們先看看是怎么用的:
假設(shè)我們有這樣一個(gè)html文件
<!DOCTYPE html> <html> <head> <title>Popper Tutorial</title> </head> <style> #tooltip { background: #333; color: white; font-weight: bold; padding: 4px 8px; font-size: 13px; border-radius: 4px; } </style> <body> <button id="button" aria-describedby="tooltip">My button</button> <div id="tooltip" role="tooltip">My tooltip</div> <script src="https://unpkg.com/@popperjs/core@2"></script> <script> const button = document.querySelector('#button'); const tooltip = document.querySelector('#tooltip'); const popperInstance = Popper.createPopper(button, tooltip); </script> </body> </html>
我們的目的是把tooltip組件定位到button組件下面,如下圖
我們僅僅使用了
使用createPopper
函數(shù),需要傳遞兩個(gè)參數(shù):參考元素(reference element)和彈出式元素(popper element),以及一個(gè)可選的配置對(duì)象。參考元素是要彈出的元素的參照點(diǎn),而彈出式元素是要彈出的元素本身。
例如,以下是一個(gè)創(chuàng)建彈出式元素的示例代碼:
import { createPopper } from '@popperjs/core'; const referenceElement = document.querySelector('#reference'); const popperElement = document.querySelector('#popper'); const popper = createPopper(referenceElement, popperElement, { placement: 'top', });
上面的代碼中,createPopper
函數(shù)接收參考元素和彈出式元素,以及一個(gè)配置對(duì)象。這里的配置對(duì)象中指定了彈出式元素的位置,它將出現(xiàn)在參考元素的上方。
popper是一個(gè)對(duì)象,其中的方法用于進(jìn)一步管理和控制彈出式元素的行為。例如,可以使用update
函數(shù)在參考元素或彈出式元素發(fā)生變化時(shí)重新計(jì)算彈出式元素的位置和大小,或者可以使用destroy
函數(shù)在不需要彈出式元素時(shí)將其刪除。
createPopper函數(shù)的返回值
包含以下屬性和方法的對(duì)象:
state
:一個(gè)對(duì)象,包含有關(guān)彈出式元素的位置、大小和其他信息的狀態(tài)信息。update
:一個(gè)函數(shù),用于更新彈出式元素的位置和大小。forceUpdate
:一個(gè)函數(shù),用于強(qiáng)制更新彈出式元素的位置和大小。destroy
:一個(gè)函數(shù),用于刪除彈出式元素并清除其事件偵聽器。setOptions
: 這個(gè)函數(shù)用于更新彈出式元素的配置選項(xiàng)。
上面我們的案例是如何實(shí)現(xiàn)自動(dòng)定位的呢,也就是createPopper函數(shù)調(diào)用后,
const popper = createPopper(referenceElement, popperElement, { placement: 'top', });
popperElement自動(dòng)定位到referenceElement元素的上面了,原因就是createPopper在執(zhí)行過程中,調(diào)用了setOptions方法,所以我們只要看一下setOptions方法如何實(shí)現(xiàn),就知道了它如何實(shí)現(xiàn)自動(dòng)定位了。
注:這里我們不加入任何中間件,這樣會(huì)提高復(fù)雜度,后面再講幾個(gè)典型的中間件實(shí)現(xiàn)原理。
setOptions方法本質(zhì)上是調(diào)用了forceUpdate方法。
forceUpdate() { const { reference, popper } = state.elements; state.rects = { reference: getCompositeRect( reference, getOffsetParent(popper), state.options.strategy === 'fixed' ), popper: getLayoutRect(popper), }; }
我們先開第一部分,reference, popper是啥意思呢,如下,reference就是referenceElement,popper就是popperElement。
const popper = createPopper(referenceElement, popperElement, { placement: 'top', });
state.rects是啥意思呢,簡(jiǎn)單來說,就是包含了reference的getBoundingClientRect的結(jié)果,popper也是包含了reference的getBoundingClientRect的結(jié)果。
這里再簡(jiǎn)單講一下 getBoundingClientRect
是什么。
含義:
方法返回元素的大小及其相對(duì)于視口的位置。
值:
返回值是一個(gè) DOMRect 對(duì)象,這個(gè)對(duì)象是由該元素的 getClientRects() 方法返回的一組矩形的集合, 即:是與該元素相關(guān)的CSS 邊框集合。
屬性值:
- top: 元素上邊距離頁(yè)面上邊的距離
- left: 元素右邊距離頁(yè)面左邊的距離
- right: 元素右邊距離頁(yè)面左邊的距離
- bottom: 元素下邊距離頁(yè)面上邊的距離
- width: 元素寬度
- height: 元素高度
為什么需要這些屬性呢?你想想,我絕對(duì)定位某個(gè)元素,我知道了另一個(gè)元素的坐標(biāo),是不是絕對(duì)定位上去就很簡(jiǎn)單了?
這也是定位組件最最最基本的思想,所有的定位組件都差不多。
接著講:
forceUpdate() { const { reference, popper } = state.elements; state.rects = { reference: getCompositeRect( reference, getOffsetParent(popper), state.options.strategy === 'fixed' ), popper: getLayoutRect(popper), }; }
為什么這里要用getCompositeRect來代替getBoundingClientRect的功能,其實(shí)里面主要也是用了getBoundingClientRect的功能。
最主要的區(qū)別就是一些非常細(xì)節(jié)的處理了:
絕對(duì)定位的坐標(biāo)受到transfrom: scale的影響
我們舉個(gè)例子:
<style> #a { margin: 0 auto; width: 500px; position: relative; transform: scale(2.5); } #tooltip { background: #333; color: white; font-weight: bold; padding: 4px 8px; font-size: 13px; border-radius: 4px; } </style> <body> <div id="a"> <button id="button" aria-describedby="tooltip">My button</button> <div id="tooltip" role="tooltip">My tooltip</div> </div>
注意,id是a的div元素,有可能transform的scale出現(xiàn)變化,那么你定位的時(shí)候,是不是要找出scale的值是2.5,然后在正常scale(1)的情況下,決定定位的x,y,width,height都要乘以2.5。
那么問題來了,怎么計(jì)算scale的值呢?有人說了,我可以用getComputedStyle獲取到,問題來了,我還可以用直接在css上設(shè)置scale屬性,縮小和放大,我還可以一起上兩個(gè)屬性,你咋辦?
所以我們要用以下的方法
const dom= xxx; //假設(shè)獲取到了某個(gè)dom元素 dom.getBoundingClientRect().width / dom.offsetWith
學(xué)到了吧,我真的強(qiáng)烈大家多看看開源的好的代碼,你們自己項(xiàng)目很多前端不可能知道這些細(xì)節(jié)的。
好了,繼續(xù)!請(qǐng)問相對(duì)定位的元素如何查找?
有人說了,廢話,文章開頭不是說了嗎,相當(dāng)于offsetParent或者有一些例如css屬性是transform等等屬性的dom元素。
這里面又充滿了坑!
例如,如果是offsetParent是table元素的情況,table元素,并且定位是static的話,我們需要繼續(xù)網(wǎng)上找offsetParent,為啥呢?
如果一個(gè)元素的父元素是一個(gè)table元素,而該元素又沒有顯式地設(shè)置position屬性,那么該元素的offsetParent會(huì)被設(shè)置為table元素的父元素。
所以通常offsetParent屬性得到的是position是非static的元素,這個(gè)就出現(xiàn)問題了?。ㄟ€有一些小細(xì)節(jié),繼續(xù)說下去就太多內(nèi)容了)
計(jì)算相當(dāng)于最近的offsetParent元素,如何計(jì)算絕對(duì)定位的值
源碼核心如下:
const rect = element.getBoundingClientRect(); const scroll = getNodeScroll(offsetParent); offsets = getBoundingClientRect(offsetParent, true); offsets.x += offsetParent.clientLeft; offsets.y += offsetParent.clientTop; return { x: rect.left + scroll.scrollLeft - offsets.x, y: rect.top + scroll.scrollTop - offsets.y, width: rect.width, height: rect.height, }
這里的rect是reference元素,scroll可以的話,如果offsetParent我們簡(jiǎn)單看做是window元素,然后獲取的是滾動(dòng)條的滾動(dòng)距離,offsets.x是offsetParent的left + offsetParent.clientLeft的和。
為什么這么算呢,你看啊,reference.left - offsetParent.x(可以認(rèn)為是offsetParent.left),得到的是reference元素左側(cè)到offsetParent元素左側(cè)的距離。
然后上面得到的值加上滾動(dòng)條距離,是不是就是renference元素在offsetParent中的坐標(biāo)了。
注意,源碼里的絕對(duì)定位,雖然position:absolute,但是位移用的transform,而不是top,left這種,目的是提高性能
中間件處理
const orderedModifiers = orderModifiers( mergeByName([...defaultModifiers, ...state.options.modifiers]) ); // Strip out disabled modifiers state.orderedModifiers = orderedModifiers.filter((m) => m.enabled); state.orderedModifiers.forEach( (modifier) => (state.modifiersData[modifier.name] = { ...modifier.data, }) );
首先mergeByName是什么意思,主要是把所有中間件合并了,一個(gè)中間件長(zhǎng)啥樣呢,如下:
{ name: 'offset', enabled: true, phase: 'main', requires: ['popperOffsets'], fn: offset, }
上面一個(gè)命名為offset的中間件,處理的生命周期在'main'這個(gè)生命周期中。處理這個(gè)offset的中間件函數(shù)是fn屬性里的offset函數(shù),我們跳過,這個(gè)函數(shù)的實(shí)現(xiàn),因?yàn)槲覀冎皇菫榱撕?jiǎn)單介紹中間件是什么。
后面我們會(huì)講生命周期鉤子函數(shù)。
mergeByName簡(jiǎn)單來說,就是我們的中間件如下:
[ { name: 'offset', enabled: true, phase: 'main', requires: ['popperOffsets'], fn: offset, }, { name: 'offset', enabled: true, phase: 'main', requires: ['popperOffsets'], fn: offset1, } ]
也就是可能有重名的中間件,然后將他們合并,我們看到上面數(shù)組第一個(gè)元素的fn是offset,第二個(gè)是offset1,此時(shí)offset1就會(huì)覆蓋offset,也就是所有中間件最終只能有一個(gè)名字唯一的去處理它。
最終合并為
[ { name: 'offset', enabled: true, phase: 'main', requires: ['popperOffsets'], fn: offset1, } ]
orderModifiers
上面處理過后會(huì)把結(jié)果傳給orderModifiers,它會(huì)做兩件事:
- 1、處理依賴
- 2、按照生命周期分層
處理依賴
如何處理依賴呢,我們看到上面的 offset中間件有一個(gè) requires: ['popperOffsets'],意思是offset中間件加載之前,首先要popperOffsets中間件處理。所以我們遇到這種情況就要先加載popperOffsets中間件。
注意,這里并沒有處理循環(huán)依賴的情況,需要使用者自己注意(循環(huán)依賴最終會(huì)報(bào)錯(cuò),因?yàn)榭隙〞?huì)棧溢出)。
我們簡(jiǎn)單看下這個(gè)order函數(shù)如何處理依賴關(guān)系。
function order(modifiers) { const map = new Map(); const visited = new Set(); const result = []; modifiers.forEach(modifier => { map.set(modifier.name, modifier); }); // On visiting object, check for its dependencies and visit them recursively function sort(modifier: Modifier<any, any>) { visited.add(modifier.name); const requires = [ ...(modifier.requires || []), ...(modifier.requiresIfExists || []), ]; requires.forEach(dep => { if (!visited.has(dep)) { const depModifier = map.get(dep); if (depModifier) { sort(depModifier); } } }); result.push(modifier); } modifiers.forEach(modifier => { if (!visited.has(modifier.name)) { // check for visited object sort(modifier); } }); return result; }
這里的關(guān)鍵就是sort函數(shù),首先visited函數(shù)會(huì)判斷在加載某個(gè)中間件時(shí),你是否有依賴,有的話,我看看我之前加載過沒有,沒有的話我就先加載依賴。
按照生命周期分層
代碼如下
modifierPhases.reduce((acc, phase) => { return acc.concat( orderedModifiers.filter(modifier => modifier.phase === phase) ); }, []);
modifierPhases是一個(gè)字符串?dāng)?shù)組,這個(gè)數(shù)組的順序就是生命周期鉤子函數(shù)的順序,或者說處理中間件的順序。
// 如下的變量看做字符串即可 const modifierPhases = [ beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite, ];
通過reduce函數(shù),會(huì)先處理數(shù)組靠前的名字的中間件。
我們接著看剛才的中間件處理流程:
const orderedModifiers = orderModifiers( mergeByName([...defaultModifiers, ...state.options.modifiers]) ); // Strip out disabled modifiers state.orderedModifiers = orderedModifiers.filter((m) => m.enabled); state.orderedModifiers.forEach( (modifier) => (state.modifiersData[modifier.name] = { ...modifier.data, }) );
orderedModifiers我們之前介紹了,接著state抽取了所有orderedModifiers中的data數(shù)據(jù),一般情況是沒有這個(gè)數(shù)據(jù)的。
最后orderedModifiers也掛載到了state.orderedModifiers上。
簡(jiǎn)而言之,中間件就是把我們定位坐標(biāo)進(jìn)行了變換,或者添加了監(jiān)聽事件
比如絕對(duì)定位好的坐標(biāo),如果滾動(dòng)條滾動(dòng),是不是要更新坐標(biāo)才能繼續(xù)定位準(zhǔn)確?
這里主邏輯就解釋完了,然后有人就會(huì)說what????沒有寫什么時(shí)候把定位的坐標(biāo)賦給定位元素?。?!
這個(gè)邏輯是寫在中間件里的,所以自然而然我們開始講中間件。
所有官方中間件都會(huì)講
eventListeners中間件
這個(gè)中間件簡(jiǎn)單來說就是遞歸尋找所有的父元素,如果有滾動(dòng)條的話,就加上scroll事件,然后觸發(fā)更新定位坐標(biāo)。
為啥要更新定位坐標(biāo)上面已經(jīng)說的很清楚了唄。
順便再給window事件加上resize事件,這個(gè)也是為了害怕resize窗口導(dǎo)致定位元素定位偏離。
popperOffsets中間件
簡(jiǎn)單來說,就是根據(jù)placement計(jì)算坐標(biāo),placement比如是top,就是絕對(duì)定位到某個(gè)元素的上方,而且是居中對(duì)齊。
這個(gè)坐標(biāo)咋算呢,我簡(jiǎn)單說下頂部居中對(duì)齊,大家自己算就行了
const commonX = reference.x + reference.width / 2 - element.width / 2; case top: offsets = { x: commonX, y: reference.y - element.height, };
最后得到的定位坐標(biāo)放到了state屬性上,如下:
state.modifiersData[popperOffsets] = 定位坐標(biāo)
computeStyle中間件
如果state上的popperOffsets屬性不為null,也就是我們上面計(jì)算過的popperOffsets。然后給定義新的定位元素的坐標(biāo)。
有人會(huì)問了,為啥要定義新的坐標(biāo)
if (state.modifiersData.popperOffsets != null) { state.styles.popper = { ...state.styles.popper, ...mapToStyles({ placement: getBasePlacement(state.placement), variation: getVariation(state.placement), popper: state.elements.popper, popperRect: state.rects.popper, gpuAcceleration, isFixed: state.options.strategy === 'fixed', offsets: state.modifiersData.popperOffsets, position: state.options.strategy, adaptive, roundOffsets, }), }; }
可以看到,核心的是mapToStyles這個(gè)處理函數(shù)。我們看下它的實(shí)現(xiàn),簡(jiǎn)單來說:
如果gpuAcceleration參數(shù)為true,那么我們的定位使用transfrom,否則使用left,top這種方式定位
如果adaptive為true,假設(shè)reference元素變寬或者變窄(比如一段文字),它會(huì)自動(dòng)定位上去
applyStyles中間件
這個(gè)很簡(jiǎn)單,上面我們不是把定位的坐標(biāo)求出來了嗎,這個(gè)中間件就是把定位組件的styles屬性合并上去的,源碼如下,element就是定位元素,這種方式值得大家學(xué)習(xí),而不是直接賦值給style。
const attributes = state.attributes[name] || {}; Object.assign(element.style, style);
offset中間件
這個(gè)太簡(jiǎn)單了,偏移距離用的,請(qǐng)看下圖:
flip中間件
原理是,比如我們現(xiàn)在placement:bottom,表示定位到reference元素的下方,當(dāng)我們向下滾動(dòng)的時(shí)候,是不是這個(gè)定位的元素因?yàn)樵谙路?,遲早會(huì)到視口的下面,如下圖:
為了能看見tooltip,我們自動(dòng)翻轉(zhuǎn)到上方!
這就是flip的功能,至于如何實(shí)現(xiàn),我們馬上分析:
假設(shè)我們傳入的placement是bottom,會(huì)自動(dòng)計(jì)算它相反的位置:最后生成['bottom','top'],意思是如果bottom超出視口邊界,就轉(zhuǎn)到top的位置去。
這個(gè)位置我們還可以外界自定義,默認(rèn)的是placement是top,那么就生成['top','bottom'],如果是left就生成['left', 'right'],也就是自己的位置和相反的位置。
然后通過一個(gè)函數(shù)detectOverflow(建議大家可以單獨(dú)copy一份這個(gè)函數(shù)的代碼,表示是否傳入的元素已經(jīng)超過視口的)
但是原理也很簡(jiǎn)單,如果我去寫的話,就是判斷當(dāng)前元素的最上邊是否超過定位它的父元素的最上邊,最左邊和其他方向都是一樣的。比較坐標(biāo)嘛。
然后如果超出也很簡(jiǎn)單,你超出了top,你就返回top: true,沒有就返回top:false,我知道如果超出不就馬上計(jì)算另一個(gè)方向的坐標(biāo)了嗎
以上就是react 定位組件源碼解析的詳細(xì)內(nèi)容,更多關(guān)于react 定位組件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 解決React報(bào)錯(cuò)Expected an assignment or function call and instead saw an expression
- React報(bào)錯(cuò)信息之Expected?an?assignment?or?function?call?and?instead?saw?an?expression
- VueJs中的shallowRef與shallowReactive函數(shù)使用比較
- Project?Reactor?響應(yīng)式范式編程
- React 遠(yuǎn)程動(dòng)態(tài)組件實(shí)踐示例詳解
- 通過示例源碼解讀React首次渲染流程
- react?express實(shí)現(xiàn)webssh?demo解析
相關(guān)文章
在React中強(qiáng)制重新渲染的4 種方式案例代碼
這篇文章主要介紹了在React中強(qiáng)制重新渲染的4 種方式,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-12-12react實(shí)現(xiàn)動(dòng)態(tài)選擇框
這篇文章主要為大家詳細(xì)介紹了react實(shí)現(xiàn)動(dòng)態(tài)選擇框,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08React-Native使用Mobx實(shí)現(xiàn)購(gòu)物車功能
本篇文章主要介紹了React-Native使用Mobx實(shí)現(xiàn)購(gòu)物車功能,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09react實(shí)現(xiàn)組件狀態(tài)緩存的示例代碼
本文主要介紹了react實(shí)現(xiàn)組件狀態(tài)緩存的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02