总结 这个组件比想象中的难啃很多
一开始甚至以为就是通过 position: sticky
的方式来控制目标元素定位的,揣测可能是由于兼容性原因放弃使用这种方式?
该组件实现的功能是:元素在滚动到某位置时固定在页面上。
其实现方法是:通过设置目标元素target
(默认是 window
)和offsetTop/offsetBottom
的属性值来控制当前元素滚动到距离目标元素指定位置时,变为固定定位。
1 2 3 4 5 6 | 成员 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | offsetBottom | 距离窗口底部达到指定偏移量后触发 | number | | | offsetTop | 距离窗口顶部达到指定偏移量后触发 | number | | | target | 设置 `Affix` 需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 | () => HTMLElement | () => window | | onChange | 固定状态改变时触发的回调函数 | Function(affixed) | 无 |
代码分析
组件外层包裹ConfigConsumer
组件 ,该组件注入的上下文中提供 getPrefixCls
方法用于计算类名
children
组件外层包裹三个div
,前两个分别是placeholder
和负责固定定位的affix
,placeholder
是用于计算children
的尺寸,一方面提供固定定位时脱离文档流的 affix
大小,一方面保持原有占位大小不变
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Affix { render () { <ConfigConsumer > { ({getPrefixCls}) => { const className = getPrefixCls(...) return ( <div {...props } style ={mergedPlaceholderStyle} ref ={this.savePlaceholderNode} > <div className ={className} ref ={this.saveFixedNode} style ={this.state.affixStyle} > <ResizeObserver onResize ={this.updatePosition} > {children}</ResizeObserver > </div > </div > ) } } </ConfigConsumer > } }
第三个包裹的div
就是ResizeObserver
,其负责在元素resize
时触发对应的 onResize
回调来更新子元素位置,可以看看该组件的写法,使用了比较新的 API:ResizeObserver
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class ReactResizeObserver extends React.Component <ResizeObserverProps , {}> { resizeObserver : ResizeObserver | null = null ; componentDidMount ( ) { this .onComponentUpdated (); } componentDidUpdate ( ) { this .onComponentUpdated (); } componentWillUnmount ( ) { this .destroyObserver (); } onComponentUpdated ( ) { const { disabled } = this .props ; const element = findDOMNode (this ) as DomElement ; if (!this .resizeObserver && !disabled && element) { this .resizeObserver = new ResizeObserver (this .onResize ); this .resizeObserver .observe (element); } else if (disabled) { this .destroyObserver (); } } onResize = () => { const { onResize } = this .props ; if (onResize) { onResize (); } }; destroyObserver ( ) { if (this .resizeObserver ) { this .resizeObserver .disconnect (); this .resizeObserver = null ; } } render ( ) { const { children = null } = this .props ; return children; } }
监听target
元素的事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 componentDidMount ( ) { const { target } = this .props ; if (target) { this .timeout = setTimeout (() => { addObserveTarget (target (), this ); this .updatePosition ({} as Event ); }); } } componentWillUnmount ( ) { clearTimeout (this .timeout ); removeObserveTarget (this ); (this .updatePosition as any ).cancel (); }
另外值得一提的是这个组件用到的装饰器:throttleByAnimationFrameDecorator
装饰器方法的实现文档可以参考文档 ,对类方法装饰时,三个参数分别是 target(class 本身)、key(方法名)、descriptor = Object.getOwnPropertyDescriptor(class, key) 属性描述符
装饰器的目的主要是在 被装饰的方法上注入 requestAnimationFrame()
方法,并利用 raf
的机制进行 节流,raf
返回的 requestId
只有在内部重绘方法被浏览器调用后才会重置为 null
,才有可能进行下一次调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 export default function throttleByAnimationFrame (fn: (...args: any []) => void ) { let requestId : number | null ; const later = (args: any [] ) => () => { requestId = null ; fn (...args); }; const throttled = (...args: any [] ) => { if (requestId == null ) { requestId = raf (later (args)); } }; (throttled as any ).cancel = () => raf.cancel (requestId!); return throttled; } export function throttleByAnimationFrameDecorator ( ) { return function (target: any , key: string , descriptor: any ) { ... } }