Antd 組件庫中,包含有 Trigger 的組件,它們都有相似的組件結構。如下組件均可以歸爲這一類:node
ToolTipreact
Popoverapp
PopConfrimdom
Cascader函數
DataPicker動畫
TimePickerthis
Selectspa
TreeSelectcode
Dropdowncomponent
這些組件的特色,就是都有彈框。經過點擊, hover, 或是改變焦點的方式,打開或關閉彈框。而這個特色,就是 Trigger 組件所要實現的功能:彈框可見性的控制。
先看很黃的這一組,Popover/PopConfirm -> Tooltip -> rcTooltip -> rcTrigger -> child
這類組件,能夠爲彈框傳入自定義的內容或組件。Popover 和 PopConfirm 都是從 Tooltip 整容來的,爲彈窗添加了經常使用的功能。
其中,內部組件 Trigger,負責爲組件添加監聽事件,來控制彈框的可見性。
// rc-trigger\lib\Trigger.js var ALL_HANDLERS = ['onClick', 'onMouseDown', 'onTouchStart', 'onMouseEnter', 'onMouseLeave', 'onFocus', 'onBlur'];
能夠看出,Trigger 支持 onClick, onMouseDown, onTouchStart, onMouseEnter, onFocus 來觸發彈窗,經過 onClick, onMouseLeave, onBlur 來取消彈窗。彈窗的打開與關閉方式能夠不是對應的,例如能夠 mouseEnter 打開,click 關閉。咱們寫彈窗組件的時候,也能把這一部分邏輯抽象出來,感受應該很棒。
當咱們用 React 寫彈窗組件的時候,確定想過要如何添加點擊取消功能。使用遮罩,會有一次無效操做,對選框類彈框就不太好了。Antd 的作法是添加全局監聽事件,經過判斷點擊的元素,是否包含在組件或彈框中,便可判斷是否爲取消彈框的點擊。下面是 Antd 中判斷組件包含性的方法
function contains(root, n) { var node = n; while (node) { if (node === root) { return true; } node = node.parentNode; } return false; }
除了添加監聽事件,Trigger 還有一個很重要的功能,就是彈框的渲染。Trigger 中的彈框並無渲染進正常的文檔流中,它 mixin 了一個 getContainerRenderMixin 內部組件,該組件經過 React 的非穩定方法 unstable_renderSubtreeIntoContainer(),將組件掛載到 document.body 下。由於若是把彈窗渲染在正常文檔流中,組件的定位會受到父元素定位的影響,咱們知道,若是父元素爲靜態定位,定位標的就會變成再父一級元素。將彈窗掛載在 body 下,就能解決這個問題。
雖然彈框脫離了正常文檔流,但其仍受父組件控制。而組件包裹的子組件仍然渲染在正常文檔流。
// rc-util\lib\getContainerRenderMixin2 _reactDom2["default"].unstable_renderSubtreeIntoContainer(instance, component, instance._container, function callback() { instance._component = this; if (ready) { ready.call(this); } });
很綠的一組,也是寄生於 Trigger,但不屬於 Tooltip 一族,它們的彈窗內容被傳入了定義好的組件,定製性更強。從 Trigger 這一層開始,結構都差很少。
藍色表明憂傷,看到這麼多層嵌套,看着是挺憂傷的。Popup -> Animate -> AnimateChild -> rcAlign -> PopupInner -> LazyRenderBox -> content
Popup 組件負責按參數,配置彈框的各類特性,像要不要遮罩、zIndex設多少、隱藏要不要卸載掉彈框等,把這些屬性渲染到功能組件中
Animate 和 AnimateChild,顧名思義,就是搞動畫的。Animate 組件相似於 React 的 ReactTransitionGroup 組件,它會調用子組件中的鉤子函數,來控制動畫效果。
// rc-animate\lib\AnimateChild.js componentWillUnmount: function componentWillUnmount() { this.stop(); }, componentWillEnter: function componentWillEnter(done) { if (_util2["default"].isEnterSupported(this.props)) { this.transition('enter', done); } else { done(); } }, componentWillAppear: function componentWillAppear(done) { if (_util2["default"].isAppearSupported(this.props)) { this.transition('appear', done); } else { done(); } }, componentWillLeave: function componentWillLeave(done) { if (_util2["default"].isLeaveSupported(this.props)) { this.transition('leave', done); } else { done(); } },
AnimateChild 在生命週期的鉤子函數中,經過改變類名,如改變後綴 -appear, -enter, -leave,來執行動畫效果。Antd 動畫主要是 CSS3 動畫。
.zoom-motion(@className, @keyframeName, @duration: @animation-duration-base) { .make-motion(@className, @keyframeName, @duration); .@{className}-enter, .@{className}-appear { transform: scale(0); // need this by yiminghe animation-timing-function: @ease-out-circ; } .@{className}-leave { animation-timing-function: @ease-in-out-circ; } }
Align 組件負責對齊元素。前面說了,Popup 脫離了正常文檔流,掛在了 document.body 上。那如何把兩個風馬牛不相及的元素對齊呢?還要考慮到窗口改變時也能對得上。靠的是元素的可視矩陣(區域)計算,說的挺輕巧,反正代碼我還沒看懂。
// rc-align\node_modules\dom-align\lib\index.js function domAlign(el, refNode, align) { // masive code }
渲染了個div,添了個 LazyRenderBox 功能組件
這個組件很懶,由於你看不見它的時候,它就不幹活了。
shouldComponentUpdate: function shouldComponentUpdate(nextProps) { return nextProps.hiddenClassName || nextProps.visible; },
以前的一沓都是套在外面的衣服,你的 content 纔是用戶想看的。
儘管 React 組件樹很複雜,可是最終渲染出來的 Dom 樹倒是格外的清爽。
Antd 不簡單,看來寫一套靠譜的組件可不是鬧着玩的,一些看似簡單的功能,實現起來倒是十分的複雜。一沓代碼看下來,可能就領悟了皮毛,卻感受已經受益不淺了。