使用 Web Workers 在 React 應用程序中管理長時間運行的任務

已發表: 2022-03-10
快速總結↬在本教程中,我們將通過構建一個利用 Web Worker 的示例 Web 應用程序來學習如何使用 Web Worker API 來管理 JavaScript 應用程序中的耗時和 UI 阻塞任務。 最後,我們將通過將所有內容轉移到 React 應用程序來結束本文。

對於 Web 應用程序,響應時間很重要。 無論您的應用程序在做什麼,用戶都需要即時響應。 無論是僅顯示一個人的姓名還是處理數字,Web 應用程序用戶都要求您的應用程序每次都響應他們的命令。 鑑於 JavaScript 的單線程特性,有時這很難實現。 但在本文中,我們將學習如何利用 Web Worker API 來提供更好的體驗。

在寫這篇文章時,我做了以下假設:

  1. 為了能夠繼續學習,您至少應該對 JavaScript 和文檔 API 有一定的了解;
  2. 您還應該具備 React 的工作知識,以便您可以使用 Create React App 成功啟動一個新的 React 項目。

如果您需要對該主題的更多見解,我在“更多資源”部分中提供了一些鏈接,以幫助您快速了解。

首先,讓我們開始使用 Web Workers。

什麼是網絡工作者?

要了解 Web Workers 及其要解決的問題,有必要了解 JavaScript 代碼是如何在運行時執行的。 在運行時,JavaScript 代碼按順序依次執行。 一旦一段代碼結束,則下一個代碼開始運行,依此類推。 用技術術語來說,我們說 JavaScript 是單線程的。 這種行為意味著一旦某段代碼開始運行,之後的每個代碼都必須等待該代碼完成執行。 因此,每一行代碼都會“阻止”執行它之後的所有其他內容。 因此,希望每段代碼盡快完成。 如果某些代碼需要太長時間才能完成,我們的程序似乎已經停止工作。 在瀏覽器上,這表現為一個凍結的、無響應的頁面。 在某些極端情況下,選項卡將完全凍結。

想像一下在單車道上行駛。 如果您前面的任何司機碰巧因任何原因停止行駛,那麼您就遇到了交通堵塞。 使用像 Java 這樣的程序,其他車道上的交通可能會繼續。 因此,Java 被稱為是多線程的。 Web Workers 嘗試將多線程行為引入 JavaScript。

下面的截圖顯示了 Web Worker API 被許多瀏覽器支持,所以你應該對使用它有信心。

顯示網絡工作者的瀏覽器支持圖表
Web Workers 瀏覽器支持。 (大預覽)

Web Worker 在後台線程中運行,不會干擾 UI,它們通過事件處理程序與創建它們的代碼進行通信。

Web Worker 的一個很好的定義來自 MDN:

“worker 是一個使用構造函數創建的對象(例如Worker() ,它運行一個命名的 JavaScript 文件——該文件包含將在工作線程中運行的代碼;worker 在另一個不同於當前window的全局上下文中運行。因此, 使用window快捷方式獲取當前全局範圍(而不是Worker中的self將返回錯誤。”

工作人員是使用Worker構造函數創建的。

 const worker = new Worker('worker-file.js')

可以在 Web Worker 中運行大多數代碼,但有一些例外。 例如,您不能從工作人員內部操作 DOM。 無法訪問document API。

工作者和產生它們的線程使用postMessage()方法相互發送消息。 同樣,它們使用onmessage事件處理程序響應消息。 獲得這種差異很重要。 發送消息是使用方法實現的; 接收回消息需要事件處理程序。 正在接收的消息包含在事件的data屬性中。 我們將在下一節中看到一個這樣的例子。 但是讓我快速提一下,我們一直在討論的那種工人被稱為“敬業的工人”。 這意味著工作者只能被調用它的腳本訪問。 也可以有一個可以從多個腳本訪問的工作人員。 這些稱為共享工作者,並使用SharedWorker構造函數創建,如下所示。

 const sWorker = new SharedWorker('shared-worker-file.js')

要了解有關 Workers 的更多信息,請參閱此 MDN 文章。 本文的目的是讓您開始使用 Web 工作者。 讓我們通過計算第 n 個斐波那契數來解決它。

跳躍後更多! 繼續往下看↓

計算第 N 個斐波那契數

注意:對於本節和接下來的兩節,我使用 VSCode 上的 Live Server 來運行應用程序。 你當然可以使用別的東西。

這是您一直在等待的部分。 最後,我們將編寫一些代碼來查看 Web Workers 的運行情況。 嗯,沒那麼快。 除非我們遇到它解決的問題,否則我們不會欣賞 Web Worker 所做的工作。 在本節中,我們將看到一個示例問題,在下一節中,我們將看到 web worker 如何幫助我們做得更好。

想像一下,您正在構建一個允許用戶計算第 n 個斐波那契數的 Web 應用程序。 如果您不熟悉“斐波那契數”這個術語,您可以在此處閱讀更多相關信息,但總而言之,斐波那契數是一個數字序列,每個數字都是前面兩個數字的總和。

在數學上,它表示為:

因此,序列的前幾個數字是:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ...

在某些來源中,序列從F 0 = 0開始,在這種情況下,下面的公式適用於n > 1

在本文中,我們將從 F 1 = 1 開始。從公式中我們可以立即看到的一件事是數字遵循遞歸模式。 現在手頭的任務是編寫一個遞歸函數來計算第 n 個斐波那契數(FN)。

經過幾次嘗試,相信您可以輕鬆想出下面的功能。

 const fib = n => { if (n < 2) { return n // or 1 } else { return fib(n - 1) + fib(n - 2) } }

功能很簡單。 如果 n 小於 2,則返回 n(或 1),否則,返回n-1n-2 FN 之和。 使用箭頭函數和三元運算符,我們可以提出單線。

 const fib = n => (n < 2 ? n : fib(n-1) + fib(n-2))

該函數的時間複雜度為0(2 n ) 。 這僅僅意味著隨著 n 值的增加,計算總和所需的時間呈指數增長。 對於較大的 n 值,這會導致一個真正長時間運行的任務,可能會干擾我們的 UI。 讓我們看看這個在行動。

注意這絕不是解決此特定問題的最佳方法。 我選擇使用這種方法是為了本文的目的。

首先,創建一個新文件夾並將其命名為您喜歡的任何名稱。 現在在該文件夾中創建一個src/文件夾。 另外,在根文件夾中創建一個index.html文件。 在src/文件夾中,創建一個名為index.js的文件。

打開index.html並添加以下 HTML 代碼。

 <!DOCTYPE html> <html> <head> <link rel="stylesheet" href="styles.css"> </head> <body> <div class="heading-container"> <h1>Computing the nth Fibonnaci number</h1> </div> <div class="body-container"> <p id='error' class="error"></p> <div class="input-div"> <input id='number-input' class="number-input" type='number' placeholder="Enter a number" /> <button id='submit-btn' class="btn-submit">Calculate</button> </div> <div id='results-container' class="results"></div> </div> <script src="/src/index.js"></script> </body> </html>

這部分非常簡單。 首先,我們有一個標題。 然後我們有一個帶有輸入和按鈕的容器。 用戶將輸入一個數字,然後單擊“計算”。 我們還有一個容器來保存計算結果。 最後,我們將src/index.js文件包含在script標記中。

您可以刪除樣式錶鍊接。 但是如果你時間不夠,我已經定義了一些你可以使用的 CSS。 只需在根文件夾中創建styles.css文件並添加以下樣式:

 body { margin: 0; padding: 0; box-sizing: border-box; } .body-container, .heading-container { padding: 0 20px; } .heading-container { padding: 20px; color: white; background: #7a84dd; } .heading-container > h1 { margin: 0; } .body-container { width: 50% } .input-div { margin-top: 15px; margin-bottom: 15px; display: flex; align-items: center; } .results { width: 50vw; } .results>p { font-size: 24px; } .result-div { padding: 5px 10px; border-radius: 5px; margin: 10px 0; background-color: #e09bb7; } .result-div p { margin: 5px; } span.bold { font-weight: bold; } input { font-size: 25px; } p.error { color: red; } .number-input { padding: 7.5px 10px; } .btn-submit { padding: 10px; border-radius: 5px; border: none; background: #07f; font-size: 24px; color: white; cursor: pointer; margin: 0 10px; }

現在打開src/index.js讓我們慢慢開發吧。 添加下面的代碼。

 const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)); const ordinal_suffix = (num) => { // 1st, 2nd, 3rd, 4th, etc. const j = num % 10; const k = num % 100; switch (true) { case j === 1 && k !== 11: return num + "st"; case j === 2 && k !== 12: return num + "nd"; case j === 3 && k !== 13: return num + "rd"; default: return num + "th"; } }; const textCont = (n, fibNum, time) => { const nth = ordinal_suffix(n); return ` <p id='timer'>Time: <span class='bold'>${time} ms</span></p> <p><span class="bold" id='nth'>${nth}</span> fibonnaci number: <span class="bold" id='sum'>${fibNum}</span></p> `; };

這裡我們有三個函數。 第一個是我們之前看到的計算第 n 個 FN 的函數。 第二個函數只是一個實用函數,用於將適當的後綴附加到整數。 第三個函數接受一些參數並輸出一個標記,我們稍後將插入到 DOM 中。 第一個參數是正在計算其 FN 的數字。 第二個參數是計算的 FN。 最後一個參數是執行計算所需的時間。

仍然在src/index.js中,在前一個代碼的下方添加以下代碼。

 const errPar = document.getElementById("error"); const btn = document.getElementById("submit-btn"); const input = document.getElementById("number-input"); const resultsContainer = document.getElementById("results-container"); btn.addEventListener("click", (e) => { errPar.textContent = ''; const num = window.Number(input.value); if (num < 2) { errPar.textContent = "Please enter a number greater than 2"; return; } const startTime = new Date().getTime(); const sum = fib(num); const time = new Date().getTime() - startTime; const resultDiv = document.createElement("div"); resultDiv.innerHTML = textCont(num, sum, time); resultDiv.className = "result-div"; resultsContainer.appendChild(resultDiv); });

首先,我們使用document API 來獲取 HTML 文件中的DOM節點。 我們得到了我們將顯示錯誤消息的段落的引用; 輸入; 計算按鈕和我們將顯示結果的容器。

接下來,我們將“click”事件處理程序附加到按鈕。 當按鈕被點擊時,我們獲取輸入元素內的任何內容並將其轉換為一個數字,如果我們得到小於 2 的任何內容,我們將顯示一條錯誤消息並返回。 如果我們得到一個大於 2 的數字,我們繼續。 首先,我們記錄當前時間。 之後,我們計算 FN。 完成後,我們得到一個時間差,表示計算花費了多長時間。 在代碼的其餘部分,我們創建了一個新的div 。 然後我們將其內部 HTML 設置為我們之前定義的textCont()函數的輸出。 最後,我們向它添加一個類(用於樣式)並將其附加到結果容器中。 這樣做的效果是每個計算將出現在前一個下方的單獨div中。

顯示最多 43 個計算出的斐波那契數
一些斐波那契數字。 (大預覽)

我們可以看到,隨著數量的增加,計算時間也增加(指數)。 例如,從 30 到 35,我們的計算時間從 13 毫秒跳到 130 毫秒。 我們仍然可以認為這些操作是“快速的”。 在 40 時,我們看到計算時間超過 1 秒。 在我的機器上,這是我開始注意到頁面變得無響應的地方。 在這一點上,當計算正在進行時,我無法再與頁面交互。 我不能專注於輸入或做任何其他事情。

還記得我們談到 JavaScript 是單線程的嗎? 好吧,那個線程已經被這個長時間運行的計算“阻塞”了,所以其他的一切都必須“等待”它完成。 它可能從您機器上的較低或較高值開始,但您一定會達到這一點。 請注意,計算 44 需要將近 10 秒。如果您的 Web 應用程序上還有其他事情要做,那麼用戶必須等待 Fib(44) 完成才能繼續。 但是,如果您部署了一個 Web Worker 來處理該計算,您的用戶可以在運行時繼續執行其他操作。

現在讓我們看看網絡工作者如何幫助我們克服這個問題。

Web Worker 示例

在本節中,我們會將計算第 n 個 FN 的工作委託給 Web Worker。 這將有助於釋放主線程並在計算進行時保持我們的 UI 響應。

開始使用 Web Worker 非常簡單。 讓我們看看如何。 創建一個新文件src/fib-worker.js 。 並輸入以下代碼。

 const fib = (n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)); onmessage = (e) => { const { num } = e.data; const startTime = new Date().getTime(); const fibNum = fib(num); postMessage({ fibNum, time: new Date().getTime() - startTime, }); };

請注意,我們已經將計算第 n 個斐波那契數的函數fib移到了這個文件中。 該文件將由我們的網絡工作者運行。

回想一下什麼是 web worker 一節,我們提到 web worker 和它們的父級使用onmessage事件處理程序和postMessage()方法進行通信。 在這裡,我們使用onmessage事件處理程序來監聽來自父腳本的消息。 一旦我們收到一條消息,我們就從事件的數據屬性中解構數字。 接下來,我們獲取當前時間並開始計算。 一旦結果準備好,我們使用postMessage()方法將結果發布回父腳本。

打開src/index.js讓我們進行一些更改。

 ... const worker = new window.Worker("src/fib-worker.js"); btn.addEventListener("click", (e) => { errPar.textContent = ""; const num = window.Number(input.value); if (num < 2) { errPar.textContent = "Please enter a number greater than 2"; return; } worker.postMessage({ num }); worker.onerror = (err) => err; worker.onmessage = (e) => { const { time, fibNum } = e.data; const resultDiv = document.createElement("div"); resultDiv.innerHTML = textCont(num, fibNum, time); resultDiv.className = "result-div"; resultsContainer.appendChild(resultDiv); }; });

首先要做的是使用Worker構造函數創建 Web Worker。 然後在按鈕的事件監聽器中,我們使用worker.postMessage({ num })向工作人員發送一個數字。 之後,我們設置了一個函數來監聽worker中的錯誤。 這裡我們只是簡單地返回錯誤。 如果你願意,你當然可以做更多,比如在 DOM 中顯示它。 接下來,我們監聽來自worker的消息。 一旦我們收到一條消息,我們就解構timefibNum ,並繼續在 DOM 中顯示它們的過程。

請注意,在 web worker 內部, onmessage事件在 worker 範圍內可用,因此我們可以將其編寫為self.onmessageself.postMessage() 。 但是在父腳本中,我們必須將這些附加到工人本身。

在下面的屏幕截圖中,您將在 Chrome 開發工具的源選項卡中看到 web worker 文件。 您應該注意的是,無論您輸入什麼數字,UI 都會保持響應。 這種行為是網絡工作者的魔力。

活動 Web Worker 文件的視圖
一個正在運行的網絡工作者文件。 (大預覽)

我們的網絡應用程序取得了很大進展。 但是我們還可以做一些其他事情來讓它變得更好。 我們當前的實現使用單個工作者來處理每個計算。 如果一條新消息在運行時出現,則舊消息將被替換。 為了解決這個問題,我們可以為每個調用創建一個新的 worker 來計算 FN。 讓我們在下一節中看看如何做到這一點。

使用多個 Web Worker

目前,我們正在使用單個工作人員處理每個請求。 因此,傳入的請求將替換尚未完成的先前請求。 我們現在想要做一個小的改變,為每個請求生成一個新的 web worker。 一旦完成,我們將殺死這個工人。

打開src/index.js並在按鈕的單擊事件處理程序中移動創建 web worker 的行。 現在事件處理程序應該如下所示。

 btn.addEventListener("click", (e) => { errPar.textContent = ""; const num = window.Number(input.value); if (num < 2) { errPar.textContent = "Please enter a number greater than 2"; return; } const worker = new window.Worker("src/fib-worker.js"); // this line has moved inside the event handler worker.postMessage({ num }); worker.onerror = (err) => err; worker.onmessage = (e) => { const { time, fibNum } = e.data; const resultDiv = document.createElement("div"); resultDiv.innerHTML = textCont(num, fibNum, time); resultDiv.className = "result-div"; resultsContainer.appendChild(resultDiv); worker.terminate() // this line terminates the worker }; });

我們做了兩個改變。

  1. 我們將這一行const worker = new window.Worker("src/fib-worker.js")移到按鈕的單擊事件處理程序中。
  2. 我們添加了這條線worker.terminate()以在我們完成後丟棄工人。

因此,每次單擊按鈕,我們都會創建一個新的工作人員來處理計算。 因此我們可以不斷改變輸入,一旦計算完成,每個結果都會顯示在屏幕上。 在下面的屏幕截圖中,您可以看到 20 和 30 的值出現在 45 之前。但我先開始 45。 一旦函數返回 20 和 30,它們的結果就會被發布,並且 worker 終止。 當一切都完成後,我們不應該在源選項卡上有任何工作人員。

顯示終止工人的斐波那契數
多個獨立工作者的插圖。 (大預覽)

我們可以在這裡結束這篇文章,但如果這是一個 React 應用程序,我們將如何將 Web Worker 引入其中。 這是下一節的重點。

React 中的 Web Worker

首先,使用 CRA 創建一個新的 React 應用程序。 將fib-worker.js文件複製到您的 react 應用程序的public/文件夾中。 將文件放在這裡源於 React 應用程序是單頁應用程序這一事實。 這是唯一特定於在反應應用程序中使用工作人員的事情。 從這裡開始的一切都是純粹的 React。

src/文件夾中創建一個文件helpers.js並從中導出ordinal_suffix()函數。

 // src/helpers.js export const ordinal_suffix = (num) => { // 1st, 2nd, 3rd, 4th, etc. const j = num % 10; const k = num % 100; switch (true) { case j === 1 && k !== 11: return num + "st"; case j === 2 && k !== 12: return num + "nd"; case j === 3 && k !== 13: return num + "rd"; default: return num + "th"; } };

我們的應用程序將要求我們維護一些狀態,因此創建另一個文件src/reducer.js並粘貼到狀態減速器中。

 // src/reducers.js export const reducer = (state = {}, action) => { switch (action.type) { case "SET_ERROR": return { ...state, err: action.err }; case "SET_NUMBER": return { ...state, num: action.num }; case "SET_FIBO": return { ...state, computedFibs: [ ...state.computedFibs, { id: action.id, nth: action.nth, loading: action.loading }, ], }; case "UPDATE_FIBO": { const curr = state.computedFibs.filter((c) => c.id === action.id)[0]; const idx = state.computedFibs.indexOf(curr); curr.loading = false; curr.time = action.time; curr.fibNum = action.fibNum; state.computedFibs[idx] = curr; return { ...state }; } default: return state; } };

讓我們一個接一個地檢查每個動作類型。

  1. SET_ERROR :觸發時設置錯誤狀態。
  2. SET_NUMBER :將我們輸入框中的值設置為狀態。
  3. SET_FIBO :向計算的 FN 數組添加一個新條目。
  4. UPDATE_FIBO :在這裡我們尋找一個特定的條目並將其替換為一個新對象,該對象具有計算的 FN 和計算它所花費的時間。

我們很快就會使用這個減速器。 在此之前,讓我們創建將顯示計算出的 FN 的組件。 創建一個新文件src/Results.js並粘貼以下代碼。

 // src/Results.js import React from "react"; export const Results = (props) => { const { results } = props; return ( <div className="results-container"> {results.map((fb) => { const { id, nth, time, fibNum, loading } = fb; return ( <div key={id} className="result-div"> {loading ? ( <p> Calculating the{" "} <span className="bold"> {nth} </span>{" "} Fibonacci number... </p> ) : ( <> <p> Time: <span className="bold">{time} ms</span> </p> <p> <span className="bold"> {nth} </span>{" "} fibonnaci number:{" "} <span className="bold"> {fibNum} </span> </p> </> )} </div> ); })} </div> ); };

通過這個更改,我們開始將之前的 index.html 文件轉換為 jsx。 該文件有一個職責:獲取表示計算的 FN 的對像數組並顯示它們。 與我們之前的唯一區別是引入了加載狀態。 所以現在當計算運行時,我們顯示加載狀態,讓用戶知道正在發生一些事情。

讓我們通過更新src/App.js中的代碼來完成最後的工作。 代碼相當長,所以我們將分兩步完成。 讓我們添加第一個代碼塊。

 import React from "react"; import "./App.css"; import { ordinal_suffix } from "./helpers"; import { reducer } from './reducer' import { Results } from "./Results"; function App() { const [info, dispatch] = React.useReducer(reducer, { err: "", num: "", computedFibs: [], }); const runWorker = (num, id) => { dispatch({ type: "SET_ERROR", err: "" }); const worker = new window.Worker('./fib-worker.js') worker.postMessage({ num }); worker.onerror = (err) => err; worker.onmessage = (e) => { const { time, fibNum } = e.data; dispatch({ type: "UPDATE_FIBO", id, time, fibNum, }); worker.terminate(); }; }; return ( <div> <div className="heading-container"> <h1>Computing the nth Fibonnaci number</h1> </div> <div className="body-container"> <p className="error"> {info.err} </p> // ... next block of code goes here ... // <Results results={info.computedFibs} /> </div> </div> ); } export default App;

像往常一樣,我們帶來我們的進口。 然後我們用 useReducer 鉤子實例化一個狀態和更新函數。 然後我們定義一個函數runWorker() ,它接受一個數字和一個 ID,並開始調用一個 web worker 來計算該數字的 FN。

請注意,要創建工作者,我們將相對路徑傳遞給工作者構造函數。 在運行時,我們的 React 代碼會附加到public/index.html文件,因此它可以在同一目錄中找到fib-worker.js文件。 當計算完成時(由worker.onmessage觸發), UPDATE_FIBO操作被調度,並且工作人員隨後終止。 我們現在所擁有的與我們以前所擁有的並沒有太大的不同。

在這個組件的返回塊中,我們渲染了與之前相同的 HTML。 我們還將計算出的數字數組傳遞給<Results />組件進行渲染。

讓我們在return語句中添加最後的代碼塊。

 <div className="input-div"> <input type="number" value={info.num} className="number-input" placeholder="Enter a number" onChange={(e) => dispatch({ type: "SET_NUMBER", num: window.Number(e.target.value), }) } /> <button className="btn-submit" onClick={() => { if (info.num < 2) { dispatch({ type: "SET_ERROR", err: "Please enter a number greater than 2", }); return; } const id = info.computedFibs.length; dispatch({ type: "SET_FIBO", id, loading: true, nth: ordinal_suffix(info.num), }); runWorker(info.num, id); }} > Calculate </button> </div>

我們在輸入上設置了一個onChange處理程序來更新info.num狀態變量。 在按鈕上,我們定義了一個onClick事件處理程序。 當按鈕被點擊時,我們檢查數字是否大於 2。請注意,在調用runWorker()之前,我們首先調度一個動作以將一個條目添加到計算的 FN 數組中。 一旦工作人員完成其工作,該條目將被更新。 通過這種方式,每個條目都保持其在列表中的位置,這與我們以前不同。

最後,複製之前的styles.css的內容,替換App.css的內容。

我們現在一切就緒。 現在啟動你的反應服務器並玩弄一些數字。 注意加載狀態,這是一個 UX 改進。 此外,請注意,即使您輸入高達 1000 的數字並單擊“計算”,UI 也會保持響應。

在工作人員處於活動狀態時顯示加載狀態。
顯示加載狀態和活躍的網絡工作者。 (大預覽)

注意加載狀態和活動工作人員。 一旦計算出第 46 個值,worker 就會被殺死,並且加載狀態會被最終結果替換。

  • 這個 React 應用程序的源代碼在 Github 上可用,並且在 vercel 上有一個託管應用程序。

結論

呸! 走了很長一段路,所以讓我們總結一下。 我鼓勵您查看 Web Workers 的 MDN 條目(請參閱下面的資源列表),以了解使用 Web Workers 的其他方式。

在本文中,我們了解了 Web Worker 是什麼以及它們要解決的問題類型。 我們還看到瞭如何使用純 JavaScript 來實現它們。 最後,我們看到瞭如何在 React 應用程序中實現 web worker。

我鼓勵您利用這個出色的 API 為您的用戶提供更好的體驗。

更多資源

  • Console.time() , MDN 網絡文檔
  • {JSON}佔位符,官網
  • 使用 Web Workers,MDN 網絡文檔
  • 斐波那契數,維基百科
  • 條件(三元)運算符,MDN 網絡文檔
  • Document 、Web API、MDN 網絡文檔
  • 入門,創建 React 應用程序(文檔)
  • Function.prototype.toString() , MDN 網絡文檔
  • IIFE,MDN 網絡文檔
  • workerSetup.js ,很棒的全棧教程,GitHub
  • “使用 Web Workers 在 JavaScript 中進行並行編程”,Uday Hiwarale,Medium