react?定位組件源碼解析
正文
react組件庫(kù)系列耽擱了一些時(shí)間,繼續(xù)了!定位組件在b端里面實(shí)在太常見(jiàn)了。
首先介紹一下什么是定位組件,如下圖:

也就是上面的“這是一個(gè)彈窗的div”其實(shí)是絕對(duì)定位到按鈕上的,因?yàn)槭墙^對(duì)定位,所以可以定位到任意元素上。
整體源碼來(lái)自于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ù),說(shuō)實(shí)話,原以為自己原生dom api掌握還挺熟練的,看了這些兼容性的代碼,我才知道自己有多無(wú)知,舉個(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>
肯定有人說(shuō)了,這個(gè)我熟啊,相當(dāng)于上面包含它的元素只要不是static定位的。這個(gè)沒(méi)錯(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的祖先元素。
正文開(kāi)始!
如何創(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ù),用于刪除彈出式元素并清除其事件偵聽(tīng)器。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í)行過(guò)程中,調(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),
};
}
我們先開(kāi)第一部分,reference, popper是啥意思呢,如下,reference就是referenceElement,popper就是popperElement。
const popper = createPopper(referenceElement, popperElement, {
placement: 'top',
});
state.rects是啥意思呢,簡(jiǎn)單來(lái)說(shuō),就是包含了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來(lái)代替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。
那么問(wèn)題來(lái)了,怎么計(jì)算scale的值呢?有人說(shuō)了,我可以用getComputedStyle獲取到,問(wèn)題來(lái)了,我還可以用直接在css上設(shè)置scale屬性,縮小和放大,我還可以一起上兩個(gè)屬性,你咋辦?
所以我們要用以下的方法
const dom= xxx; //假設(shè)獲取到了某個(gè)dom元素 dom.getBoundingClientRect().width / dom.offsetWith
學(xué)到了吧,我真的強(qiáng)烈大家多看看開(kāi)源的好的代碼,你們自己項(xiàng)目很多前端不可能知道這些細(xì)節(jié)的。
好了,繼續(xù)!請(qǐng)問(wèn)相對(duì)定位的元素如何查找?
有人說(shuō)了,廢話,文章開(kāi)頭不是說(shuō)了嗎,相當(dāng)于offsetParent或者有一些例如css屬性是transform等等屬性的dom元素。
這里面又充滿了坑!
例如,如果是offsetParent是table元素的情況,table元素,并且定位是static的話,我們需要繼續(xù)網(wǎng)上找offsetParent,為啥呢?
如果一個(gè)元素的父元素是一個(gè)table元素,而該元素又沒(méi)有顯式地設(shè)置position屬性,那么該元素的offsetParent會(huì)被設(shè)置為table元素的父元素。
所以通常offsetParent屬性得到的是position是非static的元素,這個(gè)就出現(xiàn)問(wèn)題了?。ㄟ€有一些小細(xì)節(jié),繼續(xù)說(shuō)下去就太多內(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ù),我們跳過(guò),這個(gè)函數(shù)的實(shí)現(xiàn),因?yàn)槲覀冎皇菫榱撕?jiǎn)單介紹中間件是什么。
后面我們會(huì)講生命周期鉤子函數(shù)。
mergeByName簡(jiǎn)單來(lái)說(shuō),就是我們的中間件如下:
[
{
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
上面處理過(guò)后會(huì)把結(jié)果傳給orderModifiers,它會(huì)做兩件事:
- 1、處理依賴
- 2、按照生命周期分層
處理依賴
如何處理依賴呢,我們看到上面的 offset中間件有一個(gè) requires: ['popperOffsets'],意思是offset中間件加載之前,首先要popperOffsets中間件處理。所以我們遇到這種情況就要先加載popperOffsets中間件。
注意,這里并沒(méi)有處理循環(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í),你是否有依賴,有的話,我看看我之前加載過(guò)沒(méi)有,沒(méi)有的話我就先加載依賴。
按照生命周期分層
代碼如下
modifierPhases.reduce((acc, phase) => {
return acc.concat(
orderedModifiers.filter(modifier => modifier.phase === phase)
);
}, []);
modifierPhases是一個(gè)字符串?dāng)?shù)組,這個(gè)數(shù)組的順序就是生命周期鉤子函數(shù)的順序,或者說(shuō)處理中間件的順序。
// 如下的變量看做字符串即可 const modifierPhases = [ beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite, ];
通過(guò)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ù),一般情況是沒(méi)有這個(gè)數(shù)據(jù)的。
最后orderedModifiers也掛載到了state.orderedModifiers上。
簡(jiǎn)而言之,中間件就是把我們定位坐標(biāo)進(jìn)行了變換,或者添加了監(jiān)聽(tīng)事件
比如絕對(duì)定位好的坐標(biāo),如果滾動(dòng)條滾動(dòng),是不是要更新坐標(biāo)才能繼續(xù)定位準(zhǔn)確?
這里主邏輯就解釋完了,然后有人就會(huì)說(shuō)what????沒(méi)有寫(xiě)什么時(shí)候把定位的坐標(biāo)賦給定位元素啊??!
這個(gè)邏輯是寫(xiě)在中間件里的,所以自然而然我們開(kāi)始講中間件。
所有官方中間件都會(huì)講
eventListeners中間件
這個(gè)中間件簡(jiǎn)單來(lái)說(shuō)就是遞歸尋找所有的父元素,如果有滾動(dòng)條的話,就加上scroll事件,然后觸發(fā)更新定位坐標(biāo)。
為啥要更新定位坐標(biāo)上面已經(jīng)說(shuō)的很清楚了唄。
順便再給window事件加上resize事件,這個(gè)也是為了害怕resize窗口導(dǎo)致定位元素定位偏離。
popperOffsets中間件
簡(jiǎn)單來(lái)說(shuō),就是根據(jù)placement計(jì)算坐標(biāo),placement比如是top,就是絕對(duì)定位到某個(gè)元素的上方,而且是居中對(duì)齊。
這個(gè)坐標(biāo)咋算呢,我簡(jiǎn)單說(shuō)下頂部居中對(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ì)算過(guò)的popperOffsets。然后給定義新的定位元素的坐標(biāo)。
有人會(huì)問(wèn)了,為啥要定義新的坐標(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)單來(lái)說(shuō):
如果gpuAcceleration參數(shù)為true,那么我們的定位使用transfrom,否則使用left,top這種方式定位
如果adaptive為true,假設(shè)reference元素變寬或者變窄(比如一段文字),它會(huì)自動(dòng)定位上去
applyStyles中間件
這個(gè)很簡(jiǎn)單,上面我們不是把定位的坐標(biāo)求出來(lái)了嗎,這個(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ì)到視口的下面,如下圖:

為了能看見(jiàn)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'],也就是自己的位置和相反的位置。
然后通過(guò)一個(gè)函數(shù)detectOverflow(建議大家可以單獨(dú)copy一份這個(gè)函數(shù)的代碼,表示是否傳入的元素已經(jīng)超過(guò)視口的)
但是原理也很簡(jiǎn)單,如果我去寫(xiě)的話,就是判斷當(dāng)前元素的最上邊是否超過(guò)定位它的父元素的最上邊,最左邊和其他方向都是一樣的。比較坐標(biāo)嘛。
然后如果超出也很簡(jiǎn)單,你超出了top,你就返回top: true,沒(méi)有就返回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í)踐示例詳解
- 通過(guò)示例源碼解讀React首次渲染流程
- react?express實(shí)現(xiàn)webssh?demo解析
相關(guān)文章
在React中強(qiáng)制重新渲染的4 種方式案例代碼
這篇文章主要介紹了在React中強(qiáng)制重新渲染的4 種方式,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-12-12
react實(shí)現(xiàn)動(dòng)態(tài)選擇框
這篇文章主要為大家詳細(xì)介紹了react實(shí)現(xiàn)動(dòng)態(tài)選擇框,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08
React-Native使用Mobx實(shí)現(xiàn)購(gòu)物車(chē)功能
本篇文章主要介紹了React-Native使用Mobx實(shí)現(xiàn)購(gòu)物車(chē)功能,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09
react實(shí)現(xiàn)組件狀態(tài)緩存的示例代碼
本文主要介紹了react實(shí)現(xiàn)組件狀態(tài)緩存的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02

