Это первые 130 не поливают оригинал, я хочу получить более хороший оригинальный текст, поиск числа общественности, касающихся нас ~ Эта статья впервые появилась в правительстве для принятия блога облачного интерфейса:Анализ и размышления о Comlink, мощном инструменте для многопоточной веб-разработки.
предисловие
JavaScript — однопоточный язык, и все задачи выполняются в основном потоке, и если основной поток заблокирован, последующие задачи выполняться не могут. Поскольку это один поток, почему мы субъективно воспринимаем его как «многопоточный» во время использования?
цикл событий
В основном потому, что JavaScript предоставляетцикл событийМеханизм, после того, как мы инициируем асинхронный запрос или операцию синхронизации, обработанный обратный вызов будет помещен в очередь задач. Когда стек выполнения пуст, будет обработан обратный вызов в очереди задач, поэтому основной поток не будет заблокирован. к следующему рисунку:
Среды Node и Deno также используют механизм цикла обработки событий, но в модели есть различия. Конкретные детали цикла событий не будут подробно рассматриваться в этой статье, но основная идея такова:Очередь задач + асинхронный обратный вызов.
На самом деле, даже если есть механизм цикла событий, некоторые задачи все равно будут сильно занимать основной поток, например, почти бесконечный цикл, который напрямую приведет к тому, что ЦП будет занимать 100%.В это время все последующие задачи блокируются, страница зависает или даже не отвечает, что очень недружелюбно с точки зрения взаимодействия с пользователем. Но часто такие задачи неизбежны, и мы обычно делим их на две категории:
- С интенсивным использованием ЦП: время, необходимое для завершения вычислений, в основном ограничено вычислением ЦП.
Просмотр логического процессора Количество сердечников
navigator.hardwareConcurrency // 16
С вышеуказанными предпосылками мы можем вызвать эти обструктивные многопоточные обработки задач.
Web Worker
- DOM Ограничения, часть BOM Ограничения
- ограничение гомологии
- Общайтесь через механизм прослушивания сообщений
- Доступ к файлу сценария должен осуществляться через сеть
- международная практика,ресурсСвоевременно после использованияосвобожден
очень простой пример
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<p id="first"></p>
<p id="second"></p>
<p id="third"></p>
<script>
// 第一个文本
document.querySelector('#first').innerHTML = 'First'
// 第二个文本
const second = document.querySelector('#second')
if (window.Worker) {
second.innerHTML = '...'
const worker = new Worker('worker.js')
worker.postMessage({
uuid: new Date().getTime()
});
worker.onmessage = function(e) {
second.innerHTML = e.data
}
worker.onerror = function(e) {
second.innerHTML = 'Error occured!'
}
} else {
second.innerHTML = 'Not supprot Web Worker!'
}
// 第三个文本
document.querySelector('#third').innerHTML = 'Third'
</script>
</body>
</html>
worker.js
onmessage = function(e) {
const time = Math.random() * 3000
// 模拟复杂计算
setTimeout(() => {
postMessage(`Second ${time.toFixed(0)} ms, ID is ${e.data.uuid}`)
}, time)
}
Посмотреть код(Примечание. В этом и последующих примерах кодов скрипт будет загружаться с помощью Blob-to-URL)
Видно, что основной поток в основном отвечает за отображение UI, а рабочий поток отвечает за вычисление значений, которые необходимо отобразить, поэтому проблема в следующем:
- Может ли серверная часть вернуть этот шаг вычисления отображаемого значения?
- Как сделать несколько операций в отдельных потоках?
На вопрос 1 ответ да.Многопоточность во внешнем интерфейсе предназначена только для расширения вычислительной мощности современных браузеров.Обычно эта часть имеет большой неиспользованный потенциал и может использоваться для многих вещей, таких как генерация файлов, сложные вычисления , так далее. Если вы этого не сделаете, очевидно, что это можно сделать с помощью асинхронных запросов.
Для вопроса 2, если вы объявите несколько в отдельных рабочих потокахonmessage
Функция, согласно правилам продвижения переменных, вступает в силу только последняя. Затем, если вы хотите выполнять различные операции, помимо открытия нового рабочего потока (бессмысленного), вы можете передать его только в эту функцию прослушивателя.switch
илиif
Вернуться, так что однократное нарушение принципа ответственности.
// 若要在线程脚本中执行多个操作,通常需要这么写
onmessage = function(e) {
if (condition1) // do something
if (condition2) // do something
if (condition3) // do something
...
}
В дополнение к рабочим потокам, в основном потоке есть такая проблема, потому что событие Message может быть связано только один раз, вы хотите выполнить сложное условное суждение, что делает код чрезвычайно безобразно раздутым, затемНасколько элегантно использовать многопоточное развитиеШерстяная ткань?
Для только что упомянутой проблемы мы можем вернуть в виде асинхронного интерфейса желаемые результаты благодаря объектам Promise ES6, обычно для нашего асинхронного формулируемого следующим образом:
fetchSometing().then(res => {
// do something
})
Затем сравните формулировку Web Worker:
worker.postMessage();
worker.onmessage = function(e) {
// do something
}
Представьте, сможем ли мы дополнительно оптимизировать многопоточный метод записи,postMessage
а такжеonmessage
Инкапсулируйте его в функцию, которая возвращает обещание и выполняет «асинхронную» операцию, вызывая?
Это, безусловно, возможно. Итак, эта функция должна быть в рабочем потоке, как нам вызвать функцию в рабочем потоке для работы?
RPC:Remote Procedure Call, удаленный вызов процедуры, относится к вызову метода, отличного от текущего контекста, обычно к другому потоку, домену, сетевому узлу и вызову через предоставленный интерфейс.
С помощью RPC мы можем добиться того, чего хотим. Здесь мы представим главного героя этой статьиComlink!
Если нет условий, мы должны создавать условия
Comlink
Comlink — это проект с открытым исходным кодом от Google Chrome Labs, который предоставляет возможности PRC для внешнего многопоточного программирования.
Comlink makes WebWorkers enjoyable.
Взгляните на простейший пример, предоставленный проектом:
main.js
// <script src="https://unpkg.com/comlink/dist/umd/comlink.js"></script>
async function init() {
const worker = new Worker("worker.js");
const obj = Comlink.wrap(worker);
alert(`Counter: ${await obj.counter}`);
await obj.inc();
alert(`Counter: ${await obj.counter}`);
}
init();
worker.js
importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
const obj = {
counter: 0,
inc() {
this.counter++;
},
};
Comlink.expose(obj);
Очевидно, что Comlink «RPC» возможности именно то, что мы хотим, обратите внимание на пример выше ключевых моментовComlink.wrap(worker)
а такжеComlink.expose(obj)
, который таким образом предоставляет контекст в сценарии рабочего потока среде основного потока Давайте узнаем о его конкретной реализации, взглянув на часть основного кода.
Анализ исходного кода
Первый взглядwrap
Конкретная реализация функции:
// 包装函数
export function wrap<T>(ep: Endpoint, target?: any): Remote<T> {
return createProxy<T>(ep, [], target) as any;
}
// 由函数名可见,返回的是一个 Proxy
function createProxy<T>(
ep: Endpoint,
path: (string | number | symbol)[] = [],
target: object = function () {}
): Remote<T> {
let isProxyReleased = false;
// 从以下大体的结构可以看出,Proxy 分别代理了 get、set、apply、construct 等操作
const proxy = new Proxy(target, {
// 举例 get 操作
get(_target, prop) {
// ...
// 由于 await 的原因,最后会对 'then' 属性进行访问
if (prop === "then") {
if (path.length === 0) {
return { then: () => proxy };
}
// 请看文章后续部分
const r = requestResponseMessage(ep, {
type: MessageType.GET,
path: path.map((p) => p.toString()),
}).then(fromWireValue);
return r.then.bind(r);
}
// 如果访问 obj.counter 时,重新调用 createProxy 方法,此时返回一个新的 Proxy
// 需要注意 path,代表了当前访问属性的深度,如 obj.counter.a.b.c 时,path 为 ['counter', 'a', 'b', 'c']
// path 在 expose 方法中需要用到
return createProxy(ep, [...path, prop]);
},
set(_target, prop, rawValue) {
// ...
},
apply(_target, _thisArg, rawArgumentList) {
// ...
},
construct(_target, rawArgumentList) {
// ...
},
});
return proxy as any;
}
Как можно заметить,wrap
Возвращается объект Proxy, и проксиget
,set
,apply
,construct
四种不同的操作。 Такие какobj.counter
Операция вернет новый объект Proxy.
Здесь следует отметить, что,await obj.counter
then
if (prop === "then")
requestResponseMessage
функция:
function requestResponseMessage(
ep: Endpoint,
msg: Message,
transfers?: Transferable[]
): Promise<WireValue> {
return new Promise((resolve) => {
const id = generateUUID();
// 消息监听
ep.addEventListener("message", function l(ev: MessageEvent) {
if (!ev.data || !ev.data.id || ev.data.id !== id) {
return;
}
ep.removeEventListener("message", l as any);
resolve(ev.data);
} as any);
// 若使用 onMessage,则不需要主动开启
if (ep.start) {
ep.start();
}
ep.postMessage({ id, ...msg }, transfers);
});
}
знакомыйaddEventListener
а такжеpostMessage
由呈现在眼前,所以当访问代理对象上的属性时,其实是发送了 GET 消息到工作线程,把真实值通过消息返回,形成看上去是本地调用的假象。
увидеть сноваexpose
Конкретная реализация функции:
export function expose(obj: any, ep: Endpoint = self as any) {
// 消息监听
ep.addEventListener("message", function callback(ev: MessageEvent) {
if (!ev || !ev.data) {
return;
}
// id: 每一次消息的 ID,通过上述 generateUUID 生成
// type: 操作类型,如 get 为 MessageType.GET
// path: 访问对象层级,wrap 中有详述
const { id, type, path } = {
path: [] as string[],
...(ev.data as Message),
};
const argumentList = (ev.data.argumentList || []).map(fromWireValue);
let returnValue;
try {
const parent = path.slice(0, -1).reduce((obj, prop) => obj[prop], obj);
// 根据 path 取到 obj 相应层级的值
const rawValue = path.reduce((obj, prop) => obj[prop], obj);
switch (type) {
// 举例 get 操作
case MessageType.GET:
{
returnValue = rawValue;
}
break;
case MessageType.SET:
// ...
case MessageType.APPLY:
// ...
case MessageType.CONSTRUCT:
// ...
case MessageType.ENDPOINT:
// ...
case MessageType.RELEASE:
// ...
break;
default:
return;
}
} catch (value) {
returnValue = { value, [throwMarker]: 0 };
}
Promise.resolve(returnValue)
.catch((value) => {
return { value, [throwMarker]: 0 };
})
.then((returnValue) => {
// 忽略,感兴趣可以参看源码
const [wireValue, transferables] = toWireValue(returnValue);
// 将处理完后的数据返回
ep.postMessage({ ...wireValue, id }, transferables);
if (type === MessageType.RELEASE) {
// 释放处理
ep.removeEventListener("message", callback as any);
closeEndPoint(ep);
}
});
} as any);
// 若使用 onMessage,则不需要主动开启
if (ep.start) {
ep.start();
}
}
self
указывает на контекст рабочего потока,addEventListener
а такжеstart
Начать отправку и прослушивание очередей сообщений, характера и методовonmessageпоследовательно, что также подтверждаетwrap
Идея — взять значение объекта в контексте рабочего потока и вернуть его через сообщение.
Только здесь будет видно, а операции, которые перехватывались из структуры Switch-Case и прокси-объекта, видно, что будут обрабатываться разные операции, и в этой статье подробно не будет.
Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'Worker': xxx could not be cloned.
сообщить об ошибке.Перезвонитеа такжеобщий поток
Случай: Экспорт в Excel
У предприятий часто есть такой спрос, экспортировать отчеты Excel. Технологии обычно возвращаются внутренним файловым потоком и загружают файл для создания внешнего интерфейса, который также принимает во внимание проблемы с производительностью. На самом деле, при благословлении многопоточного, чистого фронтенда также можно добиться, написав следующий код Comlink (100 000 данных):
main.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://unpkg.com/comlink/dist/umd/comlink.js"></script>
<script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
<script src="https://unpkg.com/file-saver/dist/FileSaver.min.js"></script>
</head>
<body>
<button id="btn">Download</button>
<p id="time"></p>
<script>
const button = document.querySelector('#btn');
const worker = new Worker("worker.js");
// 使用 Comlink 包装
const getWorkBook = Comlink.wrap(worker);
// 点击触发下载
async function download() {
button.disabled = true;
// 生成 xlsx 文档的 blob 数据
const blob = await getWorkBook(100000);
// 下载
saveAs(blob, "test.xlsx");
button.disabled = false;
};
button.addEventListener('click', download);
// 观察时间是否卡顿
setInterval(() => {
document.querySelector('#time').innerHTML = new Date().toLocaleTimeString();
}, 1000);
</script>
</body>
</html>
worker.js
importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
importScripts("https://unpkg.com/xlsx/dist/xlsx.full.min.js");
// 模拟生成 Excel 并导出
const getWorkBook = (count) => {
const aoa = [];
for (let i = 0; i < count; i++) {
const arr = []
for (let j = 0; j < 10; j++) {
if (i === 0) {
arr.push(`Column${j + 1}`);
continue;
}
arr.push(Math.floor(Math.random() * 100));
}
aoa.push(arr);
}
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(aoa);
XLSX.utils.book_append_sheet(wb, ws, 'Sheet');
// XLSX.writeFile 无法获取 DOM,故采用此写法
const data = XLSX.write(wb, { type: 'array' });
return new Blob([data],{type:"application/octet-stream"});
};
Comlink.expose(getWorkBook);
Видно, что код, использующий Comlink, очень чистый и его очень легко расширять (например: читать Excel и анализировать)!
Кстати, сравнение эффекта от неиспользования многопоточности можно сказать очень наглядное:
думать
Что касается болевых точек многопоточного кодирования, Comlink разумно дополнительно инкапсулирует свой внешний уровень, скрывает внутреннюю коммуникационную логику и реализует режим RPC. В реальном процессе разработки мы часто сталкиваемся с таким методом связи, основанным на событии сообщения, напримерiframe
,window.open
а такжеwindow.opener
, Теоретически к этим сценариям можно применить реализацию Comlink.
Вначале расширение логики функций с помощью switch-case или условного суждения часто было первым решением, о котором мы могли подумать, потому что оно нарушает принцип единой ответственности и от него безжалостно отказываются. Однако, если этот метод может достичь определенной степени сплоченности, он часто будет иметь неожиданные эффекты, и этот метод дизайн-мышления также может быть применен к другим областям.
Ссылка на ссылку
Рекомендуемое чтение
работы с открытым исходным кодом
- Zhengcaiyun интерфейсный таблоид
адрес с открытым исходным кодомwww.zoo.team/openweekly/(На главной странице официального сайта таблоида есть группа обмена WeChat)
адрес с открытым исходным кодомGitHub.com/Chinese Patent Medicine-Inc/Reservoir…
Карьера
ZooTeam, молодая, увлеченная и творческая команда, связанная с отделом исследований и разработок продукции Zhengcaiyun, базируется в живописном Ханчжоу. В настоящее время в команде более 60 фронтенд-партнеров, средний возраст которых составляет 27 лет, и почти 40% из них — инженеры полного стека, настоящая молодежная штурмовая группа. В состав членов входят «ветераны» солдат из Ali и NetEase, а также первокурсники из Чжэцзянского университета, Университета науки и технологий Китая, Университета Хандянь и других школ. В дополнение к ежедневным деловым связям, команда также проводит технические исследования и фактические боевые действия в области системы материалов, инженерной платформы, строительной платформы, производительности, облачных приложений, анализа и визуализации данных, а также продвигает и внедряет ряд внутренних технологий. Откройте для себя новые горизонты передовых технологических систем.
Если вы хотите измениться, вас забрасывают вещами, и вы надеетесь начать их бросать; если вы хотите измениться, вам сказали, что вам нужно больше идей, но вы не можете сломать игру; если вы хотите изменить , у вас есть возможность добиться этого результата, но вы не нужны; если вы хотите изменить то, чего хотите достичь, вам нужна команда для поддержки, но вам некуда вести людей; если вы хотите изменить установившийся ритм, это будет "5 лет рабочего времени и 3 года стажа работы"; если вы хотите изменить исходный Понимание хорошее, но всегда есть размытие того слоя оконной бумаги.. , Если вы верите в силу веры, верьте, что обычные люди могут достичь необыкновенных вещей, и верьте, что они могут встретить лучшего себя. Если вы хотите участвовать в процессе становления бизнеса и лично способствовать росту фронтенд-команды с глубоким пониманием бизнеса, надежной технической системой, технологиями, создающими ценность, и побочным влиянием, я думаю, что мы должны говорить. В любое время, ожидая, пока вы что-нибудь напишете, отправьте это наZooTeam@cai-inc.com