在上一篇文章Button組件的源碼分析中遇到了一個Wave
組件, Wave
組件在Ant design
中提供了通用的表單控件點擊效果,在本身閱讀源碼以前,也並無過更多留心過在這些表單控件的動畫效果是如何實現的,甚至可能有時都沒注意到這些動畫效果。下面先一塊兒來看如下具體的效果(留意邊框之外,一閃一閃的波浪動畫效果):css
Button
組件node
Radio
組件segmentfault
Switch
組件緩存
看完UI效果以後咱們大概已經知道是什麼了,再看代碼部分,因爲代碼書寫順序與閱讀順序並不一致,爲了方便理解,咱們在分析源碼的過程當中,會調整代碼解釋的順序app
// 一個新的依賴,暫時不知道是什麼,依據名字推測與動畫效果有關 import TransitionEvents from 'css-animation/lib/Event'; export default class Wave extends React.Component<{insertExtraNode?: boolean}> { //... some type code // 咱們發現Wave組件只提供組件邏輯,不參與UI展現,這種容器組件,每每在DidMount或WillMount聲明週期中開始 // 構建組件邏輯,往下看 render() { return this.props.children; } // 只有一行代碼, 先看下方bindAnimationEvent方法 componentDidMount() { this.instance = this.bindAnimationEvent(findDOMNode(this) as HTMLElement); } // 在組件卸載時,清除掉事件監聽與定時器,避免內存泄漏 componentWillUnmount() { if (this.instance) { this.instance.cancel(); } if (this.clickWaveTimeoutId) { clearTimeout(this.clickWaveTimeoutId); } } // 根據名字推測: 爲DOM節點綁定動畫效果,進入函數內部查看 bindAnimationEvent = (node: HTMLElement) => { //... some code const onClick = (e: MouseEvent) => { //... some code // 不管是否正在執行動畫,先清除掉動畫效果(至於怎麼清除,先不關注) this.resetEffect(node); // 從target取得顏色值 const waveColor = getComputedStyle(node).getPropertyValue('border-top-color') || // Firefox Compatible getComputedStyle(node).getPropertyValue('border-color') || getComputedStyle(node).getPropertyValue('background-color'); // 在這裏能夠看到以前定義的私有變量clickWaveTimeoutId,被用做儲存一個定時器 this.clickWaveTimeoutId = window.setTimeout(() => this.onClick(node, waveColor), 0); }; // 監聽node(this.props.children)的onClick事件 node.addEventListener('click', onClick, true); // 將移除監聽事件的回調函數封裝在一個對象中並做爲返回值,看着這裏應該想起以前定義的私有變量instance, // 回顧DidMount生命週期函數,你會發現這個返回的對象將會被儲存在instance中 return { cancel: () => { node.removeEventListener('click', onClick, true); }, }; } //未完待續
咱們經過觀察上方bindAnimationEvent
方法,其主要作了三件事,調用了兩個外部函數this.resetEffect
與 this.onClick
一、過濾不執行的條件(代碼省略掉了,非主幹邏輯)
二、聲明onClick
函數,並做爲node
的點擊事件觸發時要執行的函數
三、返回一個儲存了取消監聽click
事件方法的對象函數
我的認爲bindAnimationEvent
方法,作了太多的事情,而ComponentDidMount
作的事情太少(單一原則)
往下方,繼續查看 this.resetEffect
與 this.onClick
分別是作什麼的,以及如何實現的源碼分析
// 這個函數是實現動畫效果的核心,其主要有三個行爲:一、建立內聯style標籤, 二、插入css字符串 三、並將其插入到document中 // 咱們知道css也是能夠控制DOM變化的,好比僞類元素:after :before 這裏正是經過:after來實現效果的 onClick = (node: HTMLElement, waveColor: string) => { //... some code 1 const { insertExtraNode } = this.props; /* 建立了一個div元素extraNode,裝飾該div,在inserExtracNode= true時,將extraNode做爲node的子元素 */ /* 建立一個div元素,並緩存中私有變量extraNode中 */ this.extraNode = document.createElement('div'); /* 這裏用let 更合適 */ const extraNode = this.extraNode; extraNode.className = 'ant-click-animating-node'; // 可能有人好奇這個extraNode是幹嗎的? // 往以後的代碼中看的時候會發現,在insertExtraNode爲false時,Wave經過插入僞類元素:after 來做爲承載動畫效果的DOM元素 // 在insertExtraNode = true時,會生成一個div替代:after僞類元素,猜想是某些this.props.children可能自帶:after,因此 // 使用div元素來替代:after避免衝突,在這裏咱們只須要知道它是做爲承載動畫css的DOM元素便可 // 獲取指定的string insertExtraNode ? 'ant-click-animating' : 'ant-click-animating-without-extra-node'; const attributeName = this.getAttributeName(); // Element.removeAttribute('someString'); 從element中刪除值爲someString的屬性 // Element.setAttribute(name, value); 爲element元素值爲value的name屬性 node.removeAttribute(attributeName); node.setAttribute(attributeName, 'true'); // 行爲1:這裏建立了一個內聯style標籤 styleForPesudo = styleForPesudo || document.createElement('style'); if (waveColor && waveColor !== '#ffffff' && waveColor !== 'rgb(255, 255, 255)' && this.isNotGrey(waveColor) && /* 透明度不爲0的任意顏色 */ !/rgba\(\d*, \d*, \d*, 0\)/.test(waveColor) && // any transparent rgba color waveColor !== 'transparent') { /* 給子元素加上borderColor */ extraNode.style.borderColor = waveColor; /* 行爲2:在內聯style標籤中插入樣式字符串, 利用僞元素:after做爲承載效果的DOM */ styleForPesudo.innerHTML = `[ant-click-animating-without-extra-node]:after { border-color: ${waveColor}; }`; /* 行爲3:將style標籤插入到document中 */ if (!document.body.contains(styleForPesudo)) { document.body.appendChild(styleForPesudo); } } /* 在inserExtarNode爲true時,將extraNode插入到node子元素中 */ if (insertExtraNode) { node.appendChild(extraNode); } /* 爲元素增長動畫效果 */ TransitionEvents.addEndEventListener(node, this.onTransitionEnd); } /** * 重置效果 * 顧名思義:這個函數經過三個行爲,致力於一件事情,取消動畫效果 * 一、刪除node的attribute屬性 二、node的子元素 三、刪除對應的內聯style標籤 */ resetEffect(node: HTMLElement) { // come code ... const { insertExtraNode } = this.props; const attributeName = this.getAttributeName(); /* 行爲1:刪除node的attribute屬性 */ node.removeAttribute(attributeName); /* 行爲3: 清空了爲僞類元素內置的style標籤 styleForPesudo */ this.removeExtraStyleNode(); if (insertExtraNode && this.extraNode && node.contains(this.extraNode)) { // Node.removeChild() 方法從DOM中刪除一個子節點。返回刪除的節點。 node.removeChild(this.extraNode); } TransitionEvents.removeEndEventListener(node, this.onTransitionEnd); } // 刪除內聯style標籤 removeExtraStyleNode() { if (styleForPesudo) { styleForPesudo.innerHTML = ''; } } // 在每次動畫執行結束後,清除掉狀態,完成一個生命週期 onTransitionEnd = (e: AnimationEvent) => { // todo if (!e || e.animationName !== 'fadeEffect') { return; } this.resetEffect(e.target as HTMLElement); } }
咱們回過頭來看第一部分的代碼,組件邏輯體如今componentDidMount
與 componentWillUnMount
中
一、在componentDidMount
中爲this.props.children
的click
事件綁定動畫執行函數this.onClick
,
二、在componentWillUnMount
中清除與動畫相關的狀態避免內存泄漏。動畫
此時,Wave
組件的代碼與邏輯已經所有分析完了,整個Wave組件的運行邏輯能夠經過下方這張圖來歸納this
本篇完~spa