一、場景
先理解什么是 Hook,拿 React 的介紹來看,它的定義是:
它可以讓你在不編寫 Class 的情況下,讓你在函數組件里“鈎入” React state 及生命周期等特性的函數
對於 Vue 提出的新的書寫 Vue 組件的 API:Composition API RFC,作用也是類似,所以我們也可以像 React 一樣叫做 Vue Hooks。
該 API 受到 React Hooks 的啟發,但有一些有趣的差異,規避了一些 React 的問題。
二、Hook 的時代意義
框架是服務於業務的,業務中很難避免的一個問題就是:邏輯復用。同樣的功能,同樣的組件,在不一樣的場合下,我們有時候不得不去寫 2+次。為了避免耦合,后來各大框架紛紛想出了一些辦法,比如 mixin, render props, 高階組件等實現邏輯上的復用,但是都有一些額外的問題
- mixin 與組件之間存在隱式依賴,可能產生沖突。傾向於增加更多狀態,降低了應用的可預測性
- 高階組件 多層包裹嵌套組件,增加了復雜度和理解成本,對於外層是黑盒
- Render Props 使用繁瑣,不好維護, 代碼體積過大,同樣容易嵌套過深
- ...
Hook 的出現是划時代的,通過 function 抽離的方式,實現了復雜邏輯的內部封裝:
- 邏輯代碼的復用
- 減小了代碼體積
- 沒有 this 的煩惱
三、React Hooks
React Hooks 允許你 "勾入" 諸如組件狀態和副作用處理等 React 功能中。Hooks 只能用在函數組件中,並允許我們在不需要創建類的情況下將狀態、副作用處理和更多東西帶入組件中。
React 核心團隊奉上的采納策略是不反對類組件,所以你可以升級 React 版本、在新組件中開始嘗試 Hooks,並保持既有組件不做任何更改
import React, { useState, useEffect } from "React"; const NoteForm = ({ onNoteSent }) => { const [currentNote, setCurrentNote] = useState(""); useEffect(() => { console.log(`Current note: ${currentNote}`); }); return ( <form onSubmit={e => { onNoteSent(currentNote); setCurrentNote(""); e.preventDefault(); }} >
<label>
<span>Note: </span>
<input value={currentNote} onChange={e => { const val = e.target.value && e.target.value.toUpperCase()[0]; const validNotes = ["A", "B", "C", "D", "E", "F", "G"]; setCurrentNote(validNotes.includes(val) ? val : ""); }} />
</label>
<button type="submit">Send</button>
</form> ); };
- useState 和 useEffect 是 React Hooks 中的一些例子,使得函數組件中也能增加狀態和運行副作用
- 還有更多其他 Hooks, 甚至能自定義一個,Hooks 打開了代碼復用性和擴展性的新大門
四、Vue Composition API
Vue Composition API 圍繞一個新的組件選項 setup 而創建。setup()
為 Vue 組件提供了狀態、計算值、watcher 和生命周期鈎子
API 並沒有讓原來的 API(現在被稱作 "Options-based API")消失。允許開發者 結合使用新舊兩種 APIs
可以在 Vue 2.x 中通過
@vue/composition-api
插件嘗試新 API
Vue Composition API 的例子就不多說了,常用。
五、兩者差別
1、原理
React Hook 底層是基於鏈表實現,調用的條件是每次組件被 render 的時候都會順序執行所有的 Hooks,所以下面的代碼會報錯
function App(){ const [name, setName] = useState('demo'); if(condition){ const [val, setVal] = useState(''); } }
因為底層是鏈表,每一個 Hook 的 next 是指向下一個 Hook 的,if 會導致順序不正確,從而導致報錯,所以 React 是不允許這樣使用 Hook 的。
Vue Hook 只會被注冊調用一次,Vue 能避開這些麻煩的問題,原因在於它對數據的響應是基於 proxy 的,對數據直接代理觀察。這種場景下,只要任何一個更改 data 的地方,相關的 function 或者 template 都會被重新計算,因此避開了 React 可能遇到的性能上的問題
React 數據更改的時候,會導致重新 render,重新 render 又會重新把 Hooks 重新注冊一次,所以 React 的上手難度更高一些
當然 React 對這些都有自己的解決方案,比如 useCallback,useMemo 等 Hook 的作用,這些官網都有介紹
2、代碼的執行
Vue 中,“鈎子”就是一個生命周期方法
Vue Composition API
的 setup()
晚於 beforeCreate 鈎子,早於 created 鈎子被調用;React Hooks 會在組件每次渲染時候運行,而 Vue setup() 只在組件創建時運行一次。
由於 React Hooks 會多次運行,所以 render 方法必須遵守某些規則,比如:不要在循環內部、條件語句中或嵌套函數里調用 Hooks
3、聲明狀態(Declaring state)
(1)React:useState
是 React Hooks 聲明狀態的主要途徑
- 可以向調用中傳入一個初始值作為參數
- 如果初始值的計算代價比較昂貴,也可以將其表達為一個函數,就只會在初次渲染時才會被執行
- useState() 返回一個數組,第一項是 state,第二項是一個 setter 函數
const [name, setName] = useState("Mary"); const [age, setAge] = useState(25);
useReducer
是個有用的替代選擇,其常見形式是接受一個 Redux 樣式的 reducer 函數和一個初始狀態:
const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } const [state, dispatch] = useReducer(reducer, initialState); dispatch({type: 'increment'}); // state 就會變為 {count: 1}
useReducer 還有一種 延遲初始化 的形式,傳入一個 init 函數作為第三個參數
(2)Vue:Vue 使用兩個主要的函數來聲明狀態:ref 和 reactive。
ref()
返回一個響應式對象,其內部值可通過其 value 屬性被訪問到。可以將其用於基本類型,也可以用於對象
reactive()
只將一個對象作為其輸入並返回一個對其的響應式代理。
同樣對 Vue 的比較熟,就不多說了
4、如何跟蹤依賴(How to track dependencies)
React 中的 useEffect Hook 允許在每次渲染之后運行某些副作用(如請求數據或使用 storage 等 Web APIs),並在下次執行回調之前或當組件卸載時運行一些清理工作
默認情況下,所有用 useEffect 注冊的函數都會在每次渲染之后運行,但可以定義真實依賴的狀態和屬性,以使 React 在相關依賴沒有改變的情況下(如由 state 中的其他部分引起的渲染)跳過某些 useEffect Hook 執行
// 傳遞一個依賴項的數組作為 useEffect Hook 的第二個參數,只有當 name 改變時才會更新 localStorage
function Form() { const [name, setName] = useState('Mary'); const [surname, setSurname] = useState('Poppins'); useEffect(function persistForm() { localStorage.setItem('formData', name); }, [name]); // ...
}
顯然,使用 React Hooks 時忘記在依賴項數組中詳盡地聲明所有依賴項很容易發生,會導致 useEffect 回調 "以依賴和引用了上一次渲染的陳舊數據而非最新數據" 從而無法被更新而告終。
解決方案:
eslint-plugin-React-Hooks
包含了一條 lint 提示關於丟失依賴項的規則useCallback 和 useMemo
也使用依賴項數組參數,以分別決定其是否應該返回緩存過的( memoized)與上一次執行相同的版本的回調或值。
在 Vue Composition API 的情況下,可以使用 watch() 執行副作用以響應狀態或屬性的改變。依賴會被自動跟蹤,注冊過的函數也會在依賴改變時被響應性的調用
export default { setup() { const name = ref("Mary"); const lastName = ref("Poppins"); watch(function persistForm() => { localStorage.setItem('formData', name.value); }); } }
由此可看,也就是說 Vue 的方式更簡便。
5、訪問組件生命周期(Access to the lifecycle of the component)
Hooks 在處理 React 組件的生命周期、副作用和狀態管理時表現出了心理模式上的完全轉變。 React 文檔中也指出:如果你熟悉 React 類生命周期方法,那么可以將 useEffect Hook 視為 componentDidMount、componentDidUpdate 及 componentWillUnmount 的合集
useEffect(() => { console.log("This will only run after initial render."); return () => { console.log("This will only run when component will unmount."); }; }, []);
強調的是,使用 React Hooks 時停止從生命周期方法的角度思考,而是考慮副作用依賴什么狀態,才更符合習慣
Vue Component API
通過 onMounted、onUpdated 和 onBeforeUnmount
:
setup() { onMounted(() => { console.log(`This will only run after initial render.`); }); onBeforeUnmount(() => { console.log(`This will only run when component will unmount.`); }); }
故在 Vue 的情況下的心理模式轉變更多在停止通過組件選項(data、computed, watch、methods、生命周期鈎子等)管理代碼,要轉向用不同函數處理對應的特性。(Vue 的這種轉變更平滑,也好理解,不知道是不是一直做 Vue 的原因,總感覺寫 React 很麻煩)
6、自定義代碼(Custom code)
React 團隊聚焦於 Hooks 上的原因之一,Custom Hooks 是可以替代之前社區中采納的諸如 Higher-Order Components 或 Render Props 等提供給開發者編寫可復用代碼的一種更優秀的方式
Custom Hooks 就是普通的 JavaScript 函數,在其內部利用了 React Hooks。它遵守的一個約定是其命名應該以 use 開頭,以明示這是被用作一個 Hook 的。
// custom Hook - 用於當 value 改變時向控制台打印日志
export function useDebugState(label, initialValue) { const [value, setValue] = useState(initialValue); useEffect(() => { console.log(`${label}: `, value); }, [label, value]); return [value, setValue]; } // 調用
const [name, setName] = useDebugState("Name", "Mary");
Vue 中,組合式函數(Composition Functions)與 Hooks 在邏輯提取和重用的目標上是一致的,在 Vue 中實現一個類似的 useDebugState 組合式函數
export function useDebugState(label, initialValue) { const state = ref(initialValue); watch(() => { console.log(`${label}: `, state.value); }); return state; } // elsewhere:
const name = useDebugState("Name", "Mary");
注意:根據約定,組合式函數也像 React Hooks 一樣使用 use 作為前綴以明示作用,並且表面該函數用於 setup() 中
7、Refs
React 的 useRef 和 Vue 的 ref 都允許你引用一個子組件 或 要附加到的 DOM 元素。
// react
const MyComponent = () => { const divRef = useRef(null); useEffect(() => { console.log("div: ", divRef.current) }, [divRef]); return ( <div ref={divRef}>
<p>My div</p>
</div> ) } // vue 的就不寫了
8、附加的函數(Additional functions)
React Hooks 在每次渲染時都會運行,沒有一個等價於 Vue 中 computed 函數的方法。所以你可以自由地聲明一個變量,其值基於狀態或屬性,並將指向每次渲染后的最新值:
const [name, setName] = useState("Mary"); const [age, setAge] = useState(25); const description = `${name} is ${age} years old`;
Vue 中,setup() 只運行一次。因此需要定義計算屬性,其應該觀察某些狀態更改並作出相應的更新:
const name = ref("Mary"); const age = ref(25); const description = computed(() => `${name.value} is ${age.value} years old`);
計算一個值開銷比較昂貴。你不會想在組件每次渲染時都計算它。React 包含了針對這點的 useMemo Hook
:React 建議你使用 useMemo 作為一個性能優化手段, 而非一個任何一個依賴項改變之前的緩存值
Vue 的 computed 執行自動的依賴追蹤,所以它不需要一個依賴項數組
9、Context 和 provide/inject
React 中的 useContext Hook,可以作為一種讀取特定上下文當前值的新方式。返回的值通常由最靠近的一層 <MyContext.Provider>
祖先樹的 value 屬性確定
// context object
const ThemeContext = React.createContext('light'); // provider
<ThemeContext.Provider value="dark">
// consumer
const theme = useContext(ThemeContext);
Vue 中類似的 API 叫 provide/inject
。在 Vue 2.x 中作為組件選項存在,在 Composition API 中增加了一對用在 setup() 中的 provide 和 inject
函數:
// key to provide
const ThemeSymbol = Symbol(); // provider
provide(ThemeSymbol, ref("dark")); // consumer
const value = inject(ThemeSymbol);
如果你想保持響應性,必須明確提供一個 ref/reactive 作為值。
10、在渲染上下文中暴露值(Exposing values to render context)
(1)在 React 的情況下
- 所有 Hooks 代碼都在組件中定義
- 且你將在同一個函數中返回要渲染的 React 元素
所以你對作用域中的任何值擁有完全訪問能力,就像在任何 JavaScript 代碼中的一樣
const Fibonacci = () => { const [nth, setNth] = useState(1); const nthFibonacci = useMemo(() => fibNaive(nth), [nth]); return ( <section>
<label> Number: <input type="number" value={nth} onChange={e => setNth(e.target.value)} />
</label>
<p>nth Fibonacci number: {nthFibonacci}</p>
</section> ); };
(2)在 Vue 的情況下
- 第一,在 template 或 render 選項中定義模板
- 第二,使用單文件組件,就要從 setup() 中返回一個包含了你想輸出到模板中的所有值的對象,由於要暴露的值很可能過多,返回語句也容易變得冗長
要達到 React 同樣簡潔表現的一種方式是從 setup() 自身中返回一個渲染函數。不過,模板在 Vue 中是更常用的一種做法,所以暴露一個包含值的對象,是你使用 Vue Composition API 時必然會多多遭遇的情況。
總結:
React 和 Vue 都有屬於屬於自己的“驚喜”,無優劣之分,自 React Hooks 在 2018 年被引入,社區利用其產出了很多優秀的作品,自定義 Hooks 的可擴展性也催生了許多開源貢獻。
Vue 受 React Hooks 啟發將其調整為適用於自己框架的方式,這也成為這些不同的技術如何擁抱變化且分享靈感和解決方案的成功案例
作者:微醫前端團隊 鏈接:https://juejin.cn/post/6847902223918170126