《JavaScript高级程序设计》笔记:新兴的API

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对象参数,该对象有两个属性:coordstimestamp。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个事件是progresserrorload,分别表示是否读取了新数据、是否发生了错误以及是否读完了整个文件。

每过50ms左右,就会触发一次progress事件,通过事件对象可以获得与XHR的progress事件相同的信息(属性):lengthComputableloadedtotal。另外,尽管可能没有包含全部数据,但每次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.navigationperformance.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是通过messageerror事件与页面通信的。来自Worker的数据保存在event.data中。Worker返回的数据也可以是任何能被序列化的值:

worker.onmessage = function(event){
    var data = event.data;
    
    //对数据执行处理
}

Worker不能完成给定的任务时会触发error事件。具体来说,Worker内部的JS在执行过程中只要遇到错误,就会触发error事件。发生error事件时,事件对象包含三个属性:filenamelinenomessage,分别表示发生错误的文件名、代码行号和完整的错误信息。

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 使用教程 - 阮一峰的网络日志_阮一峰的个人网站

posted @ 2019-05-07 09:13  风雨后见彩虹  阅读(...)  评论(... 编辑 收藏