W3Cschool
恭喜您成為首批注冊(cè)用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
JavaScript語(yǔ)言采用的是單線程模型,也就是說(shuō),所有任務(wù)排成一個(gè)隊(duì)列,一次只能做一件事。隨著電腦計(jì)算能力的增強(qiáng),尤其是多核CPU的出現(xiàn),這一點(diǎn)帶來(lái)很大的不便,無(wú)法充分發(fā)揮JavaScript的潛力。
Web Worker的目的,就是為JavaScript創(chuàng)造多線程環(huán)境,允許主線程將一些任務(wù)分配給子線程。在主線程運(yùn)行的同時(shí),子線程在后臺(tái)運(yùn)行,兩者互不干擾。等到子線程完成計(jì)算任務(wù),再把結(jié)果返回給主線程。因此,每一個(gè)子線程就好像一個(gè)“工人”(worker),默默地完成自己的工作。這樣做的好處是,一些高計(jì)算量或高延遲的工作,被worker線程負(fù)擔(dān)了,所以主進(jìn)程(通常是UI進(jìn)程)就會(huì)很流暢,不會(huì)被阻塞或拖慢。
Worker線程分成好幾種。
Web Worker有以下幾個(gè)特點(diǎn):
同域限制。子線程加載的腳本文件,必須與主線程的腳本文件在同一個(gè)域。
DOM限制。子線程所在的全局對(duì)象,與主進(jìn)程不一樣,它無(wú)法讀取網(wǎng)頁(yè)的DOM對(duì)象,即document
、window
、parent
這些對(duì)象,子線程都無(wú)法得到。(但是,navigator
對(duì)象和location
對(duì)象可以獲得。)
腳本限制。子線程無(wú)法讀取網(wǎng)頁(yè)的全局變量和函數(shù),也不能執(zhí)行alert和confirm方法,不過(guò)可以執(zhí)行setInterval和setTimeout,以及使用XMLHttpRequest對(duì)象發(fā)出AJAX請(qǐng)求。
文件限制。子線程無(wú)法讀取本地文件,即子線程無(wú)法打開(kāi)本機(jī)的文件系統(tǒng)(file://),它所加載的腳本,必須來(lái)自網(wǎng)絡(luò)。
使用之前,檢查瀏覽器是否支持這個(gè)API。
if (window.Worker) {
// 支持
} else {
// 不支持
}
主線程采用new
命令,調(diào)用Worker
構(gòu)造函數(shù),可以新建一個(gè)子線程。
var worker = new Worker('work.js');
Worker構(gòu)造函數(shù)的參數(shù)是一個(gè)腳本文件,這個(gè)文件就是子線程所要完成的任務(wù),上面代碼中是work.js
。由于子線程不能讀取本地文件系統(tǒng),所以這個(gè)腳本文件必須來(lái)自網(wǎng)絡(luò)端。如果下載沒(méi)有成功,比如出現(xiàn)404錯(cuò)誤,這個(gè)子線程就會(huì)默默地失敗。
子線程新建之后,并沒(méi)有啟動(dòng),必需等待主線程調(diào)用postMessage
方法,即發(fā)出信號(hào)之后才會(huì)啟動(dòng)。postMessage
方法的參數(shù),就是主線程傳給子線程的信號(hào)。它可以是一個(gè)字符串,也可以是一個(gè)對(duì)象。
worker.postMessage("Hello World");
worker.postMessage({method: 'echo', args: ['Work']});
只要符合父線程的同源政策,Worker線程自己也能新建Worker線程。Worker線程可以使用XMLHttpRequest進(jìn)行網(wǎng)絡(luò)I/O,但是XMLHttpRequest
對(duì)象的responseXML
和channel
屬性總是返回null
。
在子線程內(nèi),必須有一個(gè)回調(diào)函數(shù),監(jiān)聽(tīng)message事件。
/* File: work.js */
self.addEventListener('message', function(e) {
self.postMessage('You said: ' + e.data);
}, false);
self代表子線程自身,self.addEventListener表示對(duì)子線程的message事件指定回調(diào)函數(shù)(直接指定onmessage屬性的值也可)?;卣{(diào)函數(shù)的參數(shù)是一個(gè)事件對(duì)象,它的data屬性包含主線程發(fā)來(lái)的信號(hào)。self.postMessage則表示,子線程向主線程發(fā)送一個(gè)信號(hào)。
根據(jù)主線程發(fā)來(lái)的不同的信號(hào)值,子線程可以調(diào)用不同的方法。
/* File: work.js */
self.onmessage = function(event) {
var method = event.data.method;
var args = event.data.args;
var reply = doSomething(args);
self.postMessage({method: method, reply: reply});
};
主線程也必須指定message事件的回調(diào)函數(shù),監(jiān)聽(tīng)子線程發(fā)來(lái)的信號(hào)。
/* File: main.js */
worker.addEventListener('message', function(e) {
console.log(e.data);
}, false);
主線程可以監(jiān)聽(tīng)子線程是否發(fā)生錯(cuò)誤。如果發(fā)生錯(cuò)誤,會(huì)觸發(fā)主線程的error事件。
worker.onerror(function(event) {
console.log(event);
});
// or
worker.addEventListener('error', function(event) {
console.log(event);
});
使用完畢之后,為了節(jié)省系統(tǒng)資源,我們必須在主線程調(diào)用terminate方法,手動(dòng)關(guān)閉子線程。
worker.terminate();
也可以子線程內(nèi)部關(guān)閉自身。
self.close();
前面說(shuō)過(guò),主線程與子線程之間的通信內(nèi)容,可以是文本,也可以是對(duì)象。需要注意的是,這種通信是拷貝關(guān)系,即是傳值而不是傳址,子線程對(duì)通信內(nèi)容的修改,不會(huì)影響到主線程。事實(shí)上,瀏覽器內(nèi)部的運(yùn)行機(jī)制是,先將通信內(nèi)容串行化,然后把串行化后的字符串發(fā)給子線程,后者再將它還原。
主線程與子線程之間也可以交換二進(jìn)制數(shù)據(jù),比如File、Blob、ArrayBuffer等對(duì)象,也可以在線程之間發(fā)送。
但是,用拷貝方式發(fā)送二進(jìn)制數(shù)據(jù),會(huì)造成性能問(wèn)題。比如,主線程向子線程發(fā)送一個(gè)500MB文件,默認(rèn)情況下瀏覽器會(huì)生成一個(gè)原文件的拷貝。為了解決這個(gè)問(wèn)題,JavaScript允許主線程把二進(jìn)制數(shù)據(jù)直接轉(zhuǎn)移給子線程,但是一旦轉(zhuǎn)移,主線程就無(wú)法再使用這些二進(jìn)制數(shù)據(jù)了,這是為了防止出現(xiàn)多個(gè)線程同時(shí)修改數(shù)據(jù)的麻煩局面。這種轉(zhuǎn)移數(shù)據(jù)的方法,叫做Transferable Objects。
如果要使用該方法,postMessage方法的最后一個(gè)參數(shù)必須是一個(gè)數(shù)組,用來(lái)指定前面發(fā)送的哪些值可以被轉(zhuǎn)移給子線程。
worker.postMessage(arrayBuffer, [arrayBuffer]);
window.postMessage(arrayBuffer, targetOrigin, [arrayBuffer]);
通常情況下,子線程載入的是一個(gè)單獨(dú)的JavaScript文件,但是也可以載入與主線程在同一個(gè)網(wǎng)頁(yè)的代碼。假設(shè)網(wǎng)頁(yè)代碼如下:
<!DOCTYPE html>
<body>
<script id="worker" type="app/worker">
addEventListener('message', function() {
postMessage('Im reading Tech.pro');
}, false);
</script>
</body>
</html>
我們可以讀取頁(yè)面中的script,用worker來(lái)處理。
var blob = new Blob([document.querySelector('#worker').textContent]);
這里需要把代碼當(dāng)作二進(jìn)制對(duì)象讀取,所以使用Blob接口。然后,這個(gè)二進(jìn)制對(duì)象轉(zhuǎn)為URL,再通過(guò)這個(gè)URL創(chuàng)建worker。
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
部署事件監(jiān)聽(tīng)代碼。
worker.addEventListener('message', function(e) {
console.log(e.data);
}, false);
最后,啟動(dòng)worker。
worker.postMessage('');
整個(gè)頁(yè)面的代碼如下:
<!DOCTYPE html>
<body>
<script id="worker" type="app/worker">
addEventListener('message', function() {
postMessage('Work done!');
}, false);
</script>
<script>
(function() {
var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
worker.addEventListener('message', function(e) {
console.log(e.data);
}, false);
worker.postMessage('');
})();
</script>
</body>
</html>
可以看到,主線程和子線程的代碼都在同一個(gè)網(wǎng)頁(yè)上面。
上面所講的Web Worker都是專屬于某個(gè)網(wǎng)頁(yè)的,當(dāng)該網(wǎng)頁(yè)關(guān)閉,worker就自動(dòng)結(jié)束。除此之外,還有一種共享式的Web Worker,允許多個(gè)瀏覽器窗口共享同一個(gè)worker,只有當(dāng)所有網(wǎng)口關(guān)閉,它才會(huì)結(jié)束。這種共享式的Worker用SharedWorker對(duì)象來(lái)建立,因?yàn)檫m用場(chǎng)合不多,這里就省略了。
有時(shí),瀏覽器需要論詢服務(wù)器狀態(tài),以便第一時(shí)間得知狀態(tài)改變。這個(gè)工作可以放在 Worker 進(jìn)程里面。
var pollingWorker = createWorker(function (e) {
var cache;
function compare(new, old) { ... };
var myRequest = new Request('/my-api-endpoint');
setInterval(function () {
fetch('/my-api-endpoint').then(function (res) {
var data = res.json();
if(!compare(res.json(), cache)) {
cache = data;
self.postMessage(data);
}
})
}, 1000)
});
pollingWorker.onmessage = function () {
// render data
}
pollingWorker.postMessage('init');
Service worker是一個(gè)在瀏覽器后臺(tái)運(yùn)行的腳本,與網(wǎng)頁(yè)不相干,專注于那些不需要網(wǎng)頁(yè)或用戶互動(dòng)就能完成的功能。它主要用于操作離線緩存。
Service Worker有以下特點(diǎn)。
postMessage
接口與頁(yè)面通信。Service worker的常見(jiàn)用途。
使用Service Worker有以下步驟。
首先,需要向?yàn)g覽器登記Service Worker。
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
// 登記成功
console.log('ServiceWorker登記成功,范圍為', registration.scope);
}).catch(function(err) {
// 登記失敗
console.log('ServiceWorker登記失?。?, err);
});
}
上面代碼向?yàn)g覽器登記sw.js
腳本,實(shí)質(zhì)就是瀏覽器加載sw.js
。這段代碼可以多次調(diào)用,瀏覽器會(huì)自行判斷sw.js
是否登記過(guò),如果已經(jīng)登記過(guò),就不再重復(fù)執(zhí)行了。注意,Service worker腳本必須與頁(yè)面在同一個(gè)域,且必須在HTTPs協(xié)議下正常運(yùn)行。
sw.js
位于域名的根目錄下,這表明這個(gè)Service worker的范圍(scope)是整個(gè)域,即會(huì)接收整個(gè)域下面的fetch
事件。如果腳本的路徑是/example/sw.js
,那么Service worker只對(duì)/example/
開(kāi)頭的URL有效(比如/example/page1/
、/example/page2/
)。如果腳本不在根目錄下,但是希望對(duì)整個(gè)域都有效,可以指定scope
屬性。
navigator.serviceWorker.register('/path/to/serviceworker.js', {
scope: '/'
});
一旦登記完成,這段腳本就會(huì)用戶的瀏覽器之中長(zhǎng)期存在,不會(huì)隨著用戶離開(kāi)你的網(wǎng)站而消失。
.register
方法返回一個(gè)Promise對(duì)象。
登記成功后,瀏覽器執(zhí)行下面步驟。
安裝和激活,主要通過(guò)事件來(lái)判斷。
self.addEventListener('install', function(event) {
event.waitUntil(
fetchStuffAndInitDatabases()
);
});
self.addEventListener('activate', function(event) {
// You're good to go!
});
Service worker一旦激活,就開(kāi)始控制頁(yè)面。網(wǎng)頁(yè)加載的時(shí)候,可以選擇一個(gè)Service worker作為自己的控制器。不過(guò),頁(yè)面第一次加載的時(shí)候,它不受Service worker控制,因?yàn)檫@時(shí)還沒(méi)有一個(gè)Service worker在運(yùn)行。只有重新加載頁(yè)面后,Service worker才會(huì)生效,控制加載它的頁(yè)面。
你可以查看navigator.serviceWorker.controller
,了解當(dāng)前哪個(gè)ServiceWorker掌握控制權(quán)。如果后臺(tái)沒(méi)有任何Service worker,navigator.serviceWorker.controller
返回null
。
Service worker激活以后,就能監(jiān)聽(tīng)fetch
事件。
self.addEventListener('fetch', function(event) {
console.log(event.request);
});
fetch
事件會(huì)在兩種情況下觸發(fā)。
iframe
和<object>
標(biāo)簽發(fā)出的請(qǐng)求不會(huì)被攔截。fetch
事件的event
對(duì)象的request
屬性,返回一個(gè)對(duì)象,包含了所攔截的網(wǎng)絡(luò)請(qǐng)求的所有信息,比如URL、請(qǐng)求方法和HTTP頭信息。
Service worker的強(qiáng)大之處,在于它會(huì)攔截請(qǐng)求,并會(huì)返回一個(gè)全新的回應(yīng)。
self.addEventListener('fetch', function(event) {
event.respondWith(new Response("Hello world!"));
});
respondWith
方法的參數(shù)是一個(gè)Response對(duì)象實(shí)例,或者一個(gè)Promise對(duì)象(resolved以后返回一個(gè)Response實(shí)例)。上面代碼手動(dòng)創(chuàng)造一個(gè)Response實(shí)例。
下面是完整的代碼。
先看網(wǎng)頁(yè)代碼index.html
。
<!DOCTYPE html>
<html>
<head>
<style>
body {
white-space: pre-line;
font-family: monospace;
font-size: 14px;
}
</style>
</head>
<body><script>
function log() {
document.body.appendChild(document.createTextNode(Array.prototype.join.call(arguments, ", ") + '\n'));
console.log.apply(console, arguments);
}
window.onerror = function(err) {
log("Error", err);
};
navigator.serviceWorker.register('sw.js', {
scope: './'
}).then(function(sw) {
log("Registered!", sw);
log("You should get a different response when you refresh");
}).catch(function(err) {
log("Error", err);
});
</script></body>
</html>
然后是Service worker腳本sw.js
。
// The SW will be shutdown when not in use to save memory,
// be aware that any global state is likely to disappear
console.log("SW startup");
self.addEventListener('install', function(event) {
console.log("SW installed");
});
self.addEventListener('activate', function(event) {
console.log("SW activated");
});
self.addEventListener('fetch', function(event) {
console.log("Caught a fetch!");
event.respondWith(new Response("Hello world!"));
});
每一次瀏覽器向服務(wù)器要求一個(gè)文件的時(shí)候,就會(huì)觸發(fā)fetch
事件。Service worker可以在發(fā)出這個(gè)請(qǐng)求之前,前攔截它。
self.addEventListener('fetch', function (event) {
var request = event.request;
...
});
實(shí)際應(yīng)用中,我們使用fetch
方法去抓取資源,該方法返回一個(gè)Promise對(duì)象。
self.addEventListener('fetch', function(event) {
if (/\.jpg$/.test(event.request.url)) {
event.respondWith(
fetch('//www.google.co.uk/logos/example.gif', {
mode: 'no-cors'
})
);
}
});
上面代碼中,如果網(wǎng)頁(yè)請(qǐng)求JPG文件,就會(huì)被Service worker攔截,轉(zhuǎn)而返回一個(gè)Google的Logo圖像。fetch
方法默認(rèn)會(huì)加上CORS信息頭,,上面設(shè)置了取消這個(gè)頭。
下面的代碼是一個(gè)將所有JPG、PNG圖片請(qǐng)求,改成WebP格式返回的例子。
"use strict";
// Listen to fetch events
self.addEventListener('fetch', function(event) {
// Check if the image is a jpeg
if (/\.jpg$|.png$/.test(event.request.url)) {
// Inspect the accept header for WebP support
var supportsWebp = false;
if (event.request.headers.has('accept')){
supportsWebp = event.request.headers.get('accept').includes('webp');
}
// If we support WebP
if (supportsWebp) {
// Clone the request
var req = event.request.clone();
// Build the return URL
var returnUrl = req.url.substr(0, req.url.lastIndexOf(".")) + ".webp";
event.respondWith(fetch(returnUrl, {
mode: 'no-cors'
}));
}
}
});
如果請(qǐng)求失敗,可以通過(guò)Promise的catch
方法處理。
self.addEventListener('fetch', function(event) {
event.respondWith(
fetch(event.request).catch(function() {
return new Response("Request failed!");
})
);
});
登記成功后,可以在Chrome瀏覽器訪問(wèn)chrome://inspect/#service-workers
,查看整個(gè)瀏覽器目前正在運(yùn)行的Service worker。訪問(wèn)chrome://serviceworker-internals
,可以查看瀏覽器目前安裝的所有Service worker。
一個(gè)已經(jīng)登記過(guò)的Service worker腳本,如果發(fā)生改動(dòng),瀏覽器就會(huì)重新安裝,這被稱為“升級(jí)”。
Service worker有一個(gè)Cache API,用來(lái)緩存外部資源。
self.addEventListener('install', function(event) {
// pre cache a load of stuff:
event.waitUntil(
caches.open('myapp-static-v1').then(function(cache) {
return cache.addAll([
'/',
'/styles/all.css',
'/styles/imgs/bg.png',
'/scripts/all.js'
]);
})
)
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
上面代碼中,caches.open
方法用來(lái)建立緩存,然后使用addAll
方法添加資源。caches.match
方法則用來(lái)建立緩存以后,匹配當(dāng)前請(qǐng)求是否在緩存之中,如果命中就取出緩存,否則就正常發(fā)出這個(gè)請(qǐng)求。一旦一個(gè)資源進(jìn)入緩存,它原來(lái)指定是否過(guò)期的HTTP信息頭,就會(huì)被忽略。緩存之中的資源,只在你移除它們的時(shí)候,才會(huì)被移除。
單個(gè)資源可以使用cache.put(request, response)
方法添加。
下面是一個(gè)在安裝階段緩存資源的例子。
var staticCacheName = 'static';
var version = 'v1::';
self.addEventListener('install', function (event) {
event.waitUntil(updateStaticCache());
});
function updateStaticCache() {
return caches.open(version + staticCacheName)
.then(function (cache) {
return cache.addAll([
'/path/to/javascript.js',
'/path/to/stylesheet.css',
'/path/to/someimage.png',
'/path/to/someotherimage.png',
'/',
'/offline'
]);
});
};
上面代碼將JavaScript腳本、CSS樣式表、圖像文件、網(wǎng)站首頁(yè)、離線頁(yè)面,存入瀏覽器緩存。這些資源都要等全部進(jìn)入緩存之后,才會(huì)安裝。
安裝以后,就需要激活。
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys()
.then(function (keys) {
return Promise.all(keys
.filter(function (key) {
return key.indexOf(version) !== 0;
})
.map(function (key) {
return caches.delete(key);
})
);
})
);
});
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: