原文連接javascript
本文從 mini React
—— Preact
源碼的角度,分析 React Hook
各個 API
的優勢缺點。 從而理解爲何要用 hook
,以及如何最佳使用。html
let currentIndex; // 全局索引
let currentComponent; // 當前 hook 所在的組件
function getHookState(index) {
const hooks =
currentComponent.__hooks ||
(currentComponent.__hooks = {_list: [], _pendingEffects: []});
if (index >= hooks._list.length) {
hooks._list.push({});
}
return hooks._list[index];
}
複製代碼
// 獲取註冊的 hook
const hookState = getHookState(currentIndex++);
複製代碼
hook api
時,索引 currentIndex + 1
依次存入數組。 當組件 render
以前,會先調用 hook render
,重置索引和設置當前組件,hook 注入在 options
內。options._render = vnode => {
currentComponent = vnode._component;
currentIndex = 0;
// ...
};
複製代碼
首先須要知道一點的是,函數組件
在每次 diff
時,整個函數都會從新執行, 而 class組件
只會執行 this.render
,所以 hook
在性能上會有些損耗, 考慮到這一點 hook
爲那些聲明開銷很大的數據結構和函數,提供了 useMemo
和 useCallback
優化。java
hook
在每次 render
時,取上一次 hook state
時, 若是在循環,條件或嵌套函數不肯定的分支裏執行,就有可能取錯數據,致使混亂。node
function Todo(props) {
const [a] = useState(1);
if(props.flag) {
const [b] = useState(2);
}
const [c] = useState(3);
// ...
}
複製代碼
<Todo flag={true} />
複製代碼
a = 1, b = 2, c = 3
;<Todo flag={false} />
複製代碼
a = 1, c = 2
。c
取錯了狀態!第二條嘛,就顯而易見了,hook
寄生於 react
組件和生命週期。react
Preact hook
在 options
對象上聲明瞭 _render
-> diffed
-> _commit
-> unmount
四個鉤子, 分別會在對象組件的生命週期前執行,這樣侵入性較小。// 聲明 hook
const [state, setState] = useState(initialState);
// 更新 state
setState(newState);
// 也能夠函數式更新
setState(prevState => { // 能夠拿到上一次的 state 值
// 也可使用 Object.assign
return {...prevState, ...updatedValues};
});
複製代碼
state
值開銷很大,能夠傳入函數,初始化只會執行一次。const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
複製代碼
Object.is
判斷),不會觸發組件更新。const [state, setState] = useState(0);
// ...
// 更新 state 不會觸發組件從新渲染
setState(0);
setState(0);
複製代碼
props.state === 1
初始化 hook
,爲何 props.state === 2
時,hook state
不會變化?function Component(props) {
const [state, setState] = useState(props.state);
// ...
}
複製代碼
hook state
變動是怎麼驅動組件渲染的,爲何說能夠當 class state
使用?Preact
中 useState
是使用 useReducer
實現的,便於行文,代碼會略加修改。function useState(initialState) {
const hookState = getHookState(currentIndex++);
if (!hookState._component) {
hookState._component = currentComponent;
hookState._value = [
invokeOrReturn(undefined, initialState),
action => {
const nextValue = invokeOrReturn(hookState._value[0], action);
if (hookState._value[0] !== nextValue) {
hookState._value[0] = nextValue;
hookState._component.setState({});
}
}
];
}
return hookState._value;
}
複製代碼
// 工具函數,用來支持函數式初始和更新
function invokeOrReturn(arg, f) {
return typeof f === 'function' ? f(arg) : f;
}
複製代碼
useState
只會在組件首次 render
時初始化一次,之後由返回的函數來更新狀態。props
的值來初始化 useState
;hookState._value[0] !== nextValue
比較新舊值避免沒必要要的渲染。this.setState
函數。這就是爲何 hook
能夠代替 class
的 this.state
使用。query
參數,首次加載組件只發一次請求內容。function Component(props) {
const [state, setState] = useState({});
useEffect(() => {
ajax.then(data => setState(data));
}, []); // 依賴項
// ...
}
複製代碼
useState
有說到,props
初始 state
有坑,能夠用 useEffect
實現。function Component(props) {
const [state, setState] = useState(props.state);
useEffect(() => {
setState(props.state);
}, [props.state]); // props.state 變更賦值給 state
// ...
}
複製代碼
function WindowWidth(props) {
const [width, setWidth] = useState(0);
function onResize() {
setWidth(window.innerWidth);
}
// 只執行一次反作用,組件 unmount 時會被清除
useEffect(() => {
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return <div>Window width: {width}</div>;
}
複製代碼
useEffect
在使用 state
時最好把它做爲依賴,否則容易產生 bug
function Component() {
const [a, setA] = useState(0);
useEffect(() => {
const timer = setInterval(() => console.log(a), 100);
return () => clearInterval(timer)
}, []);
return <button onClick={() => setA(a+1)}>{a}</button>
}
複製代碼
當你點擊按鈕 a+=1
時,此時 console.log
依舊打印 0
。 這是由於 useEffect
的反作用只會在組件首次加載時入 _pendingEffects
數組,造成閉包。git
修改以下:github
function Component() {
const [a, setA] = useState(0);
useEffect(() => {
const timer = setInterval(() => console.log(a), 100);
return () => clearInterval(timer)
- }, []);
+ }, [a]);
return <button onClick={() => setA(a+1)}>{a}</button>
}
複製代碼
這段代碼在 React
裏運行,輸出會隨點擊按鈕而變化,而在 preact
中,以前定時器未被清除, 說明有 bug
。-_-||ajax
useEffect
解決了什麼問題通常發送數據請求 componentDidMount
中,以後 componentWillUnmount
在相關清理。 這就致使相互無關的邏輯夾雜在 componentDidMount
,而對應的掃尾工做卻分配在 componentWillUnmount
中。小程序
有了 useEffect
,你能夠把相互獨立的邏輯寫在不一樣的 useEffect
中,他人擔憂維護時,也不用擔憂其餘代碼塊裏還有清理代碼。api
每次 diff
函數組件會被當作class
組件的 this.render
函數相似使用, 總體會被執行,在主體裏操做反作用是致命的。
useEffect
的機制?function useEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
state._value = callback;
state._args = args;
currentComponent.__hooks._pendingEffects.push(state);
}
}
複製代碼
undefined
或依賴項數組中一個值變更,則 true
function argsChanged(oldArgs, newArgs) {
return !oldArgs || newArgs.some((arg, index) => arg !== oldArgs[index]);
}
複製代碼
_pendingEffects
數組中維護,代碼有兩處執行options._render = vnode => {
currentComponent = vnode._component;
currentIndex = 0;
if (currentComponent.__hooks) { // 這裏爲何要清理了再執行!!!
currentComponent.__hooks._pendingEffects.forEach(invokeCleanup);
currentComponent.__hooks._pendingEffects.forEach(invokeEffect);
currentComponent.__hooks._pendingEffects = [];
}
};
複製代碼
function invokeCleanup(hook) {
if (hook._cleanup) hook._cleanup();
}
function invokeEffect(hook) {
const result = hook._value(); // 若是反作用函數有返回函數的,會被當成清理函數保存。
if (typeof result === 'function') hook._cleanup = result;
}
複製代碼
options.diffed = vnode => {
const c = vnode._component;
if (!c) return;
const hooks = c.__hooks;
if (hooks) {
if (hooks._pendingEffects.length) {
afterPaint(afterPaintEffects.push(c));
}
}
};
複製代碼
function afterPaint(newQueueLength) {
if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) {
prevRaf = options.requestAnimationFrame;
(prevRaf || afterNextFrame)(flushAfterPaintEffects);
}
}
複製代碼
function flushAfterPaintEffects() {
afterPaintEffects.some(component => {
if (component._parentDom) {
try {
component.__hooks._pendingEffects.forEach(invokeCleanup);
component.__hooks._pendingEffects.forEach(invokeEffect);
component.__hooks._pendingEffects = [];
} catch (e) {
options._catchError(e, component._vnode);
return true;
}
}
});
afterPaintEffects = [];
}
複製代碼
我很懷疑,options._render
的代碼是從 flushAfterPaintEffects
不假思索的拷過去。 致使上面講到的一個 bug
。
afterPaint
利用 requestAnimationFrame
或 setTimeout
來達到如下目的
與 componentDidMount
、componentDidUpdate
不一樣的是,在瀏覽器完成佈局與繪製以後,傳給 useEffect 的函數會延遲調用,不會在函數中執行阻塞瀏覽器更新屏幕的操做。 (勘誤:React
中 useEffect
能達到這效果,Preact
並無實現)
function Counter () {
const [count, setCount] = useState(0);
const [val, setValue] = useState('');
const expensive = useMemo(() => {
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum
}, [ count ]); // ✅ 只有 count 變化時,回調函數纔會執行
return (
<>
<span>You Clicked {expensive} times</span>
<button onClick={() => setCount(count + 1)}>Click me</button>
<input value={val} onChange={event => setValue(event.target.value)} />
</>
)
}
複製代碼
useMemo
解決了什麼問題上面反覆強調了,函數組件體會被反覆執行,若是進行大的開銷的會吃性能。 因此 react
提供了 useMemo
來緩存函數執行返回結果,useCallback
來緩存函數。
function useMemo(factory, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
state._args = args;
state._factory = factory;
return (state._value = factory());
}
return state._value;
}
複製代碼
能夠看出,只是把傳入的函數根據依賴性執行了一遍把結果保存在內部的 hook state
中。
記住,全部的 hook api
都同樣,不要在沒有傳入state
做爲依賴項的狀況下,在副租用中 使用 state
。
const onClick = useCallback(
() => console.log(a, b),
[a, b]
);
複製代碼
useCallback
解決了什麼問題上面提到了,用來緩存函數的
function WindowWidth(props) {
const [width, setWidth] = useState(0);
- function onResize() {
- setWidth(window.innerWidth);
- }
+ const onResize = useCallback(() => {
+ setWidth(window.innerWidth);
+ }, []);
useEffect(() => {
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return <div>Window width: {width}</div>;
}
複製代碼
上面說過,沒有依賴的時,不使要用 width
,但可使用 setWidth
, 函數是引用,閉包變量 setWidth
是同一個地址。
useMemo
的封裝function useCallback(callback, args) {
return useMemo(() => callback, args);
}
複製代碼
function Counter() {
const [start, setStart] = useState(false);
const [time, setTime] = useState(60);
useEffect(() => { // effect 函數,不接受也不返回任何參數
let interval;
if (start) {
interval = setInterval(() => {
setTime(time - 1); // ❌ time 在 effect 閉包函數裏是拿不到準確值的
}, 1000);
}
return () => clearInterval(interval) // clean-up 函數,當前組件被註銷時調用
}, [start]); // 依賴數組,當數組中變量變化時會調用 effect 函數
return (
<button onClick={() => setStart(!start)}>{time}</button>
);
}
複製代碼
time
值不是最新的。 能夠用 time
的初始值來傳給 useRef
,再來驅動 time
的更新。function Counter() {
const [start, setStart] = useState(false);
const [time, setTime] = useState(60);
+ const currentTime = useRef(time); // 生成一個可變引用
useEffect(() => { // effect 函數,不接受也不返回任何參數
let interval;
if (start) {
interval = setInterval(() => {
+ setTime(currentTime.current--) // currentTime.current 是可變的
- setTime(time - 1); // ❌ time 在 effect 閉包函數裏是拿不到準確值的
}, 1000);
}
return () => clearInterval(interval) // clean-up 函數,當前組件被註銷時調用
}, [start]); // 依賴數組,當數組中變量變化時會調用 effect 函數
return (
<button onClick={() => setStart(!start)}>{time}</button>
);
}
複製代碼
useRef
生成一個對象 currentTime = {current: 60}
,currentTime
對象在組件的整個生命週期內保持不變。
但這樣處理有點畫蛇添足,setTime
函數式更新不就行了嘛,current
能夠用來替代 interval
,這樣外部也能取消倒計時。
function Counter() {
const [start, setStart] = useState(false);
const [time, setTime] = useState(60);
- const currentTime = useRef(time); // 生成一個可變引用
+ const interval = useRef() // interval 能夠在這個做用域裏任何地方清除和設置
useEffect(() => { // effect 函數,不接受也不返回任何參數
- let interval;
if (start) {
- interval = setInterval(() => {
+ interval.current = setInterval(() => {
- setTime(currentTime.current--) // currentTime.current 是可變的
+ setTime(t => t - 1) // ✅ 在 setTime 的回調函數參數裏能夠拿到對應 state 的最新值
}, 1000);
}
- return () => clearInterval(interval) // clean-up 函數,當前組件被註銷時調用
+ return () => clearInterval(interval.current) // clean-up 函數,當前組件被註銷時調用
}, [start]); // 依賴數組,當數組中變量變化時會調用 effect 函數
return (
<button onClick={() => setStart(!start)}>{time}</button>
);
}
複製代碼
這樣既能消滅 interval
變量的反覆建立,也能讓外部可以清理定時器 interval.current
。
useRef
返回的對象在組件的整個生命週期內保持不變,怎麼理解?current
屬性?function useRef(initialValue) {
return useMemo(() => ({ current: initialValue }), []);
}
複製代碼
useMemo
來實現,傳入一個生成一個具備 current
屬性對象的函數, 空數組依賴,因此在整個生命週期該函數只執行一次。useRef
返回的值,沒法改變內部 hookState._value
值,只能經過 改變內部 hookState._value.current
來影響下次的使用。useEffect
使用方式相同。useEffect
區別在哪裏?useEffect
的回調在 option.diffed
階段, 使用 requestAnimationFrame
或 setTimeout(callback, 100)
來異步執行,因爲做者都認爲 this is not such a big deal
,因此代碼就不貼了,並且只是有一層 requestAnimationFrame
也達不到下一幀以前執行的效果。
useLayoutEffect
的回調在 option._commit
階段批量同步處理。
在 React
中估計使用了 requestIdleCallback
或 requestAnimationFrame
來進行時間分片,以免阻塞視覺更新。
因爲 react
本身內部使用了優先級調度,勢必會致使某些低優先級會延遲執行,只有你以爲優先級很高,在無論阻塞渲染的狀況也要同步執行, 那麼你能夠用 useLayoutEffect
。
const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
default: throw new Error('Unexpected action');
}
};
function Counter() {
const [count, dispatch] = useReducer(reducer, initialState);
return (
<div> {count} <button onClick={() => dispatch('increment')}>+1</button> <button onClick={() => dispatch('decrement')}>-1</button> <button onClick={() => dispatch('reset')}>reset</button> </div>
);
}
複製代碼
state
的初始值;state
的初始值。useReducer
?state 邏輯較複雜且包含多個子值,下一個 state 依賴於以前的 state。
使用 reducer
最好是個純函數,集中處理邏輯,修改源頭方便追溯,避免邏輯分散各處,也能避免不可預知的地方修改了狀態,致使 bug 難追溯。
useState
是 useReducer
實現的。function useReducer(reducer, initialState, init) {
const hookState = getHookState(currentIndex++);
if (!hookState._component) {
hookState._component = currentComponent;
hookState._value = [
!init ? invokeOrReturn(undefined, initialState) : init(initialState),
action => {
const nextValue = reducer(hookState._value[0], action);
if (hookState._value[0] !== nextValue) {
hookState._value[0] = nextValue;
hookState._component.setState({});
}
}
];
}
return hookState._value;
}
複製代碼
state
爲 reducer
的第一個參數,dispatch
接受的參數爲第二個參數,產生新的 state
。theme
// App.js
function App() {
return <Toolbar theme="dark" />;
}
// Toolbar.js
function Toolbar(props) {
// 很麻煩,theme 需層層傳遞全部組件。
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
// ThemedButton.js
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}
複製代碼
// context.js
+ const ThemeContext = React.createContext('light');
// App.js
function App() {
- return <Toolbar theme="dark" />;
+ return (
+ <ThemeContext.Provider value="dark">
+ <Toolbar />
+ </ThemeContext.Provider>
);
}
// Toolbar.js
function Toolbar(props) {
return (
<div>
- <ThemedButton theme={props.theme} />
+ <ThemedButton /> // 無需傳遞
</div>
);
}
// ThemedButton.js
class ThemedButton extends React.Component {
+ static contextType = ThemeContext; // 指定 contextType 讀取當前的 theme context。
render() {
- return <Button theme={this.props.theme} />;
+ return <Button theme={this.context} />; // React 會往上找到最近的 theme Provider,theme 值爲 「dark」。
}
}
複製代碼
// context.js
const ThemeContext = React.createContext('light');
// App.js
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
// Toolbar.js
function Toolbar(props) {
return (
<div>
<ThemedButton /> // 無需傳遞
</div>
);
}
// ThemedButton.js
- class ThemedButton extends React.Component {
- static contextType = ThemeContext; // 指定 contextType 讀取當前的 theme context。
- render() {
- return <Button theme={this.context} />; // React 會往上找到最近的 theme Provider,theme 值爲 「dark」。
- }
- }
+ function ThemedButton() {
+ const theme = useContext(ThemeContext);
+
+ return <Button theme={theme} />;
+ }
複製代碼
useContext(MyContext)
至關於 class 組件
中的 static contextType = MyContext
當組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 MyContext provider 的 context value 值。即便祖先使用 React.memo 或 shouldComponentUpdate,也會在組件自己使用 useContext 時從新渲染。
可使用 React.memo
或 useMemo hook
來性能優化。
function useContext(context) {
const provider = currentComponent.context[context._id];
if (!provider) return context._defaultValue;
const state = getHookState(currentIndex++);
// This is probably not safe to convert to "!"
if (state._value == null) {
state._value = true;
provider.sub(currentComponent);
}
return provider.props.value;
}
複製代碼
Select
或 Input
組件,你可能分別對應使用他們來從新組合一個新的組件,把防抖實如今新組件內部。// 防抖 hook
function useDebounce() {
const time = useRef({lastTime: Date.now()});
return (callback, ms) => {
time.current.timer && clearTimeout(time.current.timer);
time.current.timer = setTimeout(() => {
const now = Date.now();
console.log(now - time.current.lastTime);
time.current.lastTime = now;
callback();
}, ms);
}
}
複製代碼
function App() {
const [val, setVal] = useState();
const inputChange = useDebounce();
// 能夠屢次使用
// const selectChange = useDebounce();
return (
<>
<input onChange={
({target: {value}}) => {
inputChange(() => setVal(value), 500)
}
}/>{val}
</>
);
}
複製代碼
class
和 this
。class
目前還只是語法糖,標準還在更改,沒有像傳統面向對象的多態、多繼承的概念,this
理解成本很高;ts
推導類型,等等。