前言
作为前端,popper.js
是绕不过去的一道坎,它作为底层组件,帮助我们解决了 Tooltip
、dropdown
、Menu
等等一系列组件。几乎所有的这样的组件都是基于Popper.js
组件来构成的。它的核心只有6000 byte左右,并且支持tree-shaking
,支持自定义中间件、支持任意框架(基于 JavaScript
)。
当前是基于v2.x分支作为分析,仅针对2.x分支分析。最新版本的popper.js已经更名为floating-ui。
本文为接上篇没有说完的续篇,要看完整篇幅请移步这里解谜Popper.js 第一弹
基本逻辑
Modifiers
Modifier[fn]
-
computeStyles
主要计算 popper
、arrow
的 style 样式,开启GPU加速(使用transform),一些误差的磨平。
function computeStyles({ state, options }) {
const {
// gpu加速 默认开启
gpuAcceleration = true,
// 自适应 默认开启
adaptive = true,
// defaults to use builtin `roundOffsetsByDPR`
roundOffsets = true,
} = options;
const commonStyles = {
// top、bottom、start、end
placement: getBasePlacement(state.placement),
// start、end
variation: getVariation(state.placement),
// element
popper: state.elements.popper,
// update中生成的 rect
popperRect: state.rects.popper,
// gpu加速
gpuAcceleration,
isFixed: state.options.strategy === 'fixed',
};
// 在popperOffsets中生成的
if (state.modifiersData.popperOffsets != null) {
state.styles.popper = {
...state.styles.popper,
...mapToStyles({
...commonStyles,
offsets: state.modifiersData.popperOffsets,
position: state.options.strategy,
adaptive,
roundOffsets,
}),
};
}
if (state.modifiersData.arrow != null) {
state.styles.arrow = {
...state.styles.arrow,
...mapToStyles({
...commonStyles,
offsets: state.modifiersData.arrow,
position: 'absolute',
adaptive: false,
roundOffsets,
}),
};
}
state.attributes.popper = {
...state.attributes.popper,
'data-popper-placement': state.placement,
};
}
function mapToStyles({popper,popperRect,placement,variation,offsets,position,gpuAcceleration,adaptive,roundOffsets,isFixed,})
{
// offsets 来自于 popperOffsets 中生成
let { x = 0, y = 0 } = offsets;
const hasX = offsets.hasOwnProperty('x');
const hasY = offsets.hasOwnProperty('y');
let sideX: string = left;
let sideY: string = top;
const win: Window = window;
// 自适应处理逻辑 存在一些 style的变换
if (adaptive) {
// ... some code
}
// 经过自适应处理过后的style
const commonStyles = {
position,
...(adaptive && unsetSides),
};
// 默认为 true 自定义取消误差函数
({ x, y } =
typeof roundOffsets === 'function'
? roundOffsets({ x, y })
: { x, y });
// 取消误差 因为不同分辨率下可能会存在一些误差而导致渲染错位,通常会有<1px的误差
({ x, y } =
roundOffsets === true
? roundOffsetsByDPR({ x, y })
: { x, y });
// 如果开启gpu加速的话 使用transform
if (gpuAcceleration) {
return {
...commonStyles,
[sideY]: hasY ? '0' : '',
[sideX]: hasX ? '0' : '',
// Layer acceleration can disable subpixel rendering which causes slightly
// blurry text on low PPI displays, so we want to use 2D transforms
// instead
transform:
(win.devicePixelRatio || 1) <= 1
? `translate(${x}px, ${y}px)`
: `translate3d(${x}px, ${y}px, 0)`,
};
}
// 不开启gpu加速返回得结果
return {
...commonStyles,
[sideY]: hasY ? `${y}px` : '',
[sideX]: hasX ? `${x}px` : '',
transform: '',
};
}
-
applyStyles
将 popper
、arrow
、reference
等原有的 style 和 attribute 与 computeSytles
中计算出的 style 样式进行合并。
function applyStyles({ state }: ModifierArguments<{||}>) {
// 取出 reference、 popper、arrow 设置 style、attributes等
Object.keys(state.elements).forEach((name) => {
const style = state.styles[name] || {};
const attributes = state.attributes[name] || {};
const element = state.elements[name];
// arrow is optional + virtual elements
if (!isHTMLElement(element) || !getNodeName(element)) {
return;
}
// Flow doesn't support to extend this property, but it's the most
// effective way to apply styles to an HTMLElement
// $FlowFixMe[cannot-write]
Object.assign(element.style, style);
// 针对 attribute 特殊处理
Object.keys(attributes).forEach((name) => {
const value = attributes[name];
if (value === false) {
element.removeAttribute(name);
} else {
element.setAttribute(name, value === true ? '' : value);
}
});
});
}
-
Offsets
注:这里的offsets指的是用户自定义的一些偏移量,而不是计算得出的 popperOffsets
。
function offset({ state, options, name }: ModifierArguments<Options>) {
// options传入的参数 [0,0] 可以理解为用户自定义额外的偏移量
const { offset = [0, 0] } = options;
// 由于接下来的溢出检测,自动切换位置可能会需要其他方向的偏移量,所以这边一次性所有方位全部生成,而避免了每次计算,每次生成。
const data = placements.reduce((acc, placement) => {
// 根据placements生成 每个方位的偏移量
acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);
return acc;
}, {});
const { x, y } = data[state.placement];
if (state.modifiersData.popperOffsets != null) {
state.modifiersData.popperOffsets.x += x;
state.modifiersData.popperOffsets.y += y;
}
// 更新 modifiersData popperOffsets
state.modifiersData[name] = data;
}
-
flip
function flip({ state, options, name }) {
// flip skip
if (state.modifiersData[name]._skip) {
return;
}
//
const {
mainAxis: checkMainAxis = true,
altAxis: checkAltAxis = true,
// 默认可供尝试的placements
fallbackPlacements: specifiedFallbackPlacements,
// 会对boundary边界进行虚拟填充
padding,
// popper
boundary,
// reference
rootBoundary,
// arrow
altBoundary,
// top-start => top-end 尝试翻转变化
flipVariations = true,
allowedAutoPlacements,
} = options;
// custom placement??
const preferredPlacement = state.options.placement;
// 'top-start' => 'top'
const basePlacement = getBasePlacement(preferredPlacement);
const isBasePlacement = basePlacement === preferredPlacement;
// 获取 当发生溢出时,可供尝试的Placements,默认情况下应该为 specifiedFallbackPlacements,
//若没有 specifiedFallbackPlacements 则根据placement来生成fallbackPlacements
const fallbackPlacements =
specifiedFallbackPlacements ||
(isBasePlacement || !flipVariations ?
[getOppositePlacement(preferredPlacement)] :
getExpandedFallbackPlacements(preferredPlacement));
// 生成的placements
const placements = [preferredPlacement, ...fallbackPlacements].reduce(
(acc, placement) => {
return acc.concat(
// 当placement为 'auto'时,自动生成一个可能能用的placement
getBasePlacement(placement) === auto ?
computeAutoPlacement(state, {
placement,
boundary,
rootBoundary,
padding,
flipVariations,
allowedAutoPlacements,
}) :
placement
);
}, []
);
}
fallbackPlacements有关的一些说明,假设我们原有的placement
设置为button
,当没有足够的空间来容纳它时,会使用fallbackPlacements
中的参数来进行尝试。pacements
有下面三种情况:
specifiedFallbackPlacements
参数,则直接使用参数,- 是
basePlacement(top | left | right | bottom)
,则会取反,例如 原来是top
,取反会是bottom
是 top-start
,会先尝试top-end
、bottom-start
、bottom-end
这三种情况。- 是
auto
,则执行computeAutoPlacement
进行计算。
const referenceRect = state.rects.reference;
const popperRect = state.rects.popper;
const checksMap = new Map();
let makeFallbackChecks = true;
let firstFittingPlacement = placements[0];
// 开始生成checksMap
for (let i = 0; i < placements.length; i++) {
const placement = placements[i];
const basePlacement = getBasePlacement(placement);
const isStartVariation = getVariation(placement) === start;
// 方向 x | y
const isVertical = [top, bottom].indexOf(basePlacement) >= 0;
// 检测溢出
const overflow = detectOverflow(state, {
placement,
boundary,
rootBoundary,
altBoundary,
padding,
});
这里的是 经过 detectOverflow
函数,生成了 overflow
,利用 overflow
来判断可用的 placement
。看一下 detectOverflow
是如何检测溢出的。
function detectOverflow(state,option){
const {
placement = state.placement,
boundary = clippingParents,
rootBoundary = viewport,
// flip调用时,没有传入elementContext,为默认值
elementContext = popper,
// true => reference || false => popper
altBoundary = false,
// padding 检测距离,用户传入参数
padding = 0,
} = options;
// 根据padding和 basePlacements生成
// paddingObject,默认是{top:0,left:0,right:0,bottom:0}
// 假设padding = 10,则为 {top:10,left:10,right:10,bottom:10}
const paddingObject = mergePaddingObject(
typeof padding !== 'number'
? padding
: expandToHashMap(padding, basePlacements)
);
// altContext 在flip调用时 默认为 reference
const altContext = elementContext === popper ? reference : popper;
const popperRect = state.rects.popper;
// element 默认为 popper
const element = state.elements[altBoundary ? altContext : elementContext];
// 生成一个clippingclient {width,height,x,y} // 这个clippingclientRect默认情况下指的是父节点的滚动区域大小或者是 viewport大小
// 原有的注释:Gets the maximum area that the element is visible in due to any number of clipping parents
const clippingClientRect = getClippingRect(
isElement(element)
? element
: element.contextElement || getDocumentElement(state.elements.popper),
boundary, // clippingParents
rootBoundary // viewport
);
// 浏览器中的 真正大小, getBoundingClientRect函数重写了element. getBoundingClientRect,其中对页面的缩放及一些element.getBoundingClientRect错误情况做了容错处理。这里可以直接理解为element.getBoundingClientRect的返回值
const referenceClientRect = getBoundingClientRect(state.elements.reference);
// computeOffsets 在popperOffsets中用到过,这里就不做说明了,会生成一个基点坐标
const popperOffsets = computeOffsets({
reference: referenceClientRect,
element: popperRect,
strategy: 'absolute',
placement,
});
根据得到的基点坐标,生成新的 popperClientRect
const popperClientRect = rectToClientRect({
...popperRect,
...popperOffsets,
});
// elementClientRect默认值为 popperClientRect
const elementClientRect =
elementContext === popper ? popperClientRect : referenceClientRect;
// 四个方向检测是否有溢出,如果有溢出的话,例如:top:10,
const overflowOffsets = {
top: clippingClientRect.top - elementClientRect.top + paddingObject.top,
bottom:
elementClientRect.bottom -
clippingClientRect.bottom +
paddingObject.bottom,
left: clippingClientRect.left - elementClientRect.left + paddingObject.left,
right:
elementClientRect.right - clippingClientRect.right + paddingObject.right,
};
// 获取在offsets生成的offsetData
const offsetData = state.modifiersData.offset;
// Offsets can be applied only to the popper element
if (elementContext === popper && offsetData) {
const offset = offsetData[placement];
Object.keys(overflowOffsets).forEach((key) => {
const multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;
const axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';
overflowOffsets[key] += offset[axis] * multiply;
});
}
return overflowOffsets;
}
代码有点复杂,来分析一下detectOverflow
函数做了什么
- 根据
padding
生成paddingObject
, - 判断是检测
popper
还是reference
,生成altContext
- 根据
altContext
,生成clippingClientRect
,该变量默认情况下是父节点滚动区域的大小或者 viewport的可视区域大小,为altContext
可见的最大区域 elementClientRect
为 此时此刻,reference.getBoundingClientRect()
或者popperClientRect
- 生成
overflowOffsets
,举例:top
为clippingClientRect.top
-elementClientRect.top
+paddingObject.top
,正常情况下若可用为负数
最终,我们返回了overflowOffsets
,它包含了4个方位是否有溢出的情况。目前,我们仅分析默认情况,传参情况暂不考虑。
// 分区
// right:top-start、bottom-start
// left:top-end、bottom-end
// bottom:left-start、right-start
// top:left-end、right-end
let mainVariationSide: any = isVertical ?
(isStartVariation ?
right :
left ):
(isStartVariation ?
bottom :
top);
const len = isVertical ? 'width' : 'height';
// 若 reference width | height > popper 取反向,没太想通为什么要取反向
// 刚刚得到的right会变成 left
if (referenceRect[len] > popperRect[len]) {
mainVariationSide = getOppositePlacement(mainVariationSide);
}
const altVariationSide: any = getOppositePlacement(mainVariationSide);
const checks = [];
// 这里需要注意正数为不可用,负数|| 0 为可用,还有空间
if (checkMainAxis) {
checks.push(overflow[basePlacement] <= 0);
}
if (checkAltAxis) {
checks.push(
overflow[mainVariationSide] <= 0,
overflow[altVariationSide] <= 0
);
}
// 若有一个可用的位置
if (checks.every((check) => check)) {
firstFittingPlacement = placement;
makeFallbackChecks = false;
break;
}
checksMap.set(placement, checks);
}
// 如果没有可用的placement
if (makeFallbackChecks) {
// `2` may be desired in some cases – research later
const numberOfChecks = flipVariations ? 3 : 1;
for (let i = numberOfChecks; i > 0; i--) {
const fittingPlacement = placements.find((placement) => {
const checks = checksMap.get(placement);
if (checks) {
return checks.slice(0, i).every((check) => check);
}
});
if (fittingPlacement) {
firstFittingPlacement = fittingPlacement;
break;
}
}
}
// 若placement与预设的不同,则_skip为true。
if (state.placement !== firstFittingPlacement) {
state.modifiersData[name]._skip = true;
state.placement = firstFittingPlacement;
state.reset = true;
}
Flip 是这样,整体执行流程大致为,设置一些placements
,检测popper
四个方向,根据placements
和overflowOffsets
来推算哪个placements可以使用,如果某一个可以使用,结束退出函数。
-
preventOverflow
这部分实在是懒得写了,QAQ。简单介绍一下他的作用,计算出reference在什么情况下应该算溢出,然后记录下这个值,以便后面使用
-
Arrow
function arrow({ state, name, options }: ModifierArguments<Options>) {
//
const arrowElement = state.elements.arrow;
//
const popperOffsets = state.modifiersData.popperOffsets;
// top/bottom/left/right
const basePlacement = getBasePlacement(state.placement);
// top/bottom => x left/right => y
const axis = getMainAxisFromPlacement(basePlacement);
const isVertical = [left, right].indexOf(basePlacement) >= 0;
const len = isVertical ? 'height' : 'width';
// 若无arrow元素或者 无popper的offset
if (!arrowElement || !popperOffsets) {
return;
}
// 边距
const paddingObject = toPaddingObject(options.padding, state);
const arrowRect = getLayoutRect(arrowElement);
const minProp = axis === 'y' ? top : left;
const maxProp = axis === 'y' ? bottom : right;
const endDiff =
state.rects.reference[len] +
state.rects.reference[axis] -
popperOffsets[axis] -
state.rects.popper[len];
const startDiff = popperOffsets[axis] - state.rects.reference[axis];
const arrowOffsetParent = getOffsetParent(arrowElement);
const clientSize = arrowOffsetParent
? axis === 'y'
? arrowOffsetParent.clientHeight || 0
: arrowOffsetParent.clientWidth || 0
: 0;
const centerToReference = endDiff / 2 - startDiff / 2;
// Make sure the arrow doesn't overflow the popper if the center point is
// outside of the popper bounds
const min = paddingObject[minProp];
const max = clientSize - arrowRect[len] - paddingObject[maxProp];
const center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;
const offset = within(min, center, max);
// Prevents breaking syntax highlighting...
const axisProp: string = axis;
state.modifiersData[name] = {
[axisProp]: offset,
centerOffset: offset - center,
};
计算出arrow的位置。 8. ##### hide
function hide({ state, name }: ModifierArguments<{||}>) {
const referenceRect = state.rects.reference;
const popperRect = state.rects.popper;
const preventedOffsets = state.modifiersData.preventOverflow;
// referenceOverflow是否溢出
const referenceOverflow = detectOverflow(state, {
elementContext: 'reference',
});
// popper是否溢出
const popperAltOverflow = detectOverflow(state, {
altBoundary: true,
});
const referenceClippingOffsets = getSideOffsets(
referenceOverflow,
referenceRect
);
const popperEscapeOffsets = getSideOffsets(
popperAltOverflow,
popperRect,
preventedOffsets
);
const isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);
const hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);
state.modifiersData[name] = {
referenceClippingOffsets,
popperEscapeOffsets,
isReferenceHidden,
hasPopperEscaped,
};
state.attributes.popper = {
...state.attributes.popper,
'data-popper-reference-hidden': isReferenceHidden,
'data-popper-escaped': hasPopperEscaped,
};
}
分别判断了,reference
和popper
是否溢出,如果溢出的话,则hide。
End
第二弹到此结束了,有关popper.js仅分为2篇,这是第2篇。该文章仅是我个人的一些理解,难免有一些不对的地方,欢迎大家批评指正!!! 如果可以的话,占用你一些时间,帮我点个赞吧👍🏻👍🏻👍🏻👍🏻👍🏻👍🏻~