如何判斷一個DOM元素正在動畫,一個CSS“阻塞”JS的例子

範大腳腳發表於2017-10-19

一般情況下CSS不會直接影響JS的程式邏輯,但是以CSS實現動畫的話,這個便不太確定了,這個故事發生在與UED遷移全域性樣式的過程。

曾經我有一段實現彈出層隱藏動畫的程式碼是這個樣子的:

1 if (this.needAnimat && typeof this.animateHideAction == 'function' && this.status != 'hide') {
2   this.animateHideAction.call(this, this.$el);
3 } else
4   this.$el.hide();

在所有元件中,如果設定了animatHideAction回撥的,便會執行其中的動畫邏輯,針對彈出層來說:

① alert

② loading

③ toast

④ 底部彈出層

等元件中動畫效果各不相同:

① 動畫顯示時下沉,隱藏時上浮

② 動畫漸隱漸顯

③ 元件底部彈出

......

針對通用的動畫,一般框架會提供一段CSS類做處理,不滿足的情況,各個業務團隊便需要自己封裝:

 1 cm-fade-in, .cm-fade-out, .cm-down-in, .cm-down-out, .cm-up-in, .cm-up-out {
 2   -webkit-animation-duration: 0.3s;
 3           animation-duration: 0.3s;
 4   -webkit-animation-fill-mode: both;
 5           animation-fill-mode: both;
 6 }
 7 ......
 8 @keyframes fadeOut {
 9   0% {
10     opacity: 1;
11     -webkit-transform: scale(1);
12             transform: scale(1);
13   }
14   100% {
15     opacity: 0;
16     -webkit-transform: scale(1.185);
17             transform: scale(1.185);
18   }
19 }
20 ......

這個時候我們要實現一個居中彈出層漸隱的效果事實上只需要這樣做:

1 el.addClass('cm-fade-out');
2 
3 el.one($.fx.animationEnd, function () {
4   el.removeClass('cm-fade-out');
5   el.hide();
6 });

在動畫結束後將對應的動畫class移除,再執行真實的hide方法,隱藏dom結構。

其實,我記得是去年的時候我是這麼處理這個程式碼的,當時被一個同事罵了不嚴謹,今年就使用了animationEnd介面:

1 el.addClass('cm-fade-out');
2 
3 setTimeout(function () {
4   el.removeClass('cm-fade-out');
5   el.hide();
6 }, 340);

這裡問題來了,使用animationEnd與setTimeout去除動畫class,或者執行業務真實邏輯,到底哪家強,哪個合適?

第一反應都是認為animationEnd比較合理,於是我最近遇到了一個問題:

請求一個資料,loading一直在那裡轉,永遠不消失了!而且執行了hideLoading的操作,與資料延遲毫無關係

於是我開始愉快的定位,當時搞了一會,發現loading的動畫沒有執行,仔細一定位,發現css中的動畫相關的css丟了,於是造成的結果是:

el.addClass('cm-fade-out');

這個程式碼變成了單純的class增加,並沒有執行動畫,也就是,animationEnd的事件沒有觸發,於是沒有執行hide方法,所以loading框就一直在那裡轉

問題定位到了,解決方案就非常簡單了,將css的動畫加上即可;但是也說明了,這段程式碼中JS程式碼邏輯依賴了CSS相關,從而導致了CSS阻塞JS的假象

這裡如果使用setTimeout的話雖然感覺沒有animationEnd嚴謹,但是一定會保證這邏輯程式碼執行,從某種程度來說,似乎更好,這裡的優化程式碼是:

 1 var isTrigger = false;
 2 
 3 el.addClass(scope.animateOutClass);
 4 
 5 el.one($.fx.animationEnd, function () {
 6   isTrigger = true;
 7   el.removeClass(scope.animateOutClass);
 8   el.hide();
 9 });
10 
11 setTimeout(function () {
12   if (isTrigger) return;
13 
14   el.removeClass(scope.animateOutClass);
15   el.off($.fx.animationEnd);
16   el.hide();
17 }, 350);

如果animationEnd執行了便不理睬setTimeout,否則便走setTimeout邏輯,也不至於影響業務邏輯,但是這個似乎不是最優解決方案。

因為我沒有辦法,因為這裡得有350ms的延遲,在不存在css動畫的時候,似乎整個彈出層消失邏輯都變得2B了起來,比較好的方式是,我在執行動畫前檢測是否具有該css比較靠譜

所以,javascript檢測CSS的某一個className是否存在,似乎變成了關鍵,但是就算就算能找到具有某class,這個class也未必具有動畫屬性,或者該屬性被篡改

況且使用document.styleSheets方式去判斷某個樣式class是否存在,經過之前的經驗,本身就是大坑,還會有跨域什麼的場景,坑死人,比如這個程式碼:

 1 function getAllSelectors() {
 2   var ret = [];
 3   for (var i = 0; i < document.styleSheets.length; i++) {
 4     var rules = document.styleSheets[i].rules || document.styleSheets[i].cssRules;
 5     for (var x in rules) {
 6       if (typeof rules[x].selectorText == 'string') ret.push(rules[x].selectorText);
 7     }
 8   }
 9   return ret;
10 }
11 
12 function selectorExists(selector) {
13   var selectors = getAllSelectors();
14   for (var i = 0; i < selectors.length; i++) {
15     if (selectors[i] == selector) return true;
16   }
17   return false;
18 }
19 
20 //呼叫方式
21 selectorExists('.class');
22 selectorExists('#id');

上面的程式碼,本身比較完善了,但是如果某一個css檔案跨域的話就完蛋,所以這個方案不靠譜:

① class檢測方案本身不靠譜

② 就算class靠譜,也不能保證class就具有動畫相關屬性,所以也不靠譜!

最終我想到的方案還是對動畫屬性做檢測,檢測點主要在動畫屬性的檢測,比如關鍵屬性:

① animation-name

② transition的檢測

  1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2 <html xmlns="http://www.w3.org/1999/xhtml">
  3 <head>
  4   <title></title>
  5   <script id="others_zepto_10rc1" type="text/javascript" class="library" src="http://sandbox.runjs.cn/js/sandbox/other/zepto.min.js"></script>
  6   <style>
  7    
  8 .cm-fade-in {
  9   -webkit-animation-name: fadeIn;
 10           animation-name: fadeIn;
 11 }
 12 
 13 .cm-fade-out {
 14   -webkit-animation-name: fadeOut;
 15           animation-name: fadeOut;
 16 }
 17 
 18 @-webkit-keyframes fadeIn {
 19   0% {
 20     opacity: 0;
 21     -webkit-transform: scale(0.815);
 22             transform: scale(0.815);
 23   }
 24   100% {
 25     opacity: 1;
 26     -webkit-transform: scale(1);
 27             transform: scale(1);
 28   }
 29 }
 30 
 31 @keyframes fadeIn {
 32   0% {
 33     opacity: 0;
 34     -webkit-transform: scale(0.815);
 35             transform: scale(0.815);
 36   }
 37   100% {
 38     opacity: 1;
 39     -webkit-transform: scale(1);
 40             transform: scale(1);
 41   }
 42 }
 43 @-webkit-keyframes fadeOut {
 44   0% {
 45     opacity: 1;
 46     -webkit-transform: scale(1);
 47             transform: scale(1);
 48   }
 49   100% {
 50     opacity: 0;
 51     -webkit-transform: scale(1.185);
 52             transform: scale(1.185);
 53   }
 54 }
 55 @keyframes fadeOut {
 56   0% {
 57     opacity: 1;
 58     -webkit-transform: scale(1);
 59             transform: scale(1);
 60   }
 61   100% {
 62     opacity: 0;
 63     -webkit-transform: scale(1.185);
 64             transform: scale(1.185);
 65   }
 66 }
 67 .cm-down-in {
 68   -webkit-animation-name: downIn;
 69           animation-name: downIn;
 70 }
 71 
 72 .cm-down-out {
 73   -webkit-animation-name: downOut;
 74           animation-name: downOut;
 75 }
 76 
 77 @-webkit-keyframes downIn {
 78   0% {
 79     opacity: 0;
 80     -webkit-transform: translate3d(0, 100%, 0);
 81             transform: translate3d(0, 100%, 0);
 82   }
 83   100% {
 84     opacity: 1;
 85     -webkit-transform: translate3d(0, 0, 0);
 86             transform: translate3d(0, 0, 0);
 87   }
 88 }
 89 
 90 @keyframes downIn {
 91   0% {
 92     opacity: 0;
 93     -webkit-transform: translate3d(0, 100%, 0);
 94             transform: translate3d(0, 100%, 0);
 95   }
 96   100% {
 97     opacity: 1;
 98     -webkit-transform: translate3d(0, 0, 0);
 99             transform: translate3d(0, 0, 0);
100   }
101 }
102 @-webkit-keyframes downOut {
103   0% {
104     opacity: 1;
105     -webkit-transform: translate3d(0, 0, 0);
106             transform: translate3d(0, 0, 0);
107   }
108   100% {
109     opacity: 0;
110     -webkit-transform: translate3d(0, 100%, 0);
111             transform: translate3d(0, 100%, 0);
112   }
113 }
114 @keyframes downOut {
115   0% {
116     opacity: 1;
117     -webkit-transform: translate3d(0, 0, 0);
118             transform: translate3d(0, 0, 0);
119   }
120   100% {
121     opacity: 0;
122     -webkit-transform: translate3d(0, 100%, 0);
123             transform: translate3d(0, 100%, 0);
124   }
125 }
126 .cm-up-in {
127   -webkit-animation-name: upIn;
128           animation-name: upIn;
129 }
130 
131 .cm-up-out {
132   -webkit-animation-name: upOut;
133           animation-name: upOut;
134 }
135   </style>
136 </head>
137 <body>
138   <script type="text/javascript">
139     var hasAnimationProperty = function (className) {
140       var animateProprtys = [
141       //有什麼判斷的便新增,暫時只判斷animation,不同的動畫特性,判斷方式不一致
142       //        $.fx.cssPrefix + 'transition',
143         $.fx.cssPrefix + 'animation-name'
144       ];
145       var el = $('<div></div>');
146       $('body').append(el);
147 
148       var i, len;
149 
150       //賦予其class
151       el.attr('class', className);
152 
153       for (i = 0, len = animateProprtys.length; i < len; i++) {
154         if (el.css(animateProprtys[i]) != 'none') return true;
155       }
156       s = '';
157       return false;
158     };
159 
160     //false
161     console.log(hasAnimationProperty('test'));
162     //true
163     console.log(hasAnimationProperty('cm-up-out'));
164     //true
165     console.log(hasAnimationProperty('cm-up-in'));
166 
167   </script>
168 </body>
169 </html>
View Code

核心程式碼:

 1 var hasAnimationProperty = function (className) {
 2   var animateProprtys = [
 3   //有什麼判斷的便新增,暫時只判斷animation,不同的動畫特性,判斷方式不一致
 4   //        $.fx.cssPrefix + 'transition',
 5     $.fx.cssPrefix + 'animation-name'
 6   ];
 7   var el = $('<div></div>');
 8   $('body').append(el);
 9 
10   var i, len;
11 
12   //賦予其class
13   el.attr('class', className);
14 
15   for (i = 0, len = animateProprtys.length; i < len; i++) {
16     if (el.css(animateProprtys[i]) != 'none') return true;
17   }
18   s = '';
19   return false;
20 };
21 
22 //false
23 console.log(hasAnimationProperty('test'));
24 //true
25 console.log(hasAnimationProperty('cm-up-out'));
26 //true
27 console.log(hasAnimationProperty('cm-up-in'));

如此一來,便能判斷該class是否具有樣式屬性了,但是這個程式碼還需要擴充套件,而且這麼也有效能損害,其中涉及到dom操作了,但是想想動畫造成到gpu負擔,好像也沒什麼問題

相關文章