Khái niệm cơ bản về Web Worker

Vấn đề: Tính đồng thời của JavaScript

Có một số điểm tắc nghẽn ngăn không cho chuyển các ứng dụng thú vị (chẳng hạn như từ các hoạt động triển khai nặng máy chủ) sang JavaScript phía máy khách. Một số yếu tố trong số này bao gồm khả năng tương thích với trình duyệt, nhập tĩnh, hỗ trợ tiếp cận và hiệu suất. May mắn thay, lỗi thứ hai nhanh chóng trở thành dĩ vãng khi các nhà cung cấp trình duyệt nhanh chóng cải thiện tốc độ cho các công cụ JavaScript của họ.

Một điều vẫn còn là trở ngại cho JavaScript thực sự là chính ngôn ngữ. JavaScript là một môi trường đơn luồng, có nghĩa là nhiều tập lệnh không thể chạy cùng một lúc. Ví dụ: hãy tưởng tượng một trang web cần xử lý các sự kiện trên giao diện người dùng, truy vấn và xử lý lượng lớn dữ liệu API và thao tác DOM. Khá phổ biến, đúng không? Rất tiếc, tất cả những thao tác đó không thể thực hiện đồng thời do các giới hạn về thời gian chạy JavaScript của trình duyệt. Quá trình thực thi tập lệnh diễn ra trong một luồng.

Các nhà phát triển bắt chước "tính năng đồng thời" bằng cách sử dụng các kỹ thuật như setTimeout(), setInterval(), XMLHttpRequest và trình xử lý sự kiện. Có, tất cả các tính năng này chạy không đồng bộ, nhưng không chặn không nhất thiết có nghĩa là đồng thời. Các sự kiện không đồng bộ được xử lý sau khi tập lệnh thực thi hiện tại mang lại. Tin vui là HTML5 mang lại cho chúng ta nhiều thứ tốt hơn các mẹo này!

Giới thiệu Web Workers: đưa luồng vào JavaScript

Thông số kỹ thuật Web Workers xác định API để tạo tập lệnh nền trong ứng dụng web của bạn. Web Workers cho phép bạn làm những việc như kích hoạt các tập lệnh chạy trong thời gian dài để xử lý các tác vụ nặng về tính toán, mà không chặn giao diện người dùng hoặc các tập lệnh khác để xử lý tương tác của người dùng. Họ sẽ giúp loại bỏ hộp thoại "tập lệnh không phản hồi" khó chịu mà tất cả chúng ta đều yêu thích:

Hộp thoại tập lệnh không phản hồi
Hộp thoại tập lệnh không phản hồi thường gặp.

Worker sử dụng việc truyền thông điệp giống như chuỗi để đạt được tính song song. Các tính năng này là lựa chọn hoàn hảo để giữ cho giao diện người dùng của bạn làm mới, hoạt động hiệu quả và thích ứng cho người dùng.

Các loại trình chạy web

Điểm đáng chú ý là quy cách thảo luận về 2 loại Trình chạy web là Trình chạy chuyên dụngTrình chạy dùng chung. Bài viết này sẽ chỉ đề cập đến trình thực thi chuyên dụng. Tôi gọi chúng là "nhân viên web" hoặc "nhân viên" xuyên suốt.

Bắt đầu

Web Worker chạy trong một luồng tách biệt. Do đó, mã mà các lệnh này thực thi cần được chứa trong một tệp riêng. Nhưng trước khi làm việc đó, điều đầu tiên cần làm là tạo một đối tượng Worker mới trong trang chính. Hàm khởi tạo lấy tên của tập lệnh worker:

var worker = new Worker('task.js');

Nếu tệp được chỉ định tồn tại, trình duyệt sẽ tạo một luồng worker mới được tải xuống không đồng bộ. Worker sẽ không bắt đầu cho đến khi tệp được tải xuống và thực thi hoàn toàn. Nếu đường dẫn đến worker của bạn trả về lỗi 404, thì worker sẽ tự động không hoạt động.

Sau khi tạo worker, hãy khởi động worker bằng cách gọi phương thức postMessage():

worker.postMessage(); // Start the worker.

Giao tiếp với nhân viên bằng cách truyền tin nhắn

Hoạt động giao tiếp giữa công việc và trang mẹ của công việc được thực hiện bằng mô hình sự kiện và phương thức postMessage(). Tuỳ thuộc vào trình duyệt/phiên bản của bạn, postMessage() có thể chấp nhận một chuỗi hoặc đối tượng JSON làm đối số duy nhất. Phiên bản mới nhất của các trình duyệt hiện đại hỗ trợ truyền đối tượng JSON.

Dưới đây là ví dụ về cách sử dụng một chuỗi để truyền "Hello World" đến một worker trong doWork.js. Worker này chỉ cần trả về thông báo được chuyển đến.

Chữ viết chính:

var worker = new Worker('doWork.js');

worker.addEventListener('message', function(e) {
console.log('Worker said: ', e.data);
}, false);

worker.postMessage('Hello World'); // Send data to our worker.

doWork.js (trình thực thi):

self.addEventListener('message', function(e) {
self.postMessage(e.data);
}, false);

Khi postMessage() được gọi từ trang chính, worker của chúng tôi sẽ xử lý thông báo đó bằng cách xác định một trình xử lý onmessage cho sự kiện message. Bạn có thể truy cập vào trọng tải tin nhắn (trong trường hợp này là "Hello World") trong Event.data. Mặc dù ví dụ cụ thể này không thú vị lắm, nhưng ví dụ này cho thấy rằng postMessage() cũng là phương tiện để bạn chuyển dữ liệu trở lại luồng chính. Thuận tiện!

Tin nhắn được chuyển giữa trang chính và các worker được sao chép chứ không được chia sẻ. Ví dụ: trong ví dụ tiếp theo, thuộc tính "msg" của thông báo JSON có thể truy cập được ở cả hai vị trí. Có vẻ như đối tượng đang được truyền trực tiếp đến worker mặc dù nó đang chạy trong một không gian riêng, dành riêng. Trên thực tế, điều đang xảy ra là đối tượng được chuyển đổi tuần tự khi được chuyển giao cho worker và sau đó được chuyển đổi tuần tự ở phía bên kia. Trang và trình thực thi không chia sẻ cùng một thực thể, vì vậy, kết quả cuối cùng là một bản sao được tạo trên mỗi lượt truyền. Hầu hết các trình duyệt triển khai tính năng này bằng cách tự động mã hoá/giải mã JSON ở một trong hai đầu.

Sau đây là một ví dụ phức tạp hơn về việc truyền thông điệp bằng đối tượng JSON.

Chữ viết chính:

<button onclick="sayHI()">Say HI</button>
<button onclick="unknownCmd()">Send unknown command</button>
<button onclick="stop()">Stop worker</button>
<output id="result"></output>

<script>
function sayHI() {
worker.postMessage({'cmd': 'start', 'msg': 'Hi'});
}

function stop() {
// worker.terminate() from this script would also stop the worker.
worker.postMessage({'cmd': 'stop', 'msg': 'Bye'});
}

function unknownCmd() {
worker.postMessage({'cmd': 'foobard', 'msg': '???'});
}

var worker = new Worker('doWork2.js');

worker.addEventListener('message', function(e) {
document.getElementById('result').textContent = e.data;
}, false);
</script>

doWork2.js:

self.addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
    self.postMessage('WORKER STARTED: ' + data.msg);
    break;
case 'stop':
    self.postMessage('WORKER STOPPED: ' + data.msg +
                    '. (buttons will no longer work)');
    self.close(); // Terminates the worker.
    break;
default:
    self.postMessage('Unknown command: ' + data.msg);
};
}, false);

Đối tượng có thể chuyển

Hầu hết trình duyệt đều triển khai thuật toán sao chép có cấu trúc, cho phép bạn truyền các loại phức tạp hơn vào/ra khỏi Worker, chẳng hạn như các đối tượng File, Blob, ArrayBuffer và JSON. Tuy nhiên, khi chuyển các loại dữ liệu này bằng postMessage(), bản sao vẫn được tạo. Do đó, nếu bạn đang truyền một tệp có kích thước lớn 50 MB (ví dụ), sẽ có một mức hao tổn đáng kể trong việc lấy tệp đó giữa trình thực thi và luồng chính.

Sao chép có cấu trúc rất hiệu quả nhưng một bản sao có thể mất hàng trăm mili giây. Để chống lại lần truy cập hiệu suất này, bạn có thể sử dụng Transferable Audiences (Đối tượng có thể chuyển).

Với Đối tượng có thể chuyển, dữ liệu được chuyển từ ngữ cảnh này sang ngữ cảnh khác. Đây là công cụ không sao chép, giúp cải thiện đáng kể hiệu suất gửi dữ liệu cho Worker. Hãy coi đây là mã tham chiếu nếu bạn thuộc thế giới C/C++. Tuy nhiên, không giống như tính năng truyền qua tham chiếu, "phiên bản" trong ngữ cảnh gọi sẽ không còn dùng được sau khi được chuyển sang ngữ cảnh mới. Ví dụ: khi chuyển một ArrayBuffer từ ứng dụng chính sang Worker, ArrayBuffer ban đầu sẽ bị xoá và không còn sử dụng được nữa. Nội dung của ứng dụng này (theo đúng nghĩa đen) được chuyển sang ngữ cảnh Worker.

Để sử dụng các đối tượng có thể chuyển, hãy dùng một chữ ký hơi khác của postMessage():

worker.postMessage(arrayBuffer, [arrayBuffer]);
window.postMessage(arrayBuffer, targetOrigin, [arrayBuffer]);

Trường hợp trình thực thi, đối số đầu tiên là dữ liệu và đối số thứ hai là danh sách các mục cần được chuyển. Đối số đầu tiên không nhất thiết phải là ArrayBuffer. Ví dụ: đối tượng đó có thể là một đối tượng JSON:

worker.postMessage({data: int8View, moreData: anotherBuffer},
                [int8View.buffer, anotherBuffer]);

Điểm quan trọng là: đối số thứ hai phải là một mảng ArrayBuffer. Đây là danh sách các mục có thể chuyển.

Để biết thêm thông tin về thiết bị có thể chuyển, hãy xem bài đăng của chúng tôi tại developer.chrome.com.

Môi trường nhân viên

Phạm vi trình thực thi

Trong ngữ cảnh một trình thực thi, cả selfthis đều tham chiếu phạm vi chung của trình thực thi đó. Do đó, ví dụ trước cũng có thể được viết là:

addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
    postMessage('WORKER STARTED: ' + data.msg);
    break;
case 'stop':
...
}, false);

Ngoài ra, bạn có thể đặt trực tiếp trình xử lý sự kiện onmessage (mặc dù addEventListener luôn được các Ninja JavaScript khuyến khích).

onmessage = function(e) {
var data = e.data;
...
};

Các tính năng dành cho nhân viên

Do hành vi đa luồng, Web Workers chỉ có quyền truy cập vào một nhóm nhỏ các tính năng của JavaScript:

  • Đối tượng navigator
  • Đối tượng location (chỉ có thể đọc)
  • XMLHttpRequest
  • setTimeout()/clearTimeout()setInterval()/clearInterval()
  • Bộ nhớ đệm của ứng dụng
  • Nhập tập lệnh bên ngoài bằng phương thức importScripts()
  • Tạo trình thực thi web khác

Worker KHÔNG có quyền truy cập vào:

  • DOM (không an toàn cho chuỗi)
  • Đối tượng window
  • Đối tượng document
  • Đối tượng parent

Đang tải tập lệnh bên ngoài

Bạn có thể tải thư viện hoặc tệp tập lệnh bên ngoài vào một worker bằng hàm importScripts(). Phương thức này không lấy hoặc nhiều chuỗi đại diện cho tên tệp để nhập tài nguyên.

Ví dụ sau tải script1.jsscript2.js vào worker:

worker.js:

importScripts('script1.js');
importScripts('script2.js');

Cũng có thể viết dưới dạng một câu lệnh nhập:

importScripts('script1.js', 'script2.js');

Nhân viên phụ

Worker có khả năng tạo ra worker con. Đây là tính năng tuyệt vời để chia nhỏ thêm các tác vụ lớn trong thời gian chạy. Tuy nhiên, những nhân viên phụ cũng có một số lưu ý:

  • Trình thực thi con phải được lưu trữ trong cùng một nguồn gốc với trang gốc.
  • URI trong các trình thực thi phụ được phân giải tương ứng với vị trí của worker mẹ (trái ngược với trang chính).

Xin lưu ý rằng hầu hết các trình duyệt đều tạo quy trình riêng cho mỗi worker. Trước khi bạn sinh sản một trang trại, hãy cẩn thận về việc chiếm đoạt quá nhiều tài nguyên hệ thống của người dùng. Lý do cho việc này là các thông báo được chuyển giữa các trang chính và worker được sao chép chứ không được chia sẻ. Xem phần Giao tiếp với Worker thông qua tính năng truyền thông báo.

Để xem ví dụ về cách tạo một trình thực thi phụ, hãy xem ví dụ trong phần thông số kỹ thuật.

Nhân viên nội tuyến

Điều gì sẽ xảy ra nếu bạn muốn tạo tập lệnh worker nhanh chóng hoặc tạo một trang độc lập mà không phải tạo tệp worker riêng? Với Blob(), bạn có thể "nội tuyến" worker trong cùng một tệp HTML dưới dạng logic chính bằng cách tạo một xử lý URL tới mã worker dưới dạng chuỗi:

var blob = new Blob([
"onmessage = function(e) { postMessage('msg from worker'); }"]);

// Obtain a blob URL reference to our worker 'file'.
var blobURL = window.URL.createObjectURL(blob);

var worker = new Worker(blobURL);
worker.onmessage = function(e) {
// e.data == 'msg from worker'
};
worker.postMessage(); // Start the worker.

URL Blob

Phép màu đi kèm với lệnh gọi đến window.URL.createObjectURL(). Phương thức này sẽ tạo một chuỗi URL đơn giản có thể dùng để tham chiếu dữ liệu được lưu trữ trong đối tượng DOM File hoặc Blob. Ví dụ:

blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

URL Blob là duy nhất và tồn tại trong suốt vòng đời của ứng dụng (ví dụ: cho đến khi document được huỷ tải). Nếu đang tạo nhiều URL Blob, bạn nên phát hành các tệp tham chiếu không còn cần thiết. Bạn có thể phát hành URL Blob một cách rõ ràng bằng cách truyền URL đó đếnwindow.URL.revokeObjectURL():

window.URL.revokeObjectURL(blobURL);

Trong Chrome, có một trang tuyệt vời để xem tất cả các URL blob đã tạo: chrome://blob-internals/.

Ví dụ đầy đủ

Tiến thêm một bước nữa, chúng ta có thể tinh chỉnh cách chèn mã JS của worker vào trang. Kỹ thuật này sử dụng thẻ <script> để xác định worker:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>

<div id="log"></div>

<script id="worker1" type="javascript/worker">
// This script won't be parsed by JS engines
// because its type is javascript/worker.
self.onmessage = function(e) {
    self.postMessage('msg from worker');
};
// Rest of your worker code goes here.
</script>

<script>
function log(msg) {
    // Use a fragment: browser will only render/reflow once.
    var fragment = document.createDocumentFragment();
    fragment.appendChild(document.createTextNode(msg));
    fragment.appendChild(document.createElement('br'));

    document.querySelector("#log").appendChild(fragment);
}

var blob = new Blob([document.querySelector('#worker1').textContent]);

var worker = new Worker(window.URL.createObjectURL(blob));
worker.onmessage = function(e) {
    log("Received: " + e.data);
}
worker.postMessage(); // Start the worker.
</script>
</body>
</html>

Theo tôi, phương pháp mới này rõ ràng và dễ đọc hơn một chút. Định nghĩa này xác định thẻ tập lệnh bằng id="worker1"type='javascript/worker' (để trình duyệt không phân tích cú pháp JS). Mã đó được trích xuất dưới dạng một chuỗi bằng cách sử dụng document.querySelector('#worker1').textContent và chuyển đến Blob() để tạo tệp.

Đang tải tập lệnh bên ngoài

Khi sử dụng các kỹ thuật này để chèn mã worker cùng dòng, importScripts() sẽ chỉ hoạt động nếu bạn cung cấp URI tuyệt đối. Nếu bạn cố gắng truyền một URI tương đối, thì trình duyệt sẽ báo lỗi bảo mật. Lý do là: worker (hiện được tạo từ một URL của blob) sẽ được giải quyết bằng tiền tố blob:, trong khi ứng dụng sẽ chạy từ một giao thức khác (có thể là http://). Do đó, lỗi này là do các quy định hạn chế trên nhiều nguồn gốc.

Một cách để sử dụng importScripts() trong trình thực thi cùng dòng là "chèn" URL hiện tại của tập lệnh chính đang chạy bằng cách truyền tệp đó vào trình thực thi cùng dòng và tạo URL tuyệt đối theo cách thủ công. Việc này giúp đảm bảo tập lệnh bên ngoài được nhập từ cùng một nguồn gốc. Giả sử ứng dụng chính của bạn đang chạy từ http://example.com/index.html:

...
<script id="worker2" type="javascript/worker">
self.onmessage = function(e) {
var data = e.data;

if (data.url) {
var url = data.url.href;
var index = url.indexOf('index.html');
if (index != -1) {
    url = url.substring(0, index);
}
importScripts(url + 'engine.js');
}
...
};
</script>
<script>
var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
worker.postMessage(<b>{url: document.location}</b>);
</script>

lỗi xử lý

Giống như mọi logic JavaScript, bạn nên xử lý mọi lỗi được tạo ra trong trình thực thi web. Nếu xảy ra lỗi khi một worker đang thực thi, thì ErrorEvent sẽ được kích hoạt. Giao diện này chứa 3 thuộc tính hữu ích để tìm ra lỗi: filename – tên của tập lệnh trình thực thi gây ra lỗi, lineno – số dòng xảy ra lỗi và message – nội dung mô tả có ý nghĩa về lỗi. Dưới đây là ví dụ về cách thiết lập trình xử lý sự kiện onerror để in các thuộc tính của lỗi:

<output id="error" style="color: red;"></output>
<output id="result"></output>

<script>
function onError(e) {
document.getElementById('error').textContent = [
    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
].join('');
}

function onMsg(e) {
document.getElementById('result').textContent = e.data;
}

var worker = new Worker('workerWithError.js');
worker.addEventListener('message', onMsg, false);
worker.addEventListener('error', onError, false);
worker.postMessage(); // Start worker without a message.
</script>

Ví dụ: workerWithError.js cố gắng thực hiện 1/x, trong đó x không xác định.

// VIỆC CẦN LÀM: DevSite - Mã mẫu bị xoá vì đã sử dụng trình xử lý sự kiện nội tuyến

workerWithError.js:

self.addEventListener('message', function(e) {
postMessage(1/x); // Intentional error.
};

Lưu ý về tính bảo mật

Các hạn chế đối với quyền truy cập tại địa phương

Do các hạn chế bảo mật của Google Chrome, các worker sẽ không chạy trên máy (ví dụ: từ file://) trong các phiên bản trình duyệt mới nhất. Thay vào đó, họ âm thầm thất bại! Để chạy ứng dụng qua lược đồ file://, hãy chạy Chrome với cờ --allow-file-access-from-files đã được đặt.

Các trình duyệt khác không áp dụng quy định hạn chế tương tự.

Những điểm cần cân nhắc về cùng nguồn gốc

Tập lệnh trình chạy phải là các tệp bên ngoài có cùng lược đồ với trang gọi. Do đó, bạn không thể tải tập lệnh từ URL data: hoặc URL javascript:, và trang https: không thể bắt đầu tập lệnh worker bắt đầu bằng URL http:.

Trường hợp sử dụng

Vậy loại ứng dụng nào sẽ sử dụng nhân viên web? Sau đây là một vài ý tưởng khác khiến bạn phải não phân vân:

  • Tìm nạp trước và/hoặc lưu dữ liệu vào bộ nhớ đệm để sử dụng sau này.
  • Làm nổi bật cú pháp mã hoặc định dạng văn bản khác theo thời gian thực.
  • Trình kiểm tra chính tả.
  • Phân tích dữ liệu video hoặc âm thanh.
  • I/O trong nền hoặc cuộc thăm dò ý kiến các dịch vụ web.
  • Xử lý các mảng lớn hoặc phản hồi JSON gây khó chịu.
  • Lọc hình ảnh trong <canvas>.
  • Cập nhật nhiều hàng của cơ sở dữ liệu web cục bộ.

Để biết thêm thông tin về các trường hợp sử dụng liên quan đến API Web Workers, hãy truy cập vào phần Tổng quan về worker.

Bản thu thử

Tài liệu tham khảo