<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何無痛的為你的前端項目引入多線程

          共 5880字,需瀏覽 12分鐘

           ·

          2021-03-15 09:27

          導(dǎo)語 本文旨在介紹一種簡單,優(yōu)雅,代碼侵入性小的web worker引入方式。能夠在不影響現(xiàn)有業(yè)務(wù)邏輯的情況下,快速為你的前端項目引入多線程能力。

          一、談?wù)刉eb Worker

          眾所周知,JavaScript引擎是單線程的,這意味著所有的操作都會在主線程當(dāng)中發(fā)生。

          盡管瀏覽器內(nèi)核是多線程的,但是負(fù)責(zé)頁面渲染的UI線程總是會在JS引擎線程空閑時(執(zhí)行完一個macro task)才會執(zhí)行。

                       JavaScript的事件隊列模型

          這意味著如果頁面當(dāng)中包含某些計算密集的代碼時,因為JS引擎是單線程的,會阻塞整個事件隊列,進(jìn)而導(dǎo)致整個頁面卡住。

          而Web Worker就是為了解決這個問題而生的。

          這里引用一段阮一峰老師的定義

          Web Worker 的作用,就是為 JavaScript 創(chuàng)造多線程環(huán)境,允許主線程創(chuàng)建 Worker 線程,將一些任務(wù)分配給后者運行。在主線程運行的同時,Worker 線程在后臺運行,兩者互不干擾。等到 Worker 線程完成計算任務(wù),再把結(jié)果返回給主線程。這樣的好處是,一些計算密集型或高延遲的任務(wù),被 Worker 線程負(fù)擔(dān)了,主線程(通常負(fù)責(zé) UI 交互)就會很流暢,不會被阻塞或拖慢。

          二、多線程的力量

          我這里寫了一個簡單示例,來展示將復(fù)雜的計算邏輯移出主線程能帶來多大的提升。

          這里我們假設(shè)頁面存在一個很復(fù)雜的計算操作,需要耗費好幾秒才能完成。由于JS引擎是單線程的,如果在主線程里執(zhí)行這個計算邏輯,我們將看到頁面將在好幾秒內(nèi)是無法響應(yīng)的。從用戶視角來看,就是整個頁面“卡住了”。毫無疑問,這是非常令人挫敗的體驗。

          然后我們看一下另一個做法,把這個計算邏輯放到worker線程當(dāng)中去計算,計算完畢后再將結(jié)果傳回主線程。

          可以看到,復(fù)雜的計算操作一點也沒有影響UI線程的運行,頁面一直在流暢的更新,并且一點都不阻塞操作。

          從上面這個簡單的例子可以看出,僅僅是將計算邏輯轉(zhuǎn)移到worker線程,就能夠帶來多大的變化。

          2.1 不得不提的兼容性

          web worker的兼容性非常好。

          一個小缺點

          web worker提出的時間非常早,這是它兼容性好的原因。但是也是問題所在,web worker原生的API設(shè)計得非常古老,是基于事件訂閱的,不是特別好用。引入項目當(dāng)中的成本還是很高的。

          //in main.jsfirst.onchange = function() {  myWorker.postMessage([first.value,second.value]);  console.log('Message posted to worker');}
          //in worker.jsonmessage = function(e) { console.log('Message received from main script'); var workerResult = 'Result: ' + (e.data[0] * e.data[1]); console.log('Posting message back to main script'); postMessage(workerResult);}

          2.2 一個顧慮,postMessage真的很慢么?

          除了古老的API設(shè)計以外,很多開發(fā)者對于web worker還有個顧慮就是,聽說postMessage很慢。

          畢竟將數(shù)據(jù)作為參數(shù)傳遞給postMessage的時候,實際上會先將數(shù)據(jù)序列化為字符串,然后當(dāng)worker接收到傳遞過來的數(shù)據(jù)(序列化后的字符串)時,還需要將字符串?dāng)?shù)據(jù)反序列化一次才能使用。

          而worker再回返數(shù)據(jù)給主線程的時候,同樣也要先經(jīng)歷一次序列化,然后字符串?dāng)?shù)據(jù)到了主線程以后,還需要再反序列化一次。

          這中間涉及到四次數(shù)據(jù)的變換,從直覺上來看,開發(fā)者理所當(dāng)然的會擔(dān)憂性能層面的問題。

          不過我們先把這個問題拆解一下。

          三、數(shù)據(jù)變換的負(fù)擔(dān)

          首先,只有發(fā)生在主線程代碼當(dāng)中的數(shù)據(jù)轉(zhuǎn)換,才會對主線程造成負(fù)擔(dān)。這意味著只有兩種情況下,主線程才會分配計算資源。

          • 傳輸數(shù)據(jù)(序列化)

          • 收到數(shù)據(jù)(反序列化)

          發(fā)生在woker當(dāng)中的數(shù)據(jù)轉(zhuǎn)換是由worker線程負(fù)擔(dān)的,對主線程是毫無影響的。

          3.1 序列化和反序列化的性能問題

          接下來是第二個問題,序列化和反序列化對性能的影響有多大?

          Google的Surma做了一個關(guān)于postMessage性能的詳細(xì)測試,這里我只放出他得出的結(jié)論。

          如果想要詳細(xì)了解相關(guān)情況,可以點擊下面的鏈接閱讀他的詳細(xì)文章。

          Is postMessage slow?

          3.2 兩個關(guān)鍵數(shù)字

          序列化后的數(shù)據(jù)大小傳輸耗時典型場景
          100kb100ms用戶可感知到的最短時間(如果超過這個時間,用戶會開始感覺到卡頓)
          10kb16ms流暢動畫(60 FPS)的一幀

          四、Comlink——新瓶裝舊酒

          正如之前談到的,webWorker實際上是非常有用的,只是它的API稍微古老了一點,它是基于事件訂閱的,不是特別好用。稍微時髦一點的說法就是,給開發(fā)者帶來的心智負(fù)擔(dān)相對來說比較大。

          上文當(dāng)中提到的Surma設(shè)計了一套更加現(xiàn)代化的API,將postMessage的細(xì)節(jié)封裝了起來,使得在向worker線程傳遞數(shù)據(jù)的時候,更加像是將變量的訪問權(quán)共享給了其他線程。

          下面我們簡要看一下Comlink的官方給出的一個示例,一個簡單的計數(shù)器。

          // main.jsimport * as Comlink from "https://unpkg.com/comlink?module";
          const worker = new Worker("worker.js");// This `state` variable actually lives in the worker!const state = await Comlink.wrap(worker);await state.inc();console.log(await state.currentCount);
          // worker.jsimport * as Comlink from "https://unpkg.com/comlink?module";
          const state = { currentCount: 0,
          inc() { this.currentCount++; }}
          Comlink.expose(state);

          實際上看完這個計數(shù)器的例子,你就已經(jīng)完全搞懂Comlink該如何使用了,就這么簡單。

          Comlink精妙的地方,我個人認(rèn)為在于將數(shù)據(jù)傳遞的操作變成了一個異步的操作,這樣我們就能很好的利用ES6所提供的async/await語法糖,將數(shù)據(jù)的傳遞與接收邏輯寫得非常簡潔優(yōu)雅。開發(fā)者不需要再去考慮事件訂閱所帶來的各種復(fù)雜度。

          4.1 和現(xiàn)有框架結(jié)合

          Comlink雖然只是一個簡單的工具庫,但是將它引入到現(xiàn)有的頁面邏輯里,其實是非常簡單的。并且代碼侵入性是非常小的,我們并不需要大規(guī)模改造現(xiàn)有的代碼,就能享受到webWorker帶來的便利性。

          下面我將給出兩個簡單的示例,展示如何讓Comlink和Vue以及Vuex和諧的運轉(zhuǎn)在一起。(React和Redux其實也是相同的道理,這里我就不贅述了)。

          完整示例代碼可以從這里找到

          https://git.code.oa.com/sihanhu/web-worker-demo

          4.2 Comlink + Vue

          Dom部分非常簡單,就是一個普通的計數(shù)器

           <div id="app">      <div class="counter">        Counter is {{ counter }}        <button @click="addCounter">Add</button>      </div>    </div>

          Vue部分,實際上創(chuàng)建Worker之后,使用wrap方法將這個Worker變?yōu)橐粋€proxy對象(ES6特性),就能夠訪問woker當(dāng)中暴露的對象的任何屬性了。唯一需要留心的就是,這是個異步的操作。

          // main var app = new Vue({        el: '#app',        data: {            counter: 0,            remoteState: {},        },        methods:{            async initWorker() {                const worker = new Worker("./worker.js");                this.remoteState = Comlink.wrap(worker);            },            async addCounter() {                const count = this.counter;                this.counter = await this.remoteWorker.inc(count);            }        },        mounted(){            this.initWorker();        }    }) // worker.jsconst obj = {  inc(count) {    return count+1;  },};
          Comlink.expose(obj);
          4.3 Comlink + Vue + Vuex

          和Vuex的結(jié)合其實也很簡單。從worker線程當(dāng)中獲取值是一個異步操作,只要我們將它封裝成一個Action就可以了,非常自然。

              const worker = new Worker("vuexWorker.js");    const counterState = Comlink.wrap(worker);
          const store = new Vuex.Store({ state: { count: 0 }, mutations: { setCount: (state, value) => state.count = value, }, actions:{ async changeCount ({ commit }, value) { const count = await counterState.changeCounter(value) commit('setCount', count) }, } })
          var app = new Vue({ el: '#app', computed: { count () { return store.state.count } }, methods:{ async addCounter() { store.dispatch('changeCount', 1) }, async minusCounter() { store.dispatch('changeCount', -1) } }, }) // worker.js const obj = { changeCounter(count, value) { return count + value; }, };
          Comlink.expose(obj);

          五、總結(jié)

          將復(fù)雜的計算操作從主線程轉(zhuǎn)移到其他線程是一個簡單卻又收益巨大的改進(jìn),我非常推薦你試一試。

          5.1 我們可能并不需要Comlink

          看到這里,肯定有一些讀者心中還有疑慮,因為實際上Comlink還提供了其他能力,為什么我卻沒有提及呢?

          因為我們實際上需要的只是將postMessage的數(shù)據(jù)傳遞包裝成一個異步的操作,并且暴露出一個proxy對象供主線程便利的操作Worker線程的數(shù)據(jù)。

          這意味著實際上我們并不一定需要使用Comlink。如果有興趣的話,也可以自己用Promise和Proxy封裝一個更加輕量級的版本。

          比如Comlink也提供一個方法,能夠?qū)⒒卣{(diào)函數(shù)傳給Worker線程,然后Worker線程計算完畢再后將結(jié)果傳回來。

          但是我個人并不建議去使用這種特性,因為這會讓主線程的代碼太過于復(fù)雜了,如果編寫得不夠好,很多地方會變得難以理解,就像是“黑魔法”一樣。

          5.2 兩個建議

          在此我給兩個建議,約束對webWorker的使用,避免代碼過于復(fù)雜化。

          1. 只將包含復(fù)雜計算的操作轉(zhuǎn)移到worker線程當(dāng)中

            沒有必要把所有的計算邏輯都從主線程剝離,那樣worker.js就太重了。

            最好將worker.js作為外掛插件,只容納包含復(fù)雜計算的邏輯,這樣對現(xiàn)有代碼的侵入性和改造量也比較小。


          2. 只在worker.js當(dāng)中執(zhí)行計算邏輯

            理想的worker.js應(yīng)該只暴露一個全部是計算函數(shù)的對象。

            盡量不要在worker線程當(dāng)中再額外維持一份數(shù)據(jù)狀態(tài)了,否則線程間的狀態(tài)同步是大問題



          最后


          • 歡迎加我微信(winty230),拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個專業(yè)的技術(shù)人...

          點個在看支持我吧
          瀏覽 58
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  波多野结衣无码AⅤ一区t二区三区 | 91香蕉视频在线 | 国产乱码精品1区2区3区 | 久久国产乱子伦精品 | 超碰夜夜 |