這一章節開始我們要來介紹一個新的 Hook 也就是 useMemo,但是再說明 useMemo 之前我們先來回顧一下 Vue 裡面與 useMemo 類似的東西,也就是 Vue Computed。
首先讓我們先回憶一下關於 Vue Computed 的用法,我們先來看一下下面這個例子:
const { createApp, ref, computed } = Vue;
const app = createApp({
setup() {
const num1 = ref(2);
const total = computed(() => num1.value * 2)
return {
total
}
}
});
app.mount('#app');
在上方的例子中我們可以看到我們所熟悉的 Computed (計算屬性),而這個計算屬性會回傳 num1
乘以 2
的值,而這就是我們所熟悉的 Composition API Computed 計算屬性最基本的用法。
忘了 Computed
的用法嗎?沒關係,這邊寫了一個簡單的範例讓你回憶一下
const { createApp, ref, computed } = Vue;
const app = createApp({
setup() {
const num1 = ref(2);
const total = computed(() => {
console.log('computed');
return num1.value * 2
})
const add = () => {
console.log('methods');
return num1.value * 2
}
return {
total,
add
}
}
});
app.mount('#app');
當你打開 Console 的時候,你可以發現 Add
函式總共被呼叫了五次,因此 console.log('methods')
也出現五次,而這是因為我們在畫面上呼叫它了五次,但是 Computed
明明也是呼叫五次,但 console.log('computed')
卻只出現一次,這是因為 Computed
只會在資料改變時才會重新計算並執行,因此當資料沒有任何變化時,就不會再次執行,而這也就是我們所熟悉的 Computed 的特性。
那麼 React 的 useMemo 又是怎樣呢?讓我們接著往下看吧。
看到前面的例子後,我們可以知道 Vue 的 Computed 會在資料有變動時才會重新計算,而這個特性就是我們在 React Hook 中的 useMemo
。
首先讓我們將前面 Vue 的範例稍微改成 React Hook 版本,看一下 useMemo
是不是真的與 Vue Computed 類似
const App = () => {
const num = 1;
const total = React.useMemo(() => {
console.log('useMemo')
return num * 2
});
const add = () => {
console.log('methods');
return num * 2;
}
return (
<div>
<p>useMemo:{ total }</p>
<p>useMemo:{ total }</p>
<p>useMemo:{ total }</p>
<p>useMemo:{ total }</p>
<p>useMemo:{ total }</p>
<p>methods:{ add() }</p>
<p>methods:{ add() }</p>
<p>methods:{ add() }</p>
<p>methods:{ add() }</p>
<p>methods:{ add() }</p>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
以結果來講,確實是一樣的,因此我們可以知道 useMemo
也是在資料有變動時才會重新計算,而這也是我們在 React Hook 中的 useMemo
的特性。
所以我們就到這邊結束囉~
.
..
...
....
.....
沒有啦,其實 useMemo
還有其他特性,所以讓我們繼續往下看吧。
首先先讓我們看一段範例程式碼
const App = () => {
const [ users, setUsers ] = React.useState([]);
const getData = async () => {
// 有時候會發生 CORS,只需要重新整理即可
const { data } = await axios.get('https://randomuser.me/api/?results=10');
setUsers(data.results);
}
React.useEffect(() => {
getData()
}, []);
return (
<div>
<ul>
{
users.map((user) => (
<li key={user.email}>
{ user.name.first + user.name.last }
</li>
))
}
</ul>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
這個範例程式碼我們會使用到 RandomUser 這個服務,這個服務在練習跟需要一些假的使用者資料時非常好用,完全不用擔心裡面有任何真人個資唷。
那麼 useMemo
除了與 Vue 的 Computed
神似之外,它還有什麼特性呢?首先 useMemo
其實可以傳入兩個參數,分別是 Callback 與 Array,而 useMemo
會去記憶 Callback 計算後的結果,也就是 memoized
的概念,甚至你也可以在第二個陣列中傳入想要監聽的變數,當監聽的變數有變化時就會出發渲染
const [ users, setUsers ] = React.useState([]);
const newUsers = React.useMemo(() => {}, [ users ])
但是如果你沒有傳入第二個陣列的話,則會在每一次觸發渲染時就執行一次 useMemo
,如同前面範例一樣。
接下來讓我們實際撰寫一下會更有感覺,首先前面有提到 RandomUser 這個服務,在我們透過 AJAX 請求資料之後,我們會需要使用 sort
重新排序,所以你有可能這樣撰寫
const App = () => {
const [ users, setUsers ] = React.useState([]);;
const [ state, setState ] = React.useState(false);
const getUsers = async() => {
const { data } = await axios.get('https://randomuser.me/api/?results=10');
setUsers(data.results);
}
React.useEffect(() => {
getUsers();
}, []);
const filterUsers = (state) => {
const newUser = users.sort((a, b) => {
if(state) {
return a.dob.age < b.dob.age ? 1 : -1
}
return a.dob.age > b.dob.age ? 1 : -1
});
setUsers([...newUser]);
}
return (
<div>
<button onClick={ () => filterUsers(true) } className="border-4 border-indigo-500">年齡大到小</button>
<button onClick={ () => filterUsers(false) }className="border-4 border-indigo-500 ml-4">年齡小到大</button>
<hr className="my-4"/>
<ul>
{
users.map((user) =>(
<li key={ user.email }>
{ `Name:${user.name.title}.${user.name.first } ${user.name.last}, Age:${user.dob.age}` }
</li>
))
}
</ul>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
雖然畫面是有正常顯示且排序,但這樣做其實並不是一件好事,因為我們每次點擊按鈕都會觸發 filterUsers
,而 filterUsers
會去重新排序,這樣就會造成效能上的浪費,因為我們只是想要重新排序,而不是重新渲染,所以我們可以使用 useMemo
來優化這個問題
這邊也題外話一下,你可能會注意到 filterUsers
中的 setUsers
我是重新做陣列展開的方式 setUsers([...newUser]);
,這邊會這樣寫的原因是因爲 sort
會直接修改原本的陣列,因此如果你是直接 setUsers(newUser);
的話,你會發現畫面並不會重新渲染,因為我們並沒有修改資料,所以 React 在認知上就不會重新渲染,所以我們必須要做陣列展開的方式,讓 React 認知到我們有修改資料,才會重新渲染。
接著讓我們來看一下 useMemo
這個 Hook,那該怎麼寫,其實很簡單就把它當作在寫 Vue 的 computed
就好,只是我們會特別監聽 state
這個值有變化時才去觸發 useMemo
const App = () => {
const [ users, setUsers ] = React.useState([]);;
const [ state, setState ] = React.useState(false);
const getUsers = async() => {
const { data } = await axios.get('https://randomuser.me/api/?results=10');
setUsers(data.results);
}
React.useEffect(() => {
getUsers();
}, []);
const filterUsers = React.useMemo(() => {
return users.sort((a, b) => {
if(state) {
return a.dob.age < b.dob.age ? 1 : -1
}
return a.dob.age > b.dob.age ? 1 : -1
});
}, [ state ])
return (
<div>
<button onClick={ () => setState(true) } className="border-4 border-indigo-500">年齡大到小</button>
<button onClick={ () => setState(false) }className="border-4 border-indigo-500 ml-4">年齡小到大</button>
<hr className="my-4"/>
<ul>
{
filterUsers.map((user, index) => (
<li key={ user.email }>
{ `Name:${user.name.title}.${user.name.first } ${user.name.last}, Age:${user.dob.age}` }
</li>
))
}
</ul>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);
以上就是一個簡單粗略的 useMemo
介紹與使用。
本文將會同步更新到我的部落格