欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

react?定位組件源碼解析

 更新時間:2023年03月28日 09:13:06   作者:孟祥_成都  
這篇文章主要為大家介紹了react定位組件源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

正文

react組件庫系列耽擱了一些時間,繼續(xù)了!定位組件在b端里面實在太常見了。

首先介紹一下什么是定位組件,如下圖:

也就是上面的“這是一個彈窗的div”其實是絕對定位到按鈕上的,因為是絕對定位,所以可以定位到任意元素上。

整體源碼來自于t-design的popup組件

但是他們也是二次封裝了react-popper,react-popper又依賴@popperjs/core,所以這篇文章主要解決的是@popperjs/core的實現(xiàn)原理。

計劃:

  • 本篇是@popperjs/core的實現(xiàn)核心原理
  • 第二篇講 react-popper實現(xiàn)核心原理
  • 最后一篇是t-designpopup實現(xiàn)原理。

講源碼之前,跟大家亮點好玩的知識點:

里面有很多實用的工具函數(shù),說實話,原以為自己原生dom api掌握還挺熟練的,看了這些兼容性的代碼,我才知道自己有多無知,舉個例子,你們一個元素的position是absolute,那么它是相當(dāng)于誰定位?例如:

  <body>
    <div>
      網(wǎng)二
    </div>
    <div style="transform: translateX(2px);">
      <span style="position: absolute; top: 0" >李四</span>
  </div>
  </body>

肯定有人說了,這個我熟啊,相當(dāng)于上面包含它的元素只要不是static定位的。這個沒錯,但是只答對一部分,還有一種可能,本身元素是static元素也會成為定位上下文,比如給它加一個transform屬性,你可以試試上面的代碼,李四是相對于transform屬性的div定位的。

不僅僅是transform屬性,下面的方式都可以成定位上下文元素(當(dāng)時看源碼這里我是怎么也不明白為啥要判斷下面這些)

  • 有transform、perspective、filter屬性中任意一個不為none。
  • 是will-change屬性值為以上任意一個屬性的祖先元素。
  • 是contain屬性值包含paint、layout、strict或content的祖先元素。

正文開始!

如何創(chuàng)建基礎(chǔ)的popper

使用createPopper API,我們先看看是怎么用的:

假設(shè)我們有這樣一個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ù),需要傳遞兩個參數(shù):參考元素(reference element)和彈出式元素(popper element),以及一個可選的配置對象。參考元素是要彈出的元素的參照點,而彈出式元素是要彈出的元素本身。

例如,以下是一個創(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ù)接收參考元素和彈出式元素,以及一個配置對象。這里的配置對象中指定了彈出式元素的位置,它將出現(xiàn)在參考元素的上方。

popper是一個對象,其中的方法用于進一步管理和控制彈出式元素的行為。例如,可以使用update函數(shù)在參考元素或彈出式元素發(fā)生變化時重新計算彈出式元素的位置和大小,或者可以使用destroy函數(shù)在不需要彈出式元素時將其刪除。

createPopper函數(shù)的返回值

包含以下屬性和方法的對象:

  • state:一個對象,包含有關(guān)彈出式元素的位置、大小和其他信息的狀態(tài)信息。
  • update:一個函數(shù),用于更新彈出式元素的位置和大小。
  • forceUpdate:一個函數(shù),用于強制更新彈出式元素的位置和大小。
  • destroy:一個函數(shù),用于刪除彈出式元素并清除其事件偵聽器。
  • setOptions: 這個函數(shù)用于更新彈出式元素的配置選項。

上面我們的案例是如何實現(xiàn)自動定位的呢,也就是createPopper函數(shù)調(diào)用后,

const popper = createPopper(referenceElement, popperElement, {
  placement: 'top',
});

popperElement自動定位到referenceElement元素的上面了,原因就是createPopper在執(zhí)行過程中,調(diào)用了setOptions方法,所以我們只要看一下setOptions方法如何實現(xiàn),就知道了它如何實現(xiàn)自動定位了。

注:這里我們不加入任何中間件,這樣會提高復(fù)雜度,后面再講幾個典型的中間件實現(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是啥意思呢,簡單來說,就是包含了reference的getBoundingClientRect的結(jié)果,popper也是包含了reference的getBoundingClientRect的結(jié)果。

這里再簡單講一下 getBoundingClientRect是什么。

含義:

方法返回元素的大小及其相對于視口的位置。

值:

返回值是一個 DOMRect 對象,這個對象是由該元素的 getClientRects() 方法返回的一組矩形的集合, 即:是與該元素相關(guān)的CSS 邊框集合。

屬性值:

  • top: 元素上邊距離頁面上邊的距離
  • left: 元素右邊距離頁面左邊的距離
  • right: 元素右邊距離頁面左邊的距離
  • bottom: 元素下邊距離頁面上邊的距離
  • width: 元素寬度
  • height: 元素高度

為什么需要這些屬性呢?你想想,我絕對定位某個元素,我知道了另一個元素的坐標,是不是絕對定位上去就很簡單了?

這也是定位組件最最最基本的思想,所有的定位組件都差不多。

接著講:

forceUpdate() {
        const { reference, popper } = state.elements;
        state.rects = {
          reference: getCompositeRect(
            reference,
            getOffsetParent(popper),
            state.options.strategy === 'fixed'
          ),
          popper: getLayoutRect(popper),
        };
}

為什么這里要用getCompositeRect來代替getBoundingClientRect的功能,其實里面主要也是用了getBoundingClientRect的功能。

最主要的區(qū)別就是一些非常細節(jié)的處理了:

絕對定位的坐標受到transfrom: scale的影響

我們舉個例子:

  <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)變化,那么你定位的時候,是不是要找出scale的值是2.5,然后在正常scale(1)的情況下,決定定位的x,y,width,height都要乘以2.5。

那么問題來了,怎么計算scale的值呢?有人說了,我可以用getComputedStyle獲取到,問題來了,我還可以用直接在css上設(shè)置scale屬性,縮小和放大,我還可以一起上兩個屬性,你咋辦?

所以我們要用以下的方法

const dom= xxx; //假設(shè)獲取到了某個dom元素
dom.getBoundingClientRect().width / dom.offsetWith

學(xué)到了吧,我真的強烈大家多看看開源的好的代碼,你們自己項目很多前端不可能知道這些細節(jié)的。

好了,繼續(xù)!請問相對定位的元素如何查找?

有人說了,廢話,文章開頭不是說了嗎,相當(dāng)于offsetParent或者有一些例如css屬性是transform等等屬性的dom元素。

這里面又充滿了坑!

例如,如果是offsetParent是table元素的情況,table元素,并且定位是static的話,我們需要繼續(xù)網(wǎng)上找offsetParent,為啥呢?

如果一個元素的父元素是一個table元素,而該元素又沒有顯式地設(shè)置position屬性,那么該元素的offsetParent會被設(shè)置為table元素的父元素。

所以通常offsetParent屬性得到的是position是非static的元素,這個就出現(xiàn)問題了?。ㄟ€有一些小細節(jié),繼續(xù)說下去就太多內(nèi)容了)

計算相當(dāng)于最近的offsetParent元素,如何計算絕對定位的值

源碼核心如下:

 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我們簡單看做是window元素,然后獲取的是滾動條的滾動距離,offsets.x是offsetParent的left + offsetParent.clientLeft的和。

為什么這么算呢,你看啊,reference.left - offsetParent.x(可以認為是offsetParent.left),得到的是reference元素左側(cè)到offsetParent元素左側(cè)的距離。

然后上面得到的值加上滾動條距離,是不是就是renference元素在offsetParent中的坐標了。

注意,源碼里的絕對定位,雖然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是什么意思,主要是把所有中間件合并了,一個中間件長啥樣呢,如下:

{
  name: 'offset',
  enabled: true,
  phase: 'main',
  requires: ['popperOffsets'],
  fn: offset,
}

上面一個命名為offset的中間件,處理的生命周期在'main'這個生命周期中。處理這個offset的中間件函數(shù)是fn屬性里的offset函數(shù),我們跳過,這個函數(shù)的實現(xiàn),因為我們只是為了簡單介紹中間件是什么。

后面我們會講生命周期鉤子函數(shù)。

mergeByName簡單來說,就是我們的中間件如下:

[
{
  name: 'offset',
  enabled: true,
  phase: 'main',
  requires: ['popperOffsets'],
  fn: offset,
},
{
  name: 'offset',
  enabled: true,
  phase: 'main',
  requires: ['popperOffsets'],
  fn: offset1,
}
]

也就是可能有重名的中間件,然后將他們合并,我們看到上面數(shù)組第一個元素的fn是offset,第二個是offset1,此時offset1就會覆蓋offset,也就是所有中間件最終只能有一個名字唯一的去處理它。

最終合并為

[
{
  name: 'offset',
  enabled: true,
  phase: 'main',
  requires: ['popperOffsets'],
  fn: offset1,
}
]

orderModifiers

上面處理過后會把結(jié)果傳給orderModifiers,它會做兩件事:

  • 1、處理依賴
  • 2、按照生命周期分層

處理依賴

如何處理依賴呢,我們看到上面的 offset中間件有一個 requires: ['popperOffsets'],意思是offset中間件加載之前,首先要popperOffsets中間件處理。所以我們遇到這種情況就要先加載popperOffsets中間件。

注意,這里并沒有處理循環(huán)依賴的情況,需要使用者自己注意(循環(huán)依賴最終會報錯,因為肯定會棧溢出)。

我們簡單看下這個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ù)會判斷在加載某個中間件時,你是否有依賴,有的話,我看看我之前加載過沒有,沒有的話我就先加載依賴。

按照生命周期分層

代碼如下

modifierPhases.reduce((acc, phase) => {
    return acc.concat(
      orderedModifiers.filter(modifier => modifier.phase === phase)
    );
  }, []);

modifierPhases是一個字符串?dāng)?shù)組,這個數(shù)組的順序就是生命周期鉤子函數(shù)的順序,或者說處理中間件的順序。

// 如下的變量看做字符串即可
const modifierPhases = [
  beforeRead,
  read,
  afterRead,
  beforeMain,
  main,
  afterMain,
  beforeWrite,
  write,
  afterWrite,
];

通過reduce函數(shù),會先處理數(shù)組靠前的名字的中間件。

我們接著看剛才的中間件處理流程:

const orderedModifiers = orderModifiers(
  mergeByName([...defaultModifiers, ...state.options.modifiers])
);
// Strip out disabled modifiers
state.orderedModifiers = orderedModifiers.filter((m) =&gt; m.enabled);
state.orderedModifiers.forEach(
  (modifier) =&gt;
    (state.modifiersData[modifier.name] = {
      ...modifier.data,
    })
);

orderedModifiers我們之前介紹了,接著state抽取了所有orderedModifiers中的data數(shù)據(jù),一般情況是沒有這個數(shù)據(jù)的。

最后orderedModifiers也掛載到了state.orderedModifiers上。

簡而言之,中間件就是把我們定位坐標進行了變換,或者添加了監(jiān)聽事件

比如絕對定位好的坐標,如果滾動條滾動,是不是要更新坐標才能繼續(xù)定位準確?

這里主邏輯就解釋完了,然后有人就會說what????沒有寫什么時候把定位的坐標賦給定位元素啊?。?/p>

這個邏輯是寫在中間件里的,所以自然而然我們開始講中間件。

所有官方中間件都會講

eventListeners中間件

這個中間件簡單來說就是遞歸尋找所有的父元素,如果有滾動條的話,就加上scroll事件,然后觸發(fā)更新定位坐標。

為啥要更新定位坐標上面已經(jīng)說的很清楚了唄。

順便再給window事件加上resize事件,這個也是為了害怕resize窗口導(dǎo)致定位元素定位偏離。

popperOffsets中間件

簡單來說,就是根據(jù)placement計算坐標,placement比如是top,就是絕對定位到某個元素的上方,而且是居中對齊。

這個坐標咋算呢,我簡單說下頂部居中對齊,大家自己算就行了

  const commonX = reference.x + reference.width / 2 - element.width / 2;
    case top:
      offsets = {
        x: commonX,
        y: reference.y - element.height,
      };

最后得到的定位坐標放到了state屬性上,如下:

state.modifiersData[popperOffsets] = 定位坐標

computeStyle中間件

如果state上的popperOffsets屬性不為null,也就是我們上面計算過的popperOffsets。然后給定義新的定位元素的坐標。

有人會問了,為啥要定義新的坐標

  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這個處理函數(shù)。我們看下它的實現(xiàn),簡單來說:

  • 如果gpuAcceleration參數(shù)為true,那么我們的定位使用transfrom,否則使用left,top這種方式定位

  • 如果adaptive為true,假設(shè)reference元素變寬或者變窄(比如一段文字),它會自動定位上去

applyStyles中間件

這個很簡單,上面我們不是把定位的坐標求出來了嗎,這個中間件就是把定位組件的styles屬性合并上去的,源碼如下,element就是定位元素,這種方式值得大家學(xué)習(xí),而不是直接賦值給style。

    const attributes = state.attributes[name] || {};
    Object.assign(element.style, style);

offset中間件

這個太簡單了,偏移距離用的,請看下圖:

flip中間件

原理是,比如我們現(xiàn)在placement:bottom,表示定位到reference元素的下方,當(dāng)我們向下滾動的時候,是不是這個定位的元素因為在下方,遲早會到視口的下面,如下圖:

為了能看見tooltip,我們自動翻轉(zhuǎn)到上方!

這就是flip的功能,至于如何實現(xiàn),我們馬上分析:

假設(shè)我們傳入的placement是bottom,會自動計算它相反的位置:最后生成['bottom','top'],意思是如果bottom超出視口邊界,就轉(zhuǎn)到top的位置去。

這個位置我們還可以外界自定義,默認的是placement是top,那么就生成['top','bottom'],如果是left就生成['left', 'right'],也就是自己的位置和相反的位置。

然后通過一個函數(shù)detectOverflow(建議大家可以單獨copy一份這個函數(shù)的代碼,表示是否傳入的元素已經(jīng)超過視口的)

但是原理也很簡單,如果我去寫的話,就是判斷當(dāng)前元素的最上邊是否超過定位它的父元素的最上邊,最左邊和其他方向都是一樣的。比較坐標嘛。

然后如果超出也很簡單,你超出了top,你就返回top: true,沒有就返回top:false,我知道如果超出不就馬上計算另一個方向的坐標了嗎

以上就是react 定位組件源碼解析的詳細內(nèi)容,更多關(guān)于react 定位組件的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • react項目中使用插件配置路由的方法

    react項目中使用插件配置路由的方法

    這篇文章主要介紹了react項目中使用插件配置路由的相關(guān)知識,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-03-03
  • ahooks?useRequest源碼精讀解析

    ahooks?useRequest源碼精讀解析

    這篇文章主要為大家介紹了ahooks?useRequest的源碼精讀解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-07-07
  • 在React中強制重新渲染的4 種方式案例代碼

    在React中強制重新渲染的4 種方式案例代碼

    這篇文章主要介紹了在React中強制重新渲染的4 種方式,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧
    2023-12-12
  • 如何強制刷新react hooks組件

    如何強制刷新react hooks組件

    這篇文章主要介紹了如何強制刷新react hooks組件問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-11-11
  • React state狀態(tài)屬性詳細講解

    React state狀態(tài)屬性詳細講解

    React將組件(component)看成一個狀態(tài)機(State Machines),通過其內(nèi)部自定義的狀態(tài)(State)和生命周期(Lifecycle)實現(xiàn)并與用戶交互,維持組件的不同狀態(tài)
    2022-09-09
  • react實現(xiàn)動態(tài)選擇框

    react實現(xiàn)動態(tài)選擇框

    這篇文章主要為大家詳細介紹了react實現(xiàn)動態(tài)選擇框,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-08-08
  • React-Native使用Mobx實現(xiàn)購物車功能

    React-Native使用Mobx實現(xiàn)購物車功能

    本篇文章主要介紹了React-Native使用Mobx實現(xiàn)購物車功能,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-09-09
  • react實現(xiàn)組件狀態(tài)緩存的示例代碼

    react實現(xiàn)組件狀態(tài)緩存的示例代碼

    本文主要介紹了react實現(xiàn)組件狀態(tài)緩存的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-02-02
  • React學(xué)習(xí)筆記之條件渲染(一)

    React學(xué)習(xí)筆記之條件渲染(一)

    條件渲染在React里就和js里的條件語句一樣。下面這篇文章主要給大家介紹了關(guān)于React學(xué)習(xí)記錄之條件渲染的相關(guān)資料,文中介紹的非常詳細,對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。
    2017-07-07
  • 對react中間件的理解

    對react中間件的理解

    這篇文章主要介紹了對react中間件的理解,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-11-11

最新評論