函數式編程看React Hooks(一)簡單React Hooks實現javascript
函數式編程看React Hooks(二)事件綁定反作用深度剖析html
本教程不講解 React Hooks 的源碼,只用最簡單的方式來揭示 React Hooks 的原理和思想。 (我但願你看本文時,已經看過了上面一篇文章,由於本文會基於你已經瞭解部分 hooks 本質的前提下而展開的。例如你懂得 hooks 維護的狀態實際上是一個由閉包提供的。)前端
本文經過一個最近遇到了一個關於 React Hooks 的坑來展開講解。一步一步地揭示如何更好地去理解 hooks,去了解函數式的魅力。java
示例:codesandbox.io/s/brave-mea…react
function App() {
const [count, setCount] = useState(0);
const [isTag, setTag] = useState(false);
const onMouseMove = e => {
if (!isTag) {
return;
}
setCount(count + 1);
};
const onMouseUp = e => {
setTag(false);
};
const onMouseDown = e => {
setTag(true);
};
useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}, []);
return (
<div className="App"> <h1 onMouseDown={onMouseDown}>hello world</h1> <h2>{count}</h2> </div>
);
}
複製代碼
對於一些事件綁定複雜的邏輯,我以前是這麼寫的,爲了演示效果,去除了一些複雜的業務邏輯。npm
能夠看到在這個示例中,咱們的 count 始終爲 0。這是爲何呢?是 setCount 出問題了?百思不得其解,在咱們寫 class 類式編程時,這是一個很常見的編程習慣。爲何到了 hooks 這裏卻不行了呢?編程
咱們須要注意的一點是,如今編寫的是函數式組件,能夠說是函數式編程 (雖然不徹底是,可是是這樣的味道)。函數式編程的特色就是無反作用,輸入輸出一致性。可是對於前端一些 Dom,Bom 等 API 來講,無反作用是不可能的,事件的綁定,定時器等等都,都是有反作用的。。因此,爲了處理這一部分的邏輯,React Hooks 提供了 useEffect 這個鉤子來處理。因此說,咱們看到的全部一些奇奇怪怪的地方,效果和理想不一致的狀況,最終緣由就是這個編程模式轉變後,出現的"後遺症"。若是咱們用函數式的思想來理解,這些問題都將會迎刃而解。數組
如今起,請你拋棄 class 模式的寫法和更新方式,咱們單從函數邏輯的角度來進行講解。咱們來看看,當 App 函數第一次運行時候各個值的狀態。瀏覽器
function App() {
const [count -> 0, setCount] = useState(0);
const [isTag -> false, setTag] = useState(false);
const onMouseMove = e => {
if (!isTag -> false) {
return;
}
setCount(count -> 0 + 1);
};
const onMouseUp = e => {
setTag(false);
};
const onMouseDown = e => {
setTag(true);
};
useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}, []);
return (
<div className="App"> <h1 onMouseDown={onMouseDown}>hello world</h1> <h2>{count -> 0}</h2> </div>
);
}
複製代碼
咱們第一次渲染過程當中的 document.addEventListener("mousemove", onMouseMove);
中緩存
onMouseMove 的形態就是這樣的。
const onMouseMove = e => {
if (!false) {
return;
}
setCount(0 + 1);
};
複製代碼
document.addEventListener("mouseup", onMouseUp);
中
const onMouseUp = e => {
setTag(false);
};
複製代碼
當咱們鼠標點擊 hello world
後,會依次運行 onMouseDown
, onMouseMove
, onMouseUp
函數。
先從 onMouseDown
提及,這個時候使用 setTag
設置了 isTag
的值,設置完成後,整個 App 函數會從新運行,即從新渲染。
此時 App 內函數的狀態。(-> 此符號位標記當前的數值)
function App() {
const [count -> 0, setCount] = useState(0);
const [isTag -> true, setTag] = useState(false);
const onMouseMove = e => {
if (!isTag -> true) {
return;
}
setCount(isTag -> 0 + 1);
};
const onMouseUp = e => {
setTag(false);
};
const onMouseDown = e => {
setTag(true);
};
useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}, []);
return (
<div className="App"> <h1 onMouseDown={onMouseDown}>hello world</h1> <h2>{count -> 0}</h2> </div>
);
}
複製代碼
這個時候能夠看到,新一輪渲染中的 onMouseMove 已經更新了。可是呢,document.addEventListener("mousemove", onMouseMove);
咱們事件監聽綁定的事件仍是原來的函數也就是如下這個形態。。
const onMouseMove = e => {
if (!isTag -> false) {
return;
}
setCount(count -> 0 + 1);
};
複製代碼
由於,咱們事件綁定一旦綁定後,函數是不會變化的。
接下來就是 onMouseUp
這個時候 將 isTag
值設置成 false
。也會觸發 App 的從新運行。在 App 組件中 onMouseMove
的形態。
const onMouseMove = e => {
if (!isTag -> false) {
return;
}
setCount(count -> 0 + 1);
};
複製代碼
我這麼講,你可能有點暈。可是沒有關係,能夠看圖。
我之因此花費這麼長的篇幅來說解這個 onMouseMove
實際使用中的樣子,就是想讓你明白,千萬不要被 class 的模式給誤導了。不是說 onMouseMove
更新了,事件監聽的回調函數也改變了。事件監聽中的 onMouseMove
始終是咱們第一次渲染的樣子,(也就是 isTag
爲 false
的樣子)不會由於後面的變化去改變。
因此 isTag
始終爲 false
, setCount
一直沒法執行。
面對這個狀況,咱們能夠很天然地想到,若是咱們可以從新綁定一下新的 onMouseMove
,那麼問題不就迎刃而解了嗎?也就是說。只要是咱們在 isTag
更新的時候,從新去綁定事件監聽中的回調函數 onMouseMove
,就能夠解決咱們的問題。
因此 React Hooks,給 useEffect
提供了第二個參數,能夠放入一個依賴數組。也就是說,當咱們 isTag
更新的同時也去更新事件監聽中的回調函數。
可是更新事件函數的前提是,得先解綁舊的函數,不然的話,將會重複綁定事件。所以,react 回調函數中也提供了 return
的方式,來提供解綁。。經過這樣的描述我想你們應該也能理解爲何須要 return 解綁函數
了。。
因此上面爲了可以使得咱們的 count
可以正常更新的解決辦法,就是 hooks 一直說到的,添加正確的依賴很重要,不要去欺騙他。。。
如今是修復後的代碼,添加正確的依賴。
function App() {
const [count, setCount] = useState(0);
const [isTag, setTag] = useState(false);
const onMouseMove = e => {
if (!isTag) {
return;
}
setCount(count + 1);
};
const onMouseUp = e => {
setTag(false);
};
const onMouseDown = e => {
setTag(true);
};
useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}, [isTag]);
return (
<div className="App"> <h1 onMouseDown={onMouseDown}>hello world</h1> <h2>{count}</h2> </div>
);
}
複製代碼
咱們來看看如今事件的綁定中 回調函數的指向。每當 isTag
變化後,都會觸發回調函數的更新。使得每次咱們觸發的 onMouseMove
都是最新的。
可是咱們發現,咱們點擊移動的時候,無論怎麼移動 count 只會增長 1。由於咱們在添加依賴的時候,還須要對 count 也進行觀察,由於每次 count 值變化,咱們也得去更新綁定事件。
咱們繼續修改
useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}, [isTag, count]);
複製代碼
這個時候咱們發現只要咱們鼠標點擊後, move 事件會不斷地觸發, count
也會不斷地增長, 從而達到了咱們的目的。
那麼再來思考一個問題?每次這樣一個事件綁定咱們都得去尋找依賴項。。那麼咱們很是有可能忘記添加這個依賴,致使咱們整個組件沒法正常地運行。
幸虧 react 給我提供了一個機制,那就是 依賴項
也接受函數。
useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}, [onMouseMove]);
複製代碼
咱們嘗試一下,嗯,看似完美地解決了。可是咱們會發現,哇,爲何從新渲染了那麼屢次?還記得咱們 上一篇文章中,介紹 dep 比較的原理嗎?直接對值進行的比較。也就是意味着函數對比的話,就是地址進行比較,顯然,每次建立的函數地址都是不一樣的。(言外之意就是,每一次的從新渲染,都會致使 onMouseMove 的從新綁定,不僅僅是 isTag
, count
兩個值改變,每個變量改變引發的從新渲染都會致使 onMouseMove 的更新)
那麼咱們要如何解決麼?就要用到咱們的 useCallback
了。用來緩存函數,在上一節中,咱們也提到過實現原理。經過緩存來達到不建立新的函數。再來改造一下
const onMouseMove = useCallback(e => {
if (!isTag) {
return;
}
setCount(count + 1);
}, [isTag, count]);
useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}, [onMouseMove]);
複製代碼
示例效果:codesandbox.io/s/friendly-…
如今咱們已經完美地解決了咱們的問題,而且講解了 hooks 的一些本質,爲何這麼作的原理?咱們再打上日誌,來感覺下,整個 hooks 的運行過程吧。
示例: codesandbox.io/s/heuristic…
function App() {
console.log("開始運行");
const [count, setCount] = useState(0);
const [isTag, setTag] = useState(false);
const onMouseMove = useCallback(
e => {
if (!isTag) {
return;
}
setCount(count + 1);
},
[isTag, count]
);
const onMouseUp = e => {
console.log("up");
setTag(false);
};
const onMouseDown = e => {
console.log("down");
setTag(true);
};
useEffect(() => {
console.log("綁定事件");
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
console.log("解綁事件");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}, [onMouseMove]);
console.log("一輪結束");
return (
<div className="App"> <h1 onMouseDown={onMouseDown}>hello world</h1> <h2>{count}</h2> </div>
);
}
複製代碼
此番 React Hooks 的探究到此結束。若有任何疑問或者改進,請評論區轟炸。
注意事項
本身的一點點小的見解:
1.在某種程度上用性能來換取函數式編程的規範(雖然官方說這樣處理的性能幾乎不可計,個人意思是從寫出差代碼的機率,由於不是全部人都對 hooks 原理了如指掌。所以寫出問題的依賴的機率很是大。)如今的解決方式是儘量地添加 React Hooks 的 ESlint eslint-plugin-react-hooks
2.很是佩服 react 團隊的創造力,能想出這樣的解決方法。畢竟是 瀏覽器 與 react 的編程模式是不同,他們進行了最大程度上的融合。
zh-hans.reactjs.org/docs/hooks-…