使用Web Worker改善性能

開始之前,代碼在這里壁榕。歡迎各位大神指導(dǎo)。

在 Web Worker 之前赎瞎,解析 CSS牌里,生成布局,繪制界面以及運行 javascript 腳本都運行在瀏覽器的一個線程里务甥。如果一個 Web App 運行的 js 腳本一次運行時間過長牡辽,就會出現(xiàn)界面卡頓。這樣的用戶體驗是沒法用及格來評價的敞临。

在多核普及的當(dāng)下态辛,瀏覽器也大多支持了 Web Worker。這讓前端開發(fā)有了更多的選擇挺尿,使用 web worker 來實現(xiàn)真正的多線程奏黑。所有阻礙炊邦、延遲用戶反饋的操作都可以移入一個后臺運行的線程中。

如何發(fā)現(xiàn)性能瓶頸

筆者主要使用 React 開發(fā)熟史,所以首先聊一下 React 中如何發(fā)現(xiàn)性能出現(xiàn)問題的地方馁害。在 React16.9 中新增了 Profiler API,使用起來也非常簡單蹂匹,具體可以查看文檔((https://reactjs.org/docs/profiler.html))碘菜。

更通用一點的可以使用 chrome 的lighthouse∠弈可以安裝插件 lighthouse 插件忍啸,或者也可以直接打開開發(fā)面板的audits點擊run audits,就能看到報表了履植。console.timeconsole.timeEnd組合吊骤。

但是,以上只適用于開發(fā)模式下使用静尼。在生產(chǎn)上使用多多少少會給產(chǎn)品本身帶來額外的資源消耗白粉。一般來說,app 都會有埋點鼠渺,在埋點點時候如何順道達成性能消耗點記錄就需要具體問題具體分析了鸭巴。

Web Worker

使用 Web Worke 讓阻塞代碼在后臺運行,自然不會阻塞 UI 線程(main thread)拦盹。在例子中所用到的是typescript版本的代碼鹃祖,所有后面如果有必要會給出在 typescript 實現(xiàn)的代碼和相應(yīng)的說明。配置一類的文件請直接移步到代碼目錄查看普舆,這里就不多說了恬口。

在 Worker 的部分使用了webpack + worker-loader的方式。worker-loader的具體內(nèi)容可以參考這里沼侣。

創(chuàng)建一個 Worker

創(chuàng)建一個 Worker 非常的簡單祖能,只需要把一段命名腳本傳給Worker構(gòu)造函數(shù)就可以。比如 MDN 的一段:

這是 Worker 腳本:

// worker.js
self.onmessage = event => {
  console.log("Message received", event.data);
  self.postMessage("Worker done");
};

在 typescript 里蛾洛,首先需要處理 Worker 的上下文的問題养铸,否則tsc編譯不過。

const ctx: DedicatedWorkerGlobalScope = self as any;

ctx.onmessage = (event: MessageEvent) => {
  //...

  ctx.postMessage("done");

  // Close the worker when jobs done
  ctx.close();
};

ctx.onerror = (event: ErrorEvent): any => {
  console.error("Error in worker", event.message);
  ctx.close();
};

export default null as any;

注意:這里需要使用DedicatedWorkerGlobalScope不能直接食欲哦那個Worker轧膘,因為Worker的定義里面沒有close方法钞螟。這是因為close方法deprecated

TS2339: Property 'close' does not exist on type 'Worker'.

還有在創(chuàng)建 Worker 的最后谎碍,需要一個export語句:

export default null as any;

創(chuàng)建 Worker:

// Main thread
var myWorker = new Worker("worker.js");

myWorker.postMessage([first.value, second.value]);

myWorker.onmessage = function(e) {
  result.textContent = e.data;
  console.log("Message received from worker");
};

Typescript:

import SimpleWorker from "./simple.worker";
const worker = new SimpleWorker();

兩個線程之間(上例是 UI thread 和一個 worker)可以通過postMessageonmessage或者(addEventListener('message', () => {})的方式來傳遞消息鳞滨。

線程之間的通信是基于事件的。那么錯誤的處理也是同樣道理蟆淀,例如:

// UI thread
var myWorker = new Worker("worker.js");

myWorker.onerror = function() {
  console.log("There is an error with your worker!");
};
// Inside worker
self.onerror = err => {
  console.error("Error in worker", err);
};

引入外部腳本

importScripts(); /* imports nothing */
importScripts("foo.js"); /* imports just "foo.js" */
importScripts("foo.js", "bar.js"); /* imports two scripts */
importScripts(
  "http://example.com/hello.js"
); /* You can import scripts from other origins */

注意:下載順序可以是任意順序拯啦,但是執(zhí)行的順序是按照腳本在importScripts方法里出現(xiàn)的順序澡匪。

因為使用了worker-loader,在引入外部代碼的時候提岔,和一般的import差不多:

import { ab2str, str2ab } from "./lib/utils"; // 引入內(nèi)部依賴
import * as _ from "lodash"; // 引入外部依賴

ctx.onmessage = (event: MessageEvent) => {
  // ...
  const dataStr = ab2str(dataBuff); // 使用內(nèi)部依賴

  // ...
  const target = JSON.parse(dataStr || '[]');
  const v = _.get(target, 'a.b', 'N/A');  // 使用外部依賴

  // ...

關(guān)閉一個 Worker

Worker 也占用和消耗資源仙蛉,所以在不用的時候就要關(guān)閉它。

關(guān)閉一個 Worker 有兩種方法:一種是直接在 UI thread 里面使用terminate方法碱蒙,一種是在 Worker 的內(nèi)部調(diào)用close方法荠瘪。

// In main thread
const worker = new Worker("myworker.js");

// If it's the time to terminate a worker
worker.terminate();

在調(diào)用了terminate方法之后,Worker 會被立刻終止,即使是還在運行中的也是一樣。但是一般情況下還是希望在 Worker 執(zhí)行完成之后才去關(guān)閉纺念。這個時候就要用到 Worker 的close方法虱疏。

// In a worker
self.onmessage = event => {
  self.close();
};

如上文所說馏予,close方法就要被廢棄了,現(xiàn)在是在deprecated的狀態(tài)。具體看 MDN 的這里

要被廢棄是因為,在一個 worker 出了作用域之后就會被回收吠各。所以有沒有close這個方法并沒有太大的必要。

Inline Worker

Worker 的創(chuàng)建需要得到腳本的 URL 地址勉抓。一般情況下贾漏,這段腳本是放在 server 上的。這就需要網(wǎng)絡(luò)的傳輸藕筋。如果只是一個簡單的需要放到后臺執(zhí)行的腳本纵散,如果可以打包到一起直接發(fā)布到客戶瀏覽器會節(jié)省很多的時間。這個時候就需要 inline Worker隐圾。

它的創(chuàng)建也很簡單伍掀,并沒有什么特別的地方。只是在獲得 URL 的時候使用了Blob這個工具暇藏,如:

// URL.createObjectURL
window.URL = window.URL || window.webkitURL;

// "Server response", used in all examples
var response = "self.onmessage=function(e){postMessage('Worker: '+e.data);}";

var blob;
try {
  blob = new Blob([response], { type: "application/javascript" });
} catch (e) {
  // Backwards-compatibility
  window.BlobBuilder =
    window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
  blob = new BlobBuilder();
  blob.append(response);
  blob = blob.getBlob();
}
var worker = new Worker(URL.createObjectURL(blob));

// Test, used in all examples:
worker.onmessage = function(e) {
  alert("Response: " + e.data);
};
worker.postMessage("Test");

在 react hook 和 Worker 結(jié)合的一個 npm 包里就有過使用這種方法的代碼蜜笤。簡單的把用戶的 task(一個方法)轉(zhuǎn)成字符串,之后通過Blob得到一個 URL 來創(chuàng)建出一個 Worker叨咖。其他使用 react hook 的工作機制通知 task 執(zhí)行的結(jié)果瘩例。非常的簡單有效。代碼在這里甸各。

節(jié)選部分代碼,以饗讀者:

const createWorker = func => {
  if (func instanceof Worker) return func;
  if (typeof func === "string" && func.endsWith(".js")) return new Worker(func);
  const code = [
    `self.func = ${func.toString()};`,
    "self.onmessage = async (e) => {",
    "  const r = self.func(e.data);",
    "  if (r[Symbol.asyncIterator]) {",
    "    for await (const i of r) self.postMessage(i)",
    "  } else if (r[Symbol.iterator]){",
    "    for (const i of r) self.postMessage(i)",
    "  } else {",
    "    self.postMessage(await r)",
    "  }",
    "};"
  ];
  const blob = new Blob(code, { type: "text/javascript" });
  const url = URL.createObjectURL(blob);
  return new Worker(url);
};

現(xiàn)在這部分代碼都交給 webpack 都插件來做了焰坪。

Web Worker 不能做什么

首先 Web Worker 不能訪問 UI thread 的 UI趣倾,也就是 DOM。
如果一個 Web Worker 可以訪問 DOM某饰,那加上 UI thread 就是兩個或者兩個以上的 Worker 可以訪問 DOM 了儒恋,那就會出現(xiàn)非常麻煩的多線程特有的問題善绎,而且調(diào)試困難。所以 DOM 肯定是不能訪問的诫尽。

其他的還有很多限制可以參考這里

但是禀酱,還是可以發(fā)出網(wǎng)絡(luò)請求,可以setTimeout, setInterval牧嫉,還是可以使用CacheIndexedDB等等一些功能等剂跟。

Worker 雖好,也不能開的太多酣藻。Worker 是真正系統(tǒng)級的線程曹洽,要運行起來就需要有支撐的資源。在 Worker 之間傳輸?shù)臄?shù)據(jù)不能太大辽剧。為了避免多個 Thread 共享內(nèi)存而導(dǎo)致的多線程問題送淆,WeW Worker 傳輸數(shù)據(jù)的時候使用了兩個方式:

  1. 在多個 Worker 之間傳輸?shù)臄?shù)據(jù)是拷貝傳輸?shù)摹i_發(fā)者不需要考慮這段數(shù)據(jù)的鎖保護之類的事情怕轿。
  2. 以拷貝的方式傳輸數(shù)據(jù)偷崩,數(shù)據(jù)量過大的時候拷貝消耗的資源也會很大。這個時候就要考慮使用Transferable Object撞羽。這種類型的數(shù)據(jù)在傳輸?shù)臅r候基本不存在復(fù)制的動作阐斜,可以認為是 c++里的引用傳遞。不同的是 Worker 的Transferable Object在傳遞出去之后就當(dāng)前上下文里即不可訪問放吩。
// Create a 32MB "file" and fill it.
var uInt8Array = new Uint8Array(1024 * 1024 * 32); // 32MB
for (var i = 0; i < uInt8Array.length; ++i) {
  uInt8Array[i] = i;
}

worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);

舉個栗子

我們來把一個字符串反轉(zhuǎn)多次來模擬 CPU “繁重”的任務(wù)智听。這個栗子分為三部分一個是運行在 UI thread 上看看會有多卡,一個是運行在Promise里渡紫,看看會有什么不同的結(jié)果到推。數(shù)據(jù)全部都是基于我們的栗子來得到,對于讀者來說由于有些網(wǎng)絡(luò)惕澎、硬件等情況不同或者不完全可控會有不同莉测,定量分析不會那么準確,定性分析有一定的代表性唧喉。

同時捣卤,這個試驗和樣本的數(shù)量關(guān)系十分密切。在樣本足夠打的時候八孝,試驗只會收到異常董朝。

測試數(shù)據(jù)是怎么來的

const ITERATE_COUNT = 1000;
const STR_LEN = 3;

let queue: TaskQueue | null = null;

function prepareData(count: number = 1000, length: number = 10) {
  const data: Array<DataType> = [];
  for (let i = 0; i < count; i++) {
    const item = RandomString.generate(length);
    data.push({ key: `Key - ${i}`, val: item });
  }

  return data;
}

const rawData = prepareData(ITERATE_COUNT, STR_LEN);
(window as any).rawData = rawData;

上面的方法生成了 1000 個長度是 10 的字符串。在后面的例子里會把這些字符串全部反轉(zhuǎn)干跛。以此來模擬某種業(yè)務(wù)場景下繁重的 CPU 任務(wù)子姜。

例一、在主線程

代碼:

// Demo 1: execute reverse string in ui thread
function execTaskSync() {
  console.time("sync task in ui thread");

  const target = rawData;
  for (let el of target) {
    const { val } = el;
    reverseString(val);
  }

  console.timeEnd("sync task in ui thread");
}

(window as any).execTaskSync = execTaskSync;

這個任務(wù)量其實不夠大楼入,只會產(chǎn)生一個和后面例子對比的效果哥捕。先運行一下看看結(jié)果:

ui thread

運行結(jié)果看起來很快牧抽,如果需要更慢一些只需要把字符串?dāng)?shù)量或者字符串的長度調(diào)大就可以。運行的結(jié)果基本都在 0.xx ms 的范圍內(nèi)遥赚,只有一個是 2.27 ms扬舒。這也許只是一個現(xiàn)象,也許就很值得深究了凫佛。

在 Worker 運行

是時候讓這個功能在 worker 里面運行一次了:

ctx.onmessage = (event: MessageEvent) => {
  console.time("worker timer");

  const { target } = event.data as { target: DataType[] };
  for (let el of target) {
    const { val } = el;
    reverseString(val);
  }

  console.timeEnd("worker timer");

  ctx.postMessage("done");

  // Close the worker when jobs done
  self.close();
};

數(shù)據(jù)全部傳過來之后讲坎,在 worker 連運行。結(jié)果是這樣的:


in a worker

在 Micro Queue 運行

看起來是一個 queue御蒲,不過是一個個 Promise 接連運行的衣赶。在本例中只有一個 Promise 運行。

Queue 是什么樣的 Queue:

class Queue {
  private _startExec() {
    const task = this._queue.shift();
    if (task) task.run();
  }

  next() {
    if (this._queue.length === 0) {
      return;
    }

    this._startExec();
  }

  async addTask(
    fun: (param: any) => any,
    data: any,
    resolve: (val: any) => void,
    reject: (err: any) => void
  ) {
    const run = async () => {
      try {
        const ret = await fun(data);
        resolve(ret);
      } catch (e) {
        reject(e);
      }

      this.next();
    };

    this._queue.push({ run } as Task);
    this._startExec();
  }
}

這個是在 Queue 里添加 task 的方法厚满,在添加的時候就會在 task 運行完成之后調(diào)用 Queue 的 next 方法來開始下一個 task府瞄。

在數(shù)據(jù)量同樣的情況下運行的結(jié)果:


In queue

看起來和在主線程的運行結(jié)果相當(dāng)?shù)慕咏恕N覀儊戆褦?shù)據(jù)量加大看看會有什么結(jié)果碘箍。

const ITERATE_COUNT = 100000;
const STR_LEN = 300;

先把數(shù)量級提升到這個程度遵馆。

多次運行之后,主線程和放在 Promise 里的方式差別依然不大丰榴,只是在按鈕點擊之后明顯的增加了等待的時間货邓。在 Worker 里運行的花費時間比之主線程依然更多,但是按鈕點擊之后的等待時間并沒有相應(yīng)的更多等待四濒。

Run in Worker with Buffer

這就體現(xiàn)出 Worker 存在的意義了换况。相應(yīng)用戶點擊的速度一定會快很多。這個時候就需要Buffer出場了盗蟆。我們來測試一下使用了 Buffer 的 Worker 會出現(xiàn)什么樣的驚喜戈二。

worker-buffer

明顯在第一次消耗了很多時間之后,每次的調(diào)用都消耗了比直接調(diào)用 Worker 的postMessage更少的時間喳资。使用 Buffer 來實現(xiàn)不同 Worker 之間傳輸數(shù)據(jù)就像是 C/C++的引用傳遞一樣觉吭,這里不會涉及到數(shù)據(jù)的拷貝操作。所以節(jié)省了時間仆邓。

但是鲜滩,在代碼里:

const dataStr = JSON.stringify(data);
const dataBuff = str2ab(dataStr);

const worker = new CachedWorker();
worker.postMessage(dataBuff, [dataBuff]);

其實包含了數(shù)據(jù)->字符串(json)->buffer 的轉(zhuǎn)化過程。第一次花費的時間很多是在這些轉(zhuǎn)化的過程中消耗的节值。但是后面徙硅,筆者認為是瀏覽器做了優(yōu)化,還要繼續(xù)查一下資料搞疗,所以花費的時間只有直接傳輸 buffer 花費的時間闷游,所以大量減少。

注意:使用 Buffer 傳輸數(shù)據(jù)可以很大贴汪,比如在 Google 的某個例子中是 30M 多脐往。但是,上文的例子中扳埂,傳輸?shù)臄?shù)據(jù)的大小受到了很大的限制业簿。主要是在把 Buffer 的數(shù)據(jù)轉(zhuǎn)化為 Object 的時候會出現(xiàn)異常。有興趣的各位可以把數(shù)據(jù)的大小繼續(xù)往大調(diào)這個異常就會出現(xiàn)阳懂。所以梅尤,如何使用需要看具體的場景,比如岩调,上例可以改為在 Worker 里請求得到二進制數(shù)據(jù)再做處理巷燥。

Transferable Object

傳遞 Buffer 的時候是按照 Transferable Object 傳遞的。這種數(shù)據(jù)是實現(xiàn)了Transferable接口的數(shù)據(jù)号枕。這個接口就是一個標記的作用缰揪,表明實現(xiàn)了這個接口的數(shù)據(jù)可以如引用一般傳遞。

但是葱淳,此處的引用和 C/C++的引用是兩回事钝腺。Transferable object 在完成不同的執(zhí)行上下文(execution context)傳輸之后就不再可用了。H5 委員會為了 Worker 可以普及赞厕,默默的解決了多少使用多線程可能會出現(xiàn)的問題艳狐。

多次執(zhí)行就不用說了,只執(zhí)行一次的代碼緩存起來也存粹是浪費空間皿桑。緩沖的命中率是說緩存的結(jié)果會被用到毫目。如果緩存不會再被多次執(zhí)行的某個功能用到,那么也是沒有意義的诲侮。

在本例中镀虐,緩存的作用基本上大打折扣。字符串是隨機生成的浆西。用隨機字符串為 Key 緩存的結(jié)果粉私,基本上備用到的概率很小,而且隨機字符串的數(shù)量比較大(這里是 1000)近零。那么在查找緩存字符串的時候也要便利 map 的大部分 Key诺核。反而造成了不必要的多余計算。

所以久信,緩存需要根據(jù)代碼的執(zhí)行邏輯和緩存的命中率來判斷是否需要窖杀。

Worker 的使用離不開特定的場景

使用 Worker 或者不使用 Worker 都是要看具體的某個場景。新技術(shù)的產(chǎn)生一定是解決某個特定的問題的裙士。在使用這項新技術(shù)之前至少要盡量真實的模擬需要解決的場景入客,來驗證這個新的技術(shù)是否可行。比如,在本文使用的例子就是為了模擬筆者想要解決的問題的場景設(shè)立的桌硫。遇到的最大的問題是如果數(shù)據(jù)量達到某個臨界值的時候夭咬,在 Worker 內(nèi)部反序列化并組成 Object 的時候就會出現(xiàn)異常。而混存铆隘,因為 Key 值極大的可能是重復(fù)的卓舵,所以混存的使用就非常的有必要。在以上各種場景的模擬之后可以使用的各種技術(shù)的結(jié)合必然是緩存和使用Buffer傳輸數(shù)據(jù)膀钠。但是掏湾,數(shù)據(jù)量需要控制,不能出現(xiàn)反序列化的問題肿嘲。

或者融击,直接從 Worker 里請求得到 JSON 的二進制串,比如發(fā)送和接收二進制數(shù)據(jù)雳窟。

所以尊浪,各種技術(shù)都有在特定場合下使用的優(yōu)劣。這就需要我們具體結(jié)合場景具體分析涩拙。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末际长,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子兴泥,更是在濱河造成了極大的恐慌工育,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搓彻,死亡現(xiàn)場離奇詭異如绸,居然都是意外死亡,警方通過查閱死者的電腦和手機旭贬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門怔接,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人稀轨,你說我怎么就攤上這事扼脐。” “怎么了奋刽?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵瓦侮,是天一觀的道長。 經(jīng)常有香客問我佣谐,道長肚吏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任狭魂,我火速辦了婚禮罚攀,結(jié)果婚禮上党觅,老公的妹妹穿的比我還像新娘。我一直安慰自己斋泄,他們只是感情好杯瞻,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著是己,像睡著了一般又兵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上卒废,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機與錄音宙地,去河邊找鬼摔认。 笑死,一個胖子當(dāng)著我的面吹牛宅粥,可吹牛的內(nèi)容都是我干的参袱。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼秽梅,長吁一口氣:“原來是場噩夢啊……” “哼抹蚀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起企垦,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤环壤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后钞诡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體郑现,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年荧降,在試婚紗的時候發(fā)現(xiàn)自己被綠了接箫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡朵诫,死狀恐怖辛友,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剪返,我是刑警寧澤废累,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站随夸,受9級特大地震影響九默,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宾毒,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一驼修、第九天 我趴在偏房一處隱蔽的房頂上張望殿遂。 院中可真熱鬧,春花似錦乙各、人聲如沸墨礁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恩静。三九已至,卻和暖如春蹲坷,著一層夾襖步出監(jiān)牢的瞬間驶乾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工循签, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留级乐,地道東北人。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓县匠,卻偏偏與公主長得像风科,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子乞旦,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 一贼穆、概述 JavaScript 語言采用的是單線程模型,也就是說兰粉,所有任務(wù)只能在一個線程上完成故痊,一次只能做一件事。...
    零星小雨_c84a閱讀 2,447評論 0 2
  • 作者:阮一峰www.ruanyifeng.com/blog/2018/07/web-worker.html 概述 ...
    grain先森閱讀 1,077評論 0 1
  • 介紹web worker HTML5提供得亲桦,運行在后臺的 JavaScript崖蜜,獨立于其他腳本,不會影響頁面的性能...
    帶刀打天下閱讀 1,305評論 0 3
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,090評論 1 32
  • 【此刻】此刻我剛剛洗完澡在房間里面寫日記客峭,爸爸在看著電視豫领,媽媽在用她自以為很美的歌聲唱著贊美詩。剛剛從恭城回來舔琅,連...
    真承閱讀 146評論 0 3