requestAnimationFrame() 大多數電腦顯示器的刷新頻率60HZ,大概相當於每秒鐘重繪60次。因此,最平滑動畫的最佳迴圈間隔是1000ms/60,約等於17ms。 mozRequestAnimationFrame() mozRequestAnimationFrame()方法接收一個 ...
requestAnimationFrame()
大多數電腦顯示器的刷新頻率60HZ,大概相當於每秒鐘重繪60次。因此,最平滑動畫的最佳迴圈間隔是1000ms/60,約等於17ms。
mozRequestAnimationFrame()
mozRequestAnimationFrame()方法接收一個參數,即在重繪屏幕前調用的一個函數。這個函數負責改變下一次重繪時的DOM樣式。為了創建動畫迴圈,可以像以前使用setTimeout()方法一樣,把多個對mozRequestAnimationFrame()的調用連綴起來,如下代碼:
function updateProgress(){ var div = document.getElementById("status"); div.style.width = (parseInt(div.style.width,10) + 5) + "%"; if(div.style.width != "100%"){ mozRequestAnimationFrame(updateProgress); } } mozRequestAnimationFrame(updateProgress);
我們傳遞的mozRequestAnimationFrame()函數也會接收一個參數,它是一個時間碼(從1970年1月1日起至今的毫秒數),表示下一次重繪的實際發生時間。
註意:mozRequestAnimationFrame()會根據這個時間碼設定將來的某個時刻進行重繪,而根據這個時間碼,你也能知道那個時刻是什麼時間。然後,在優化動畫效果就有了依據。
要知道距離上一次重繪已經過去了多長時間,可以查詢mozAnimationStartTime,其中包含上次重繪的時間碼。用傳入回調函數的時間碼減去這個時間碼,就能計算出在屏幕上重繪下一組變化之前要經過多長時間。使用這個值的典型方式:
function draw(timestamp){ //計算兩次重繪的事件間隔 var diff = timestamp - startTime; //使用diff確定下一步的繪製時間 //把startTime重寫為這一次的繪製時間 startTime = timestamp; //重繪UI mozRequestAnimationFrame(draw); } var startTime = mozAnimationStartTime; mozRequestAnimationFrame(draw);
webkitRequestAnimationFrame與msRequestAnimationFrame
Chrome和IE10+也都給出了自己的實現,分別是webkitRequestAnimationFrame()和msRequestAnimationFrame()。這兩個版本跟mozilla的版本有兩個方面的微小差異。
- 首先,不會給回調函數傳遞時間碼,因此你無法知道下一次重繪將發生在什麼時間;
- 其次,Chrome又增加了第二個可選的參數,即將要發生變化的DOM元素。知道了重繪將發生在頁面中哪個特定元素的區域內,就可以將重繪限定在該區域中。
既然沒有下一次重繪的時間碼,那麼就沒有提供像mozAnimationStartTime的實現,不過,Chrome提供了另一個方法webkitCancelAnimationFrame(),用於取消之前計劃執行的重繪操作。
假如你不需要知道精確的時間差,可以參考以下模式創建動畫迴圈:
(function(){ function draw(timestamp){ //計算兩次重繪的時間間隔 var drawStart = timestamp || Date.now(), diff = drawStart - startTime; //使用diff確定下一步的繪製時間 //把startTime重寫為這一次的繪製時間 startTime = drawStart; //重繪UI requestAnimationFrame(draw); } var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame, startTime = window.mozAnimationStartTime || Date.now(); requestAnimationFrame(draw); })();
來看個實際的例子,如下代碼:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> *{margin:0;padding:0;} #status{width:20px;height:20px;background: red;} </style> </head> <body> <div id="status"></div> <script type="text/javascript"> (function(){ function draw(timestamp){ //計算兩次重繪的時間間隔 var drawStart = timestamp || Date.now(), diff = drawStart - startTime; //使用diff確定下一步的繪製時間 console.log(diff) //把startTime重寫為這一次的繪製時間 startTime = drawStart; var div = document.getElementById("status"); var computedStyle = document.defaultView.getComputedStyle(div,null); div.style.width = (parseInt(computedStyle.width,10) + 5) + "px"; //重繪UI if(parseInt(computedStyle.width,10) < 500){ requestAnimationFrame(draw); } } var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame, startTime = window.mozAnimationStartTime || Date.now(); requestAnimationFrame(draw); })(); </script> </body> </html>
requestAnimationFrame簡單相容方式:
window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function( callback ){ window.setTimeout(callback, 1000 / 60); }; })();
更全面的相容方法:
(function() { var lastTime = 0; var vendors = ['webkit', 'moz']; for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || // Webkit中此取消方法的名字變了 window[vendors[x] + 'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) { window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16.7 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; } if (!window.cancelAnimationFrame) { window.cancelAnimationFrame = function(id) { clearTimeout(id); }; } }());
詳細參考地址:《requestAnimationFrame,Web中寫動畫的另一種選擇》。
Page visibility API
如果頁面最小化了或者隱藏在了其他標簽頁後面,Page visibility API(頁面可見性API)就是為了讓開發人員知道頁面是否對用戶可見而推出的。
- document.hidden:表示頁面是否隱藏的布爾值。
- visibilitychange事件:當文檔從可見變為不可見或者從不可見變為可見時,觸發該事件。
document.addEventListener('visibilitychange',function(){ if(document.hidden){ console.log('頁面隱藏了') }else{ console.log("頁面顯示了") } },false);
IE10+以及其它高版本瀏覽器支持該API。
Geolocation API
Geolocation API在瀏覽器中實現是navigator.geolocation對象,這個對象包含3個方法,getCurrentPosition()、watchPosition()、clearWatch()。
getCurrentPosition():接受三個參數,成功回調函數、可選的失敗回調函數和可選的選項對象。
其中成功回調函數會接收一個position對象參數,該對象有兩個屬性:coords和timestamp。coords將包含與位置相關的信息,如下:
- latitude:以十進位度數表示的緯度。
- longitude:以十進位度數表示的經度。
- accuracy:經、緯度坐標的精度,以米為單位。
失敗回調接收一個參數,這個參數是一個對象,包括兩個屬性:message和code。
getCurrentPosition()的第三個參數是一個選項對象,用於設定信息的類型。可以設置的選項有三個:
- enableHighAccuracy:布爾值,表示必須儘可能使用最準確的位置信息;
- timeout:是以毫秒數表示的等待位置信息的最長時間;
- maximumAge:表示上一次取得的坐標信息的有效時間,以毫秒數表示,如果時間到重新取得新坐標信息。
navigator.geolocation.getCurrentPosition(function(position){ console.log(position.coords.latitude,position.coords.longitude); },function(error){ console.log("Error code:" + error.code); console.log("Error message:" + error.message); },{ enableHighAccuracy:false, timeout:50000, maximumAge:25000 })
watchPosition():參數跟getCurrentPosition()一樣。實際上與定時調用getCurrentPosition()的效果相同。在第一次調用watchPostion()方法後,會取得當前位置,執行成功回調或者錯誤回調。然後,watchPosition()就地等待系統發出位置已改變的信號(它不會自己輪詢位置)。調用watchPosition()會返回一個數值標識符,用於跟蹤監控的操作。基於這個返回值可以取消監控操作,只要將其傳遞給clearWatch()方法即可。
var watchId = navigator.geolocation.watchPosition(function(position){ console.log(position.coords.latitude,position.coords.longitude); },function(error){ console.log("Error code:" + error.code); console.log("Error message:" + error.message); },{ enableHighAccuracy:false, timeout:50000, maximumAge:25000 }); navigator.geolocation.clearWatch(watchId);
File API
FIle API在表單中的文件輸入欄位的基礎上,又添加了一些直接訪問文件信息的介面。HTML5在DOM中為文件輸入元素添加了一個files集合。在通過文件輸入欄位選擇了一個或者多個文件時,files集合中包含一組File對象,每個file對象對應了一個文件。每個File對象都有下列只讀屬性。
- name:本地文件系統中的文件名。
- size:文件的位元組大小。
- type:字元串,文件的MIME類型。
- lastModifiedDate:字元串,文件上一次被修改的時間(只有Chrome實現了這個屬性)。
如下例子:
HTML代碼:
<input type="file" id="files-list" multiple/>
JS代碼:
var filesList = document.getElementById("files-list"); EventUtil.addHandler(filesList,"change",function(event){ var files = EventUtil.getTarget(event).files, i = 0, len = files.length; while(i < len){ console.log(files[i].name + "(" + files[i].type + "," + files[i].size + "bytes)"); i++; } })
FileReader類型
FileReader類型實現的是一種非同步文件讀取機制。可以把FileReader想象成XMLHttpRequest,區別隻是它讀取的是文件系統,而不是遠程伺服器。為了讀取文件中的數據,FileReader提供瞭如下幾個方法。
- readAsText(file,encoding):以純文本形式讀取文件,將讀取到的文本保存到result屬性中。第二個參數用於指定編碼類型,是可選的。
- readAsDataURL(file):讀取文件並將文件以數據URI的形式保存在result屬性中。
- readAsBinaryString(file):讀取文件並將一個字元串保存在result屬性中,字元串中的每個字元表示一個位元組。
- readAsArrayBuffer(file):讀取文件並將一個包含文件內容的ArrayBuffer保存在result屬性中。
例如,可以讀取圖像文件並將其保存為數據URI,以便將其顯示給用戶,或者為瞭解析方便,可以將文件讀取為文本形式。
由於讀取是非同步的,因此FileReader提供了幾個事件。其中最有用的3個事件是progress、error和load,分別表示是否讀取了新數據、是否發生了錯誤以及是否讀完了整個文件。
每過50ms左右,就會觸發一次progress事件,通過事件對象可以獲得與XHR的progress事件相同的信息(屬性):lengthComputable、loaded和total。另外,儘管可能沒有包含全部數據,但每次progress事件中都可以通過FileReader的result屬性讀取到文件內容。
由於種種原因無法讀取到文件時,就會觸發error事件。觸發error事件時,相關的信息將保存到FileReader的error屬性中。這個屬性中將保存一個對象,該對象只有一個屬性code,即錯誤碼。這個錯誤碼如下:
- 1:未找到文件。
- 2:安全性錯誤。
- 3:讀取中斷。
- 4:文件不可讀。
- 5:編碼錯誤。
文件成功載入後會觸發load事件;如果發生error事件,就不會觸發load事件。如下例子:
var filesList = document.getElementById("files-list"); EventUtil.addHandler(filesList,"change",function(event){ var info = "", output = document.getElementById("output"), progress = document.getElementById("progress"), files = EventUtil.getTarget(event).files, type = "default", reader = new FileReader(); if(/image/.test(files[0].type)){ reader.readAsDataURL(files[0]); type = "image"; }else{ reader.readAsText(files[0]); type = "text"; }; reader.onerror = function(){ output.innerHTML = "Could not read file,error code is:" + reader.error.code; }; reader.onprogress = function(event){ if(event.lengthComputable){ output.innerHTML = event.loaded + "/" + event.total; } }; reader.onload = function(){ var html = ""; switch(type){ case "image": html = "<img src=\""+reader.result+"\"/>"; break; case "text": html = reader.result; break; } output.innerHTML = html; }; });
讀取部分內容
有時候我們想讀取文件的一部分而不是全部內容。為此,File對象還支持一個slice()方法,這個方法在Firefox的實現為mozSlice(),在Chrome中的實現為webkitSlice()。slice()方法接收兩個參數:起始位元組及要讀取的位元組數。這個方法返回一個Blob實例,Blob是File類型的父類型。下麵一個通用的方法實現相容的slice():
function blobSlice(blob,startByte,length){ if(blob.slice){ return blob.slice(startByte,length); }else if(blob.webkitSlice){ return blob.webkitSlice(startByte,length); }else if(blob.mozSlice){ return blob.mozSlice(startByte,length); }else{ return null; } }
Blob類型有一個size屬性和一個type屬性,而且它也支持slice()方法,以便進一步切割數據。通過FileReader也可以從Blob中讀取數據。下麵這個例子只讀取文件的32B內容。
var filesList = document.getElementById("files-list"); EventUtil.addHandler(filesList,"change",function(event){ var info = "", output = document.getElementById("output"), progress = document.getElementById("progress"), files = EventUtil.getTarget(event).files, reader = new FileReader(), blob = blobSlice(files[0],0,32); if(blob){ reader.readAsText(blob); reader.onerror = function(){ output.innerHTML = "Could not read file,error code is:" + reader.error.code; }; reader.onload = function(){ output.innerHTML = reader.result; }; }else{ alert("Your browser doesn't support slice()."); } });
對象URL
對象URL也被成為blob URL,指的是引用保存在File或Blob中數據的URL。使用對象URL的好處是不用把文件內容讀取到JS中而直接使用文件內容。為此,只要在需要文件內容的地方提供對象URL即可。要創建對象URL,可以使用window.URL.createObjectURL()方法,並傳入File或者Blob對象。這個方法在Chrome中的實現為window.webkitURL.createObjectURL(),因此下麵相容寫法:
function createObjectURL(blob){ if(window.URL){ return window.URL.createObjectURL(blob); }else if(window.webkitURL){ return window.webkitURL.createObjectURL(blob); }else{ return null; } }
這個函數返回值是一個字元串,指向一塊記憶體的地址。因為這個字元串是URL,所以在DOM中也能使用,例如,在頁面中顯示一個圖形文件:
var filesList = document.getElementById("files-list"); EventUtil.addHandler(filesList,"change",function(event){ var info = "", output = document.getElementById("output"), progress = document.getElementById("progress"), files = EventUtil.getTarget(event).files, url = createObjectURL(files[0]); if(url){ if(/image/.test(files[0].type)){ output.innerHTML = "<img src=\"" + url +"\"/>"; }else{ output.innerHTML = "Not an image." } }else{ alert("Your browser doesn't support URLs."); } });
直接把對象URL放到<img>標簽中,就省去了把數據先讀到JS中的麻煩。另一方面,<img>標簽則會找到響應的記憶體地址,直接讀取數據並將圖像顯示在頁面中。
如果不再需要相應的數據,最好釋放它占用的記憶體。但只要有代碼在引用對象URL,記憶體就不會釋放。要手工釋放記憶體,可以把對象URL傳給window.URL.revokeObjectURL()(在Chrome中是window.webkitURL.revokeObjectURL()),相容寫法如下:
function revokeObjectURL(blob){ if(window.URL){ return window.URL.revokeObjectURL(blob); }else if(window.webkitURL){ return window.webkitURL.revokeObjectURL(blob); }else{ return null; } }
支持對象URL的瀏覽器為IE10+、Firefox和Chrome。
讀取拖放的文件
從桌面上把文件拖放到瀏覽器中也會觸發drop事件。而且可以在event.dataTransfer.files中讀取到被放置的文件,當然此時它是一個File對象,與通過文件輸入欄位取得的File對象一樣。
下麵例子會將放置到頁面中自定義的放置目標中的文件信息顯示出來:
var droptarget = document.getElementById("droptarget"); function handleEvent(event){ var info = "", output = document.getElementById("output"), files, i, len; EventUtil.preventDefault(event); if(event.type == "drop"){ files = event.dataTransfer.files; i = 0; len = files.length; while(i < len){ info += files[i].name + "(" + files[i].type + "," + files[i].size + "byte)<br/>"; i ++; } output.innerHTML = info; } } EventUtil.addHandler(droptarget,"dragenter",handleEvent); EventUtil.addHandler(droptarget,"dragover",handleEvent); EventUtil.addHandler(droptarget,"drop",handleEvent);
使用XHR上傳文件
var droptarget = document.getElementById("droptarget"); function handleEvent(event){ var info = "", output = document.getElementById("output"), data,xhr, files,i,len; EventUtil.preventDefault(event); if(event.type == "drop"){ data = new FormData(); files = event.dataTransfer.files; i = 0; len = files.length; while(i < len){ data.append("file" + i,files[i]); i++; } xhr = new XMLHttpRequest(); xhr.open("post","FileAPIUpload.php",true); xhr.onreadystatechange = function(){ if(xhr.readyState == 4){ alert(xhr.responseText); } } xhr.send(data); } } EventUtil.addHandler(droptarget,"dragenter",handleEvent); EventUtil.addHandler(droptarget,"dragover",handleEvent); EventUtil.addHandler(droptarget,"drop",handleEvent);
Web計時
Web計時機制的核心是window.performance對象。window.performance對象有兩個屬性performance.navigation和performance.timing。
Web Workers
隨著Web應用複雜性的與日俱進,越來越複雜的計算在所難免。長時間運行的Javascript進程會導致瀏覽器凍結用戶界面,讓人感覺屏幕“凍結”了。Web Workers規範通過讓JS在後臺運行解決了這個問題。瀏覽器實現Web Workers規範的方式有很多種,可以使用線程、後臺進程或者運行在其他處理器核心上的進程,等等。
目前支持Web Workers的瀏覽器IE10+以及其它高版本瀏覽器。
使用Worker
實例化Worker對象並傳入要執行的JS文件名就可以創建一個新的Worker,如下:
var worker = new Worker("js/index.js");
這行代碼會導致瀏覽器下載index.js,但只有接收到消息才會實際執行文件中的代碼。要給Worker傳遞消息,可以使用postMessage()方法(與XDM中的postMessage()方法類似):
worker.postMessage("start!");
消息內容可以是任何能被序列化的值,不過與XDM不同的是,在所有支持的瀏覽器中,postMessage()都能接收對象參數。因此,可以隨便傳遞任何形式的對象數據,如下例子:
worker.postMessage({ type:"Command", message:"start!" });
Worker是通過message和error事件與頁面通信的。來自Worker的數據保存在event.data中。Worker返回的數據也可以是任何能被序列化的值:
worker.onmessage = function(event){ var data = event.data; //對數據執行處理 }
Worker不能完成給定的任務時會觸發error事件。具體來說,Worker內部的JS在執行過程中只要遇到錯誤,就會觸發error事件。發生error事件時,事件對象包含三個屬性:filename、lineno和message,分別表示發生錯誤的文件名、代碼行號和完整的錯誤信息。
worker.onerror = function(event){ console.log("ERROR:" + event.filename + "(" + event.lineno + "):" + event.message); }
只要調用terminate()方法就可以停止Worker的工作。
worker.terminate(); //立即停止Worker的工作
Worker全局作用域
Web Worker中的全局對象是worker對象本身。也就是說,在這個特殊的全局作用域中this和self引用的都是worker對象。為便於處理數據,Web Worker本身也是一個最小化的運行環境。
- 最小化的navigator對象,包括onLine、appName、appVersion、userAgent和platform屬性。
- 只讀的location對象。
- setTimeout()、setInterval()、clearTimeout()和clearInterVal()方法。
- XMLHttpRequest構造函數。
顯然,Web Worker的運行環境與頁面環境相比,功能是相當有限的。
當頁面在worker對象上調用postMessage()時,數據會以非同步方式被傳遞給worker,進而觸發worker中的message事件。為了處理來自頁面的數據,同樣也需要創建一個onmessage事件處理程式。
//Web Worker內部代碼 self.onmessage = function(event){ var data = event.data; //處理數據 }
大家看清楚,這裡的self引用的是Worker全局作用域中的worker對象(與頁面中的Worker對象不同一個對象)。Worker完成工作後,通過調用postMessage()可以把數據再發回頁面。例如下麵的例子假如需要Worker對傳入的數組進行排序,而Worker在排序之後又將數組發回了頁面:
//Web Worker內部代碼 self.onmessage = function(event){ var data = event.data; //別忘了,預設的sort方法只比較字元串 data.sort(function(a,b){ return a - b; }) self.postMessage(data); }
傳遞消息就是頁面與Worker相互之間通信的方式。在Worker中調用postMessage()會以非同步的方式觸發頁面中Worker實例的message事件。如果頁面想要使用這個Worker,可以這樣:
//在頁面中 var data = [23,4,7,59,11,24,222,10,3], worker = new Worker("index.js"); worker.onmessage = function(event){ var data = event.data; //對排序後的數組進行操作 console.log(data); //[3, 4, 7, 10, 11, 23, 24, 59, 222] } //將數組發送給worker排序 worker.postMessage(data);
在上面建立的index.js中,也就是在Worker作用域下代碼如下:
self.onmessage = function(event){ var data = event.data; data.sort(function(a,b){ return a - b; }) self.postMessage(data); }
排序的確是比較消耗時間的操作,因此轉交給Worker做就不會阻塞用戶界面了。另外把彩色圖像轉換成灰階圖像以及加密解密之類的操作也是相當費時的。
在Worker內部,調用close()方法也可以停止工作,Worker停止工作後就不會再有事件發生了。
//web worker內部的代碼 self.close();
包含其它腳本
雖然無法在Worker中動態創建<script>元素,Worker的全局作用域提供了一個方法是importScripts(),這個方法接收一個或者多個指向JS文件的URL。每個載入過程都是非同步的,因此所有腳本載入並執行之後,importScripts()才會執行,如下代碼:
//web worker內部的代碼 importScripts("file1.js","file2.js");
即使file2.js優先於file1.js下載完,執行的時候仍然會按照先後順序執行。
Web Worker詳細可參考:《Web Worker 使用教程 - 阮一峰的網路日誌_阮一峰的個人網站》