接著前面所講的,我們將幾個常用的hook都再複習一遍,來解構基礎以外你可能沒發現的細節,那麼我們先從最基礎的 useState
來講起。
有沒有想過,
useState
是怎麼來更改 component 裡面的內容呢?
這其中的概念,可以參考 Javascript 裡面的 Proxy
,這裡的 Proxy
指的是 Design pattern 裡面的 Proxy pattern,並不是我們常講的 Proxy server喔!Proxy
是 JavaScript 的一個內建原型,它允許你在目標對象(被代理對象)的操作上注入自定義的行為。使用 Proxy
,可以在目標對象的屬性取值、賦值、函數呼叫等操作之前或之後執行自定義的處理邏輯。
那我們先來看看 Proxy
怎麼處裡的吧:
// default物件
const defaultObject = {
name: 'Luciano',
age: 30
};
// 透過Proxy建立以defaultObject為預設的代理物件
const proxy = new Proxy(defaultObject, {
get(target, property, receiver) {
console.log(`你的名字 ${property}`);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log(`更改 ${property} 為 ${value}`);
return Reflect.set(target, property, value, receiver);
}
});
// 使用 Proxy
console.log(proxy.name); // 觸發get輸出 "你的名字 Luciano"
proxy.age = 31; // 觸發set輸出 "更改 age 為 31"
前一篇的章節也複習了 React 的 virtual dom,要更改真實的 Dom 之前你會需要一套機制來隔離虛擬 Dom 與真實 Dom,那也就是 Proxy Pattern 代理處理的部分,我們來模擬一下 useState
的組成吧!
// 我們熟知的setState type
type StateSetter<T> = (newValue: T | ((prevValue: T) => T)) => void;
function useState<T>(initialValue: T): [T, StateSetter<T>] {
let state: T = initialValue;
// 這邊也可以考慮換成new Set()
const subscribers: (() => void)[] = [];
const setState: StateSetter<T> = (newValue: T | ((prevValue: T) => T)) => {
// 這邊處理帶進來的是callback | value
const updatedValue = typeof newValue === 'function' ? (newValue as (prevValue: T) => T)(state) : newValue;
if (state !== updatedValue) {
state = updatedValue;
subscribers.forEach(subscriber => subscriber());
}
};
const stateProxy = new Proxy({ value: state }, {
get(target, property) {
if (property === 'value') {
return state;
} else if (property === 'set') {
return setState;
}
}
});
return [stateProxy.value, stateProxy.set];
}
let currentRender: (() => void) | null = null;
// 這裡就是react底層在處理的渲染時機,當然實際上不只一種
// 詳細的話應該等同於class component的渲染時機
// 原諒我只用最簡單的方式處理
// 有興趣的朋友可以參考solidjs作者的直播
// 我有映像他在介紹如何改善渲染時機的段落有示範如何實作底層
function render(component: () => void) {
currentRender = component;
if (currentRender) {
currentRender();
currentRender = null;
}
}
// 使用自定義的 useState Hook
const [count, setCount] = useState(0);
function CounterComponent() {
console.log('Rendering CounterComponent...');
console.log(`Count: ${count}`);
return null;
}
// 模擬組件渲染這裡的機制在 React 裡面會更加複雜一點
render(CounterComponent);
// 更新狀態
setCount(prevCount => prevCount + 1);
// 再次渲染通常應該對應的是 componentDidUpdate
render(CounterComponent);
在上面的範例中,我們使用 TypeScript 模擬了一個簡單的 useState
函數,該函數返回一個陣列,包含狀態和更新狀態的函數,另外處理了一個簡單的 render
函數,用於模擬組件的渲染。
注意:這只是一個簡化的腦補,真正的 React useState Hook 會更加複雜,涉及到更新、批量渲染、多個組件的狀態管理等等。
這裡演示 Proxy 在 useState
的角色,以及如何實現簡單的狀態管理機制。了解一點基礎了以後我們來講講面試常常會問到的問題吧!
useState
的運作是同步還是非同步?
了解了底層以後,不難回答出來他是同步的,那是什麼原因會讓人搞混他是非同步運行的呢?讓我們來看看範例吧:
const Search = () => {
// 這裡為基本的使用情境,當使用useState的function的同時會產生一個array
// 也就是為什麼我們常常看到範例的使用方式是長這樣了
const [txt, setTxt] = useState('');
const [resData, setResData] = useState([]);
// 這裡示範一個打api的動作,但為獨立的void function
const txtChange = (event) => {
setTxt(event.target.value);
fetch(`/apiUrlsForSearch?${txt}`)
.then((res) => res.json()).then(setResData)
}
// 下面return的部分就是基本的做法,當input的onChange觸發時會去打api
return (
<div>
<input value={txt} onChange={txtChange} />
{resData?.map(...)}
</div>
)
}
以範例為例我們來模擬一下當這個component被渲染的時候的先後順序,當畫面剛進入時,會以你帶入的預設值 txt = “”
& resData = []
去渲染,所以會是空的input和空白的結果。當我們於欄位中輸入 a
的時候會觸發 txtChange
function 然後透過此 function 的處理程序會先觸發了 setTxt
的 function 去更改 txt
的值為 “a”
,然後我們又將 txt
的值帶入 fetch
function 裡面去打 api,並將回傳結果透過 setResData
function 更改寫入 resData
中。但這個時候你應該會發現 resData
的值並沒有更動。
為什麼呢?讓我們重新看一遍這個 function 的處理順序:
// ...省略
const txtChange = (event) => {
setTxt(event.target.value);
// 這裡如果拿的是txt那麼會拿到原本就的值,其中的原理為js的閉包
// 並不是非同步的處理問題,也就是說你應該要改的是將txt改為event.target.value
// 因為透過setTxt的處理會經過一段Proxy的判斷處理程序
// 而你所取出來的setTxt又是在useState的function當中,所以這裡仍然為閉包的概念
console.log(txt) // 這裡你應該會拿到舊的值,也就是常常會讓人搞混的地方
fetch(`/apiUrlsForSearch?${txt}`) // 解法就是將txt改為event.target.value
.then((res) => res.json()).then(setResData)
}
// ...
這也是我真實經歷過的問題,面試官仍舊堅持自己的觀念為正確的,並確信 useState
為非同步的處理 function,老實說他一舉這樣的例子我當下也無法解釋的那麼清楚,我只記得我肯定沒在官方文件上看到說明 useState
為非同步的解釋,只能怪自己太菜了。
那麼其實更好一點的做法應該是將 fetch
function 拆出來透過 useEffect
的方式去監聽原本的 txt
就好了:
const Search = () => {
const [txt, setTxt] = useState('');
const [resData, setResData] = useState([]);
useEffect(() => {
fetch(`/apiUrlsForSearch?${txt}`)
.then((res) => res.json()).then(setResData)
}, [txt])
const txtChange = (event) => {
setTxt(event.target.value);
}
return (
<div>
<input value={txt} onChange={txtChange} />
{resData?.map(...)}
</div>
)
}
這樣一來當你原本的 txt
更改的同時 useEffect
就會重複觸發 fetch
function的部分,當然也可以透過 debounce 的做法來降低 server 的負擔。
那麼今天的分享就到這裡,明天我們再來詳解 useEffect
。
pattern電子書--proxy-pattern
solidjs Ryan Carniato直播
useState 的運作是同步還是非同步?
這段看了之後,覺得作者可能不太了解 JavaScript 的 event loop?
當某個 function 執行完之後,沒有拿到新的值,通常都是 async 的
如果是 sync 的,你的 setState 完成之後,正常來說應該要拿到新的值,但在這邊卻是沒有的
我也覺得你的面試官說得才是對的😆
同樣如果去 google 搜尋 useState sync or async
可能會查到滿多資料的
再來是 useState 的底層是 proxy, 想請問這邊的資料從哪裡來的呢?
proxy 是 Vue 底下資料綁定的實作,而 React 跟 Vue 在資料流處理方式也不一太一樣
都2023年了,找文章正確性的能力如果不行,也可以問問chatGPT啊!😂😂😂
reddit react版
我看過的文章
Jack Herrington talking about useState
關於event loop你應該知道的💁🏼
也許我表達的太過主觀也沒有解釋得太清楚,我想要做的是結合上一篇的內容來實做一個類似的hook出來,這是寫文章時沒注意到沒看過上下文的用戶會搞混的地方。如果你有興趣了解當初怎麼寫出來的,你可以參考。
如果你也有自己的想法歡迎show me the code,證明哪個環節是我missing掉的async。
React的 hook 讓你的 code 看起來是 sync 的
但是你的 setState 觸發之後的re-render流程卻不是在同一個 eventloop 內發生的
這樣的話感覺是對這塊的解釋手法有點問題,導致會讓人誤解
祝福作者的職涯順利囉( ^.< )
其實我文中提到的閉包指的就是你上述的流程,很感謝你認真看完我的文章,也許我也可以參考你的說法,這樣會比較好讓不懂閉包的朋友理解我的意思。
也歡迎提出任何想法時提供連結,這樣我會比較好理解是不是我的資訊錯誤。
對請看清楚是 setState()
,如果對非同步有一定認知的話應該會理解他這裡講的Asynchronous和我們一般常講的promise,是不同的處理機制,有興趣可以參考我的medium文章,而我們使用的useState()
100% 是 sync 的機制,我文章裡面提到的問題就是閉包觀念的延伸題,有興趣理解更深的話可以補完這篇
拍謝剛學React不久
function App() {
const [count, setCount] = useState(0);
const increment = () => {
// 延遲 2 秒後執行
setTimeout(() => {
// 更新狀態
setCount(count + 1);
}, 2000);
};
return (
<div>
<button onClick={increment}>+1</button>
<p>count: {count}</p>
</div>
如果是同步的話為什麼當我點擊下去後count的值沒有立刻增加
反而是等到setTimeout渲染頁面後才增加呢?
如果回調函數沒有副作用時就會是同步的
function App() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<button onClick={increment}>+1</button>
<p>count: {count}</p>
</div>
);
}
沒關係你不孤單,我真的有遇過一個交大純血的資深前端面試官,問跟你一樣的問題,想當然直接被我扣分到爆,我建議你可以先看完下面幾篇關於 javascript 的基本觀念:
閉包問題
event loop講解
event loop深入理解
如果是同步的話為什麼當我點擊下去後count的值沒有立刻增加
這個問題就是沒有完全理解閉包和event loop的運作
還有先搞清楚 setTimeout
與 Promise
的差異,你可能會比較理解基本運作的原理。
這篇文章是不想離題太多,但我覺得你有必要理解 javascript 的運作原理,可以參考前面生成 v-dom 的概念延伸。
自製框架hook概念篇
Jack Herrington影片教學
實作派範例(請先把上面資源讀完)
學習資源與態度分享
你才剛入坑,javascript的基礎不熟很正常,請先把我給你的那些資源念熟再往下吧!
function counterHook() {
let count = 0;
const add = (val: number) => count += val;
return {count, add}
}
const count1 = counterHook();
count1.add(1);
console.log(count1.count);
count1.add(1);
console.log(count1.count);
能回答這題的執行結果嗎?
如果能,那怎麼改善錯誤呢?
錯誤的原因又是什麼呢?
如果你還不能理解,就代表你的閉包觀念根本沒弄清楚!請正視問題,資源我都分享給你了,那些是個人自律的部分。
關於React 18 batch updating的更動,也無法掩蓋個人閉包觀念不熟的部分,這裡可以給你學習資源自我進修。
Jack Herrington - Mastering batch updating