原子操作這是Java多線程編程的老生常談了。所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch (切換到另一個線程)。
當然JS是單線程的,所以不存在線程打斷這么一說,我只是從Java中借引了這么一個概念。如果一段JS代碼在執行過程中沒有未知操作被引入,那么這段代碼就是100%可控和安全的,這就是原子操作。反之非原子操作可能會因為外界操作的引入導致代碼變得難以控制而產生隱晦的bug。
下面舉例說明非原子操作可能會帶來的問題
function start() { player = new Player(); player.start(); fireEvent('start'); player.resume(); fireEvent('play'); } function stop() { player.pause(); fireEvent('pause'); player.stop(); player = null; fireEvent('stop'); }
這段代碼中定義兩個方法,start表示開始播放視頻,里面分別有兩段原子操作,在每個原子操作結束之后都向外發送了事件;stop方法類似。代碼看起來簡單而完美,但由於這兩個方法都不是原子操作,所以可能會存在隱患。
下面我們用同樣簡單的方式使用這兩個方法就會產生混亂的結果。
on('start', function(){ stop(); }); start();
這段代碼試圖讓播放器一開始播放就停止,意圖明確。但是它卻會讓實際執行結果變成下面這樣
player = new Player(); player.start(); fireEvent('start'); //監聽start事件后引入的操作 player.pause(); fireEvent('pause'); player.stop(); player = null; fireEvent('stop'); //end player.resume(); fireEvent('play');
這段代碼對外界來說居然在stop事件發生之后還會發生一次play事件,堪稱詭異。
究其原因是因為觸發play事件后引入外部操作導致下一個原子操作所依賴的前提改變。這就是我說的非原子操作的隱患。
那么如何避免這種問題呢,把代碼改成這樣就行
function start() { if (!started) { player = new Player(); player.start(); started = true; fireEvent('start'); } if (started && !played) { player.resume(); played = true; fireEvent('play'); } } function stop() { if (started && played) { player.pause(); played = false; fireEvent('pause'); } if (started) { player.stop(); player = null; started = false; fireEvent('stop'); } }
只需要給每個原子操作加上足夠的前提判斷就可以避免上述問題。
有時候我們無法避免非原子操作,但是我們要認清哪些是原子操作,不要想當然得認為上一個原子操作產生的結果必然會是下一個原子操作的環境。在每個原子操作前加上足夠的判斷。