這個組件是一個圖釘組件,使用的fixed佈局,讓組件固定在窗口的某一個位置上,而且能夠在到達指定位置的時候纔去固定。html
仍是老樣子,看一個組件首先咱們先來看看他能夠傳入什麼參數vue
// Affix export interface AffixProps { /** * 距離窗口頂部達到指定偏移量後觸發 */ offsetTop?: number; offset?: number; /** 距離窗口底部達到指定偏移量後觸發 */ offsetBottom?: number; style?: React.CSSProperties; /** 固定狀態改變時觸發的回調函數 */ onChange?: (affixed?: boolean) => void; /** 設置 Affix 須要監聽其滾動事件的元素,值爲一個返回對應 DOM 元素的函數 */ target?: () => Window | HTMLElement; // class樣式命名空間,能夠定義本身的樣式命名 prefixCls?: string; }
看完傳入參數以後,就到入口函數看看這裏用到了什麼參數react
render() { // 構造當前組件的class樣式 const className = classNames({ [this.props.prefixCls || 'ant-affix']: this.state.affixStyle, }); // 這裏和以前看的同樣,忽略掉props中的在div標籤上面不須要的一些屬性 // 可是貌似沒有去掉offset,後面我還查了一下DIV上面能不能有offset // 可是沒看見有offset,只看見offsetLeft, offsetHeight.... const props = omit(this.props, ['prefixCls', 'offsetTop', 'offsetBottom', 'target', 'onChange']); const placeholderStyle = { ...this.state.placeholderStyle, ...this.props.style }; return ( // 注意咯 看這裏placeholder的做用了 如圖 // 這裏的placeholder的做用是當這個組件樣式變爲fixed的時候, // 會脫離文檔流,而後致使本來的dom結構變化,寬高都會有所變化 // 因此這是後放一個佔位元素來頂住這一組件脫離文檔流的時候的影響 <div {...props} style={placeholderStyle}> <div className={className} ref="fixedNode" style={this.state.affixStyle}> {this.props.children} </div> </div> ); }
接下來是重頭戲,從render函數中咱們應該看到了,控制當前組件的主要因素是兩層div上的style這個屬性,那麼接下來咱們就看看這兩個style是若是構造的git
這個小小的組件卻有不少的代碼,主要都是在處理狀態的代碼,乍一看下來很沒有頭緒,因此就想着從他們的生命週期開始深刻了解,而後在生命週期中果真打開了新的世界,漸漸的理清楚了頭緒,接下來我將帶領你們一同來領略affix組件的風采:es6
// 這裏就先將一些當前生命週期,組件作了什麼吧 // 首先是在Didmount的時候,這時候首先肯定當前的一個固定節點是Window仍是傳入的DOM節點, // 而後利用setTargetEventListeners函數在這個固定節點上加上一些事件, // 而後設置一個當前組件的定時器,目的是但願在組件被銷燬的時候可以將這些事件監聽一併清除 // 敲黑板,你們必定要注意了,本身寫組件的時候若是存在什麼事件監聽的時候必定要在組件銷燬 // 的時候將其一併清除,否則會帶來沒必要要的報錯 componentDidMount() { const target = this.props.target || getDefaultTarget; // Wait for parent component ref has its value this.timeout = setTimeout(() => { this.setTargetEventListeners(target); }); } // 接下來在接收到傳入參數的時候,檢查一下當前的固定節點是否和以前的同樣, // 不同的就從新給節點綁定事件,而且更新當前組件的位置 componentWillReceiveProps(nextProps) { if (this.props.target !== nextProps.target) { this.clearEventListeners(); this.setTargetEventListeners(nextProps.target); // Mock Event object. this.updatePosition({}); } } // 在組件被銷燬的時候清除左右的綁定事件 componentWillUnmount() { this.clearEventListeners(); clearTimeout(this.timeout); (this.updatePosition as any).cancel(); }
在這個三個生命週期中,咱們看見了有這麼幾個函數,setTargetEventListeners
,clearEventListeners
,updatePosition
,
咱們就來看看他們都幹了啥吧github
// 這裏先放一些這些函數須要用到的一些東西 function getTargetRect(target): ClientRect { return target !== window ? target.getBoundingClientRect() : { top: 0, left: 0, bottom: 0 }; } function getOffset(element: HTMLElement, target) { // 這裏的getBoundingClientRect()是一個頗有用的函數,獲取頁面元素位置 /** * document.body.getBoundingClientRect() * DOMRect {x: 0, y: -675, width: 1280, height: 8704, top: -675, …} * */ const elemRect = element.getBoundingClientRect(); const targetRect = getTargetRect(target); const scrollTop = getScroll(target, true); const scrollLeft = getScroll(target, false); const docElem = window.document.body; const clientTop = docElem.clientTop || 0; const clientLeft = docElem.clientLeft || 0; return { top: elemRect.top - targetRect.top + scrollTop - clientTop, left: elemRect.left - targetRect.left + scrollLeft - clientLeft, width: elemRect.width, height: elemRect.height, }; } events = [ 'resize', 'scroll', 'touchstart', 'touchmove', 'touchend', 'pageshow', 'load', ]; eventHandlers = {}; setTargetEventListeners(getTarget) { // 獲得當前固定節點 const target = getTarget(); if (!target) { return; } // 將以前的事件所有清除 this.clearEventListeners(); // 循環給當前固定節點綁定每個事件 this.events.forEach(eventName => { this.eventHandlers[eventName] = addEventListener(target, eventName, this.updatePosition); }); } // 將當前組件中的每個事件移除 clearEventListeners() { this.events.forEach(eventName => { const handler = this.eventHandlers[eventName]; if (handler && handler.remove) { handler.remove(); } }); } // 重點來了,劃重點了,這段代碼很長,可是總的來講是在計算組件和當前的固定節點以前的一個距離 // 在最外層有一個有意思的東西 就是裝飾器,等會咱們能夠單獨醬醬這個裝飾作了啥, // 若是對於裝飾器不是很明白的同窗能夠去搜一下es6的裝飾器語法糖和設計模式中的裝飾器模式 @throttleByAnimationFrameDecorator() updatePosition(e) { // 從props中獲取到須要用到的參數 let { offsetTop, offsetBottom, offset, target = getDefaultTarget } = this.props; const targetNode = target(); // Backwards support // 爲了作到版本兼容,這裏獲取一下偏移量的值 offsetTop = offsetTop || offset; // 獲取到當前固定節點的滾動的距離 //getScroll函數的第一參數是獲取的滾動事件的dom元素 // 第二個參數是x軸仍是y軸上的滾動, y軸上的爲true const scrollTop = getScroll(targetNode, true); // 找到當前組件的Dom節點 const affixNode = ReactDOM.findDOMNode(this) as HTMLElement; // 獲取當前組件Dom節點和當前固定節點的一個相對位置 const elemOffset = getOffset(affixNode, targetNode); // 將當前的節點的寬高設置暫存,等會須要賦值給placeholder的樣式 const elemSize = { width: this.refs.fixedNode.offsetWidth, height: this.refs.fixedNode.offsetHeight, }; // 定義一個固定的模式,頂部仍是底部 const offsetMode = { top: false, bottom: false, }; // Default to `offsetTop=0`. if (typeof offsetTop !== 'number' && typeof offsetBottom !== 'number') { offsetMode.top = true; offsetTop = 0; } else { offsetMode.top = typeof offsetTop === 'number'; offsetMode.bottom = typeof offsetBottom === 'number'; } // 獲取到固定節點的位置信息 const targetRect = getTargetRect(targetNode); // 算出固定節點的高度 const targetInnerHeight = (targetNode as Window).innerHeight || (targetNode as HTMLElement).clientHeight; // 若是滾動條的距離大於組件位置高度減去傳入參數的高度,而且偏移模式爲向上的時候,這時候就是固定在頂部 if (scrollTop > elemOffset.top - (offsetTop as number) && offsetMode.top) { // Fixed Top const width = elemOffset.width; this.setAffixStyle(e, { position: 'fixed', top: targetRect.top + (offsetTop as number), left: targetRect.left + elemOffset.left, width, }); this.setPlaceholderStyle({ width, height: elemSize.height, }); } else if ( // 若是滾動距離小於組件位置高度減去組件高度和傳入參數的高度而且偏移模式爲向下的時候,爲固定在底部 scrollTop < elemOffset.top + elemSize.height + (offsetBottom as number) - targetInnerHeight && offsetMode.bottom ) { // Fixed Bottom const targetBottomOffet = targetNode === window ? 0 : (window.innerHeight - targetRect.bottom); const width = elemOffset.width; this.setAffixStyle(e, { position: 'fixed', bottom: targetBottomOffet + (offsetBottom as number), left: targetRect.left + elemOffset.left, width, }); this.setPlaceholderStyle({ width, height: elemOffset.height, }); } else { const { affixStyle } = this.state; // 若是上面二者都是不的時候,可是若是窗口resize了,那就從新計算,而後賦值給組件 if (e.type === 'resize' && affixStyle && affixStyle.position === 'fixed' && affixNode.offsetWidth) { this.setAffixStyle(e, { ...affixStyle, width: affixNode.offsetWidth }); } else { // 若是以上狀況都不是,那就樣式不變 this.setAffixStyle(e, null); } this.setPlaceholderStyle(null); } }
在上面這一塊代碼中,有幾個函數是外部輔助函數,可是倒是比較有意思的,由於這些輔助函數須要寫的頗有複用性纔有做用,因此正是咱們值得學的地方getScroll()
, throttleByAnimationFrameDecorator裝飾器
,這兩個東西是值得咱們學習的,而且咱們會一塊兒學習裝飾器模式web
這個函數主要是獲取到傳入的dom節點的滾動事件,其中須要講解的是window.document.documentElement
它能夠返回一個當前文檔的一個根節點,詳情能夠查看這裏typescript
export default function getScroll(target, top): number { if (typeof window === 'undefined') { return 0; } // 爲了兼容火狐瀏覽器,因此添加了這一句 const prop = top ? 'pageYOffset' : 'pageXOffset'; const method = top ? 'scrollTop' : 'scrollLeft'; const isWindow = target === window; let ret = isWindow ? target[prop] : target[method]; // ie6,7,8 standard mode if (isWindow && typeof ret !== 'number') { ret = window.document.documentElement[method]; } return ret; }
首先咱們須要知道裝飾器的語法糖,能夠查看這裏json
還有typescript裝飾器segmentfault
接下來咱們還須要知道爲何使用裝飾器,我這裏就是簡單的說一下,裝飾器模式主要就是爲了動態的增減某一個
類的功能而存在的,詳情能夠查看這裏
// '../_util/getRequestAnimationFrame' // 因爲下面的裝飾器還使用了這個文件裏面的函數,因此一併給搬過來了 const availablePrefixs = ['moz', 'ms', 'webkit']; function requestAnimationFramePolyfill() { // 這個函數用來生成一個定時器的或者監聽器ID,若是當前定時器不是window // 上面的requestAnimationFrame那就本身生成一個,用於之後清除定時器使用 let lastTime = 0; return function(callback) { const currTime = new Date().getTime(); const timeToCall = Math.max(0, 16 - (currTime - lastTime)); const id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; } export default function getRequestAnimationFrame() { // 這個函數返回一個定時器或者監聽器ID if (typeof window === 'undefined') { return () => {}; } if (window.requestAnimationFrame) { // https://github.com/vuejs/vue/issues/4465 return window.requestAnimationFrame.bind(window); } // 作了瀏覽器兼容 const prefix = availablePrefixs.filter(key => `${key}RequestAnimationFrame` in window)[0]; return prefix ? window[`${prefix}RequestAnimationFrame`] : requestAnimationFramePolyfill(); } export function cancelRequestAnimationFrame(id) { // 這個函數用來根據ID刪除對應的定時器或者監聽器 if (typeof window === 'undefined') { return null; } if (window.cancelAnimationFrame) { return window.cancelAnimationFrame(id); } const prefix = availablePrefixs.filter(key => `${key}CancelAnimationFrame` in window || `${key}CancelRequestAnimationFrame` in window, )[0]; return prefix ? (window[`${prefix}CancelAnimationFrame`] || window[`${prefix}CancelRequestAnimationFrame`]).call(this, id) : clearTimeout(id); }
import getRequestAnimationFrame, { cancelRequestAnimationFrame } from '../_util/getRequestAnimationFrame'; // 得到一個定時器或者監聽器 const reqAnimFrame = getRequestAnimationFrame(); // 這個函數收到一個函數 返回一個被放入監聽其或者定時器額函數, // 也就是說給這個傳入的函數綁定了一個id,讓他成爲惟一的一個, // 這樣在消除他的時候也很方便 export default function throttleByAnimationFrame(fn) { let requestId; const later = args => () => { requestId = null; fn(...args); }; const throttled = (...args) => { if (requestId == null) { // 獲取定時器或者監聽器ID,將監聽事件傳入 requestId = reqAnimFrame(later(args)); } }; // 給這個函數添加上一個取消的函數 (throttled as any).cancel = () => cancelRequestAnimationFrame(requestId); // 返回構造的新函數 return throttled; } export function throttleByAnimationFrameDecorator() { return function(target, key, descriptor) { // 裝飾器函數,傳入typescript的方法構造器的三個參數 // target: 當前函數(屬性)屬於的類 // key: 當前函數(屬性)名 // dedescriptor: 當前屬性的描述 let fn = descriptor.value; let definingProperty = false; return { configurable: true, // 這裏有一個疑惑 就是這個get()函數是在何時被執行的呢? // 由於從外部看來 這個函數最多隻執行到了上一層的return,這一層的 // 沒有被執行,那麼一下代碼都不會走,可是卻可以調用新函數裏面的屬性。。。 好神奇, // 但願有大神可以在此解說一下 萬分感激 get() { if (definingProperty || this === target.prototype || this.hasOwnProperty(key)) { return fn; } let boundFn = throttleByAnimationFrame(fn.bind(this)); definingProperty = true; // 從新將傳入的函數定義成構造的新函數而且返回 Object.defineProperty(this, key, { value: boundFn, configurable: true, writable: true, }); definingProperty = false; return boundFn; }, }; }; }
下來以後我本身模擬了一下上面的裝飾器,代碼以下,而且經過查詢一些資料,知道了get和set是在何時被調用的
在寫裝飾器代碼的時候須要在tsconfig.json文件中的compilerOptions
屬性添加一下代碼"experimentalDecorators": true
這個get函數會在類被實例化的時候就進行調用,因此就可以將這些屬性賦給外部的target
也就是在this.callDecorator的時候
順帶說一下set函數 會在 this.callDecorator = something 的時候調用
import * as React from 'react'; import * as PropTypes from 'prop-types'; import { MyDecorator } from './Decorator'; export interface DemoProps { helloString?: string; } export default class DecoratorTest extends React.Component<DemoProps, any> { static propTypes = { helloString: PropTypes.string, }; constructor(props) { super(props); } @MyDecorator() callDecorator() { console.log('I am in callDecorator'); } componentDidMount() { this.callDecorator(); (this.callDecorator as any).cancel(); } render() { return ( <div> {this.props.helloString} </div> ); } }
export default function decoratorTest(fn) { console.log('in definingProperty'); const throttled = () => { fn(); }; (throttled as any).cancel = () => console.log('cancel'); return throttled; } export function MyDecorator() { return function(target, key, descriptor) { let fn = descriptor.value; let definingProperty = false; console.log('before definingProperty'); return { configurable: true, // get: function()這樣的寫法也是能夠執行 get() { if (definingProperty || this === target.prototype || this.hasOwnProperty(key)) { return fn; } let boundFn = decoratorTest(fn.bind(this)); definingProperty = true; Object.defineProperty(this, key, { value: boundFn, configurable: true, writable: true, }); definingProperty = false; return boundFn; }, }; }; }
輸順序結果如圖
import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import addEventListener from 'rc-util/lib/Dom/addEventListener'; import classNames from 'classnames'; import shallowequal from 'shallowequal'; import omit from 'omit.js'; import getScroll from '../_util/getScroll'; import { throttleByAnimationFrameDecorator } from '../_util/throttleByAnimationFrame'; function getTargetRect(target): ClientRect { return target !== window ? target.getBoundingClientRect() : { top: 0, left: 0, bottom: 0 }; } function getOffset(element: HTMLElement, target) { const elemRect = element.getBoundingClientRect(); const targetRect = getTargetRect(target); const scrollTop = getScroll(target, true); const scrollLeft = getScroll(target, false); const docElem = window.document.body; const clientTop = docElem.clientTop || 0; const clientLeft = docElem.clientLeft || 0; return { top: elemRect.top - targetRect.top + scrollTop - clientTop, left: elemRect.left - targetRect.left + scrollLeft - clientLeft, width: elemRect.width, height: elemRect.height, }; } function noop() {} function getDefaultTarget() { return typeof window !== 'undefined' ? window : null; } // Affix export interface AffixProps { /** * 距離窗口頂部達到指定偏移量後觸發 */ offsetTop?: number; offset?: number; /** 距離窗口底部達到指定偏移量後觸發 */ offsetBottom?: number; style?: React.CSSProperties; /** 固定狀態改變時觸發的回調函數 */ onChange?: (affixed?: boolean) => void; /** 設置 Affix 須要監聽其滾動事件的元素,值爲一個返回對應 DOM 元素的函數 */ target?: () => Window | HTMLElement; prefixCls?: string; } export default class Affix extends React.Component<AffixProps, any> { static propTypes = { offsetTop: PropTypes.number, offsetBottom: PropTypes.number, target: PropTypes.func, }; scrollEvent: any; resizeEvent: any; timeout: any; refs: { fixedNode: HTMLElement; }; events = [ 'resize', 'scroll', 'touchstart', 'touchmove', 'touchend', 'pageshow', 'load', ]; eventHandlers = {}; constructor(props) { super(props); this.state = { affixStyle: null, placeholderStyle: null, }; } setAffixStyle(e, affixStyle) { const { onChange = noop, target = getDefaultTarget } = this.props; const originalAffixStyle = this.state.affixStyle; const isWindow = target() === window; if (e.type === 'scroll' && originalAffixStyle && affixStyle && isWindow) { return; } if (shallowequal(affixStyle, originalAffixStyle)) { return; } this.setState({ affixStyle }, () => { const affixed = !!this.state.affixStyle; if ((affixStyle && !originalAffixStyle) || (!affixStyle && originalAffixStyle)) { onChange(affixed); } }); } setPlaceholderStyle(placeholderStyle) { const originalPlaceholderStyle = this.state.placeholderStyle; if (shallowequal(placeholderStyle, originalPlaceholderStyle)) { return; } this.setState({ placeholderStyle }); } @throttleByAnimationFrameDecorator() updatePosition(e) { let { offsetTop, offsetBottom, offset, target = getDefaultTarget } = this.props; const targetNode = target(); // Backwards support offsetTop = offsetTop || offset; const scrollTop = getScroll(targetNode, true); const affixNode = ReactDOM.findDOMNode(this) as HTMLElement; const elemOffset = getOffset(affixNode, targetNode); const elemSize = { width: this.refs.fixedNode.offsetWidth, height: this.refs.fixedNode.offsetHeight, }; const offsetMode = { top: false, bottom: false, }; // Default to `offsetTop=0`. if (typeof offsetTop !== 'number' && typeof offsetBottom !== 'number') { offsetMode.top = true; offsetTop = 0; } else { offsetMode.top = typeof offsetTop === 'number'; offsetMode.bottom = typeof offsetBottom === 'number'; } const targetRect = getTargetRect(targetNode); const targetInnerHeight = (targetNode as Window).innerHeight || (targetNode as HTMLElement).clientHeight; if (scrollTop > elemOffset.top - (offsetTop as number) && offsetMode.top) { // Fixed Top const width = elemOffset.width; this.setAffixStyle(e, { position: 'fixed', top: targetRect.top + (offsetTop as number), left: targetRect.left + elemOffset.left, width, }); this.setPlaceholderStyle({ width, height: elemSize.height, }); } else if ( scrollTop < elemOffset.top + elemSize.height + (offsetBottom as number) - targetInnerHeight && offsetMode.bottom ) { // Fixed Bottom const targetBottomOffet = targetNode === window ? 0 : (window.innerHeight - targetRect.bottom); const width = elemOffset.width; this.setAffixStyle(e, { position: 'fixed', bottom: targetBottomOffet + (offsetBottom as number), left: targetRect.left + elemOffset.left, width, }); this.setPlaceholderStyle({ width, height: elemOffset.height, }); } else { const { affixStyle } = this.state; if (e.type === 'resize' && affixStyle && affixStyle.position === 'fixed' && affixNode.offsetWidth) { this.setAffixStyle(e, { ...affixStyle, width: affixNode.offsetWidth }); } else { this.setAffixStyle(e, null); } this.setPlaceholderStyle(null); } } componentDidMount() { const target = this.props.target || getDefaultTarget; // Wait for parent component ref has its value this.timeout = setTimeout(() => { this.setTargetEventListeners(target); }); } componentWillReceiveProps(nextProps) { if (this.props.target !== nextProps.target) { this.clearEventListeners(); this.setTargetEventListeners(nextProps.target); // Mock Event object. this.updatePosition({}); } } componentWillUnmount() { this.clearEventListeners(); clearTimeout(this.timeout); (this.updatePosition as any).cancel(); } setTargetEventListeners(getTarget) { const target = getTarget(); if (!target) { return; } this.clearEventListeners(); this.events.forEach(eventName => { this.eventHandlers[eventName] = addEventListener(target, eventName, this.updatePosition); }); } clearEventListeners() { this.events.forEach(eventName => { const handler = this.eventHandlers[eventName]; if (handler && handler.remove) { handler.remove(); } }); } render() { const className = classNames({ [this.props.prefixCls || 'ant-affix']: this.state.affixStyle, }); const props = omit(this.props, ['prefixCls', 'offsetTop', 'offsetBottom', 'target', 'onChange']); const placeholderStyle = { ...this.state.placeholderStyle, ...this.props.style }; return ( <div {...props} style={placeholderStyle}> <div className={className} ref="fixedNode" style={this.state.affixStyle}> {this.props.children} </div> </div> ); } }