對(duì)比 React Hooks 和 Vue Composition API

場(chǎng)景 Hook 的時(shí)代意義 React Hooks Vue Composition API React Hooks?vs?Vue Composition API總結(jié)
場(chǎng)景
先理解什么是hook,拿react的介紹來(lái)看,它的定義是:
它可以讓你在不編寫(xiě) class 的情況下,讓你在函數(shù)組件里“鉤入” React state 及生命周期等特性的函數(shù)
對(duì)于 Vue 提出的新的書(shū)寫(xiě) Vue 組件的 API:Composition API RFC,作用也是類似,所以我們也可以像react一樣叫做 vue hooks
該 API 受到 React Hooks 的啟發(fā) 但有一些有趣的差異,規(guī)避了一些react的問(wèn)題
hook的時(shí)代意義
框架是服務(wù)于業(yè)務(wù)的,業(yè)務(wù)中很難避免的一個(gè)問(wèn)題就是 -- 邏輯復(fù)用,同樣的功能,同樣的組件,在不一樣的場(chǎng)合下,我們有時(shí)候不得不去寫(xiě)2+次,為了避免耦合,后來(lái)各大框架紛紛想出了一些辦法,比如 minix, render props, 高階組件等實(shí)現(xiàn)邏輯上的復(fù)用,但是都有一些額外的問(wèn)題
minix 與組件之間存在隱式依賴,可能產(chǎn)生沖突。傾向于增加更多狀態(tài),降低了應(yīng)用的可預(yù)測(cè)性 高階組件 多層包裹嵌套組件,增加了復(fù)雜度和理解成本,對(duì)于外層是黑盒 Render Props 使用繁瑣,不好維護(hù), 代碼體積過(guò)大,同樣容易嵌套過(guò)深 ...
hook的出現(xiàn)是劃時(shí)代的,通過(guò)function抽離的方式,實(shí)現(xiàn)了復(fù)雜邏輯的內(nèi)部封裝:
邏輯代碼的復(fù)用 減小了代碼體積 沒(méi)有this的煩惱
React Hooks
React Hooks 允許你 "勾入" 諸如組件狀態(tài)和副作用處理等 React 功能中。Hooks 只能用在函數(shù)組件中,并允許我們?cè)诓恍枰獎(jiǎng)?chuàng)建類的情況下將狀態(tài)、副作用處理和更多東西帶入組件中。
React 核心團(tuán)隊(duì)奉上的采納策略是不反對(duì)類組件,所以你可以升級(jí) React 版本、在新組件中開(kāi)始嘗試 Hooks,并保持既有組件不做任何更改
例子:
import React, { useState, useEffect } from "react";const NoteForm = ({ onNoteSent }) => {const [currentNote, setCurrentNote] = useState("");useEffect(() => {console.log(`Current note: ${currentNote}`);});return (onSubmit={e => {onNoteSent(currentNote);setCurrentNote("");e.preventDefault();}}>Note: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 : "");}}/>);};
useState 和 useEffect 是 React Hooks 中的一些例子,使得函數(shù)組件中也能增加狀態(tài)和運(yùn)行副作用 還有更多其他 hooks, 甚至能自定義一個(gè),hooks 打開(kāi)了代碼復(fù)用性和擴(kuò)展性的新大門(mén)
Vue Composition API
Vue Composition API 圍繞一個(gè)新的組件選項(xiàng) setup 而創(chuàng)建。setup()?為 Vue 組件提供了狀態(tài)、計(jì)算值、watcher 和生命周期鉤子
API 并沒(méi)有讓原來(lái)的 API(現(xiàn)在被稱作 "Options-based API")消失。允許開(kāi)發(fā)者 結(jié)合使用新舊兩種 APIs
可以在 Vue 2.x 中通過(guò)?
@vue/composition-api?插件嘗試新 API
例子:
Note:
React Hooks?vs?Vue Composition API
原理
React hook 底層是基于鏈表實(shí)現(xiàn),調(diào)用的條件是每次組件被render的時(shí)候都會(huì)順序執(zhí)行所有的hooks,所以下面的代碼會(huì)報(bào)錯(cuò)
function App(){const [name, setName] = useState('demo');if(condition){const [val, setVal] = useState('');}}
因?yàn)榈讓邮擎湵恚恳粋€(gè)hook的next是指向下一個(gè)hook的,if會(huì)導(dǎo)致順序不正確,從而導(dǎo)致報(bào)錯(cuò),所以react是不允許這樣使用hook的。
vue hook 只會(huì)被注冊(cè)調(diào)用一次,vue 能避開(kāi)這些麻煩的問(wèn)題,原因在于它對(duì)數(shù)據(jù)的響應(yīng)是基于proxy的,對(duì)數(shù)據(jù)直接代理觀察。這種場(chǎng)景下,只要任何一個(gè)更改data的地方,相關(guān)的function或者template都會(huì)被重新計(jì)算,因此避開(kāi)了react可能遇到的性能上的問(wèn)題
react數(shù)據(jù)更改的時(shí)候,會(huì)導(dǎo)致重新render,重新render又會(huì)重新把hooks重新注冊(cè)一次,所以react的上手難度更高一些
當(dāng)然react對(duì)這些都有自己的解決方案,比如useCallback,useMemo等hook的作用,這些官網(wǎng)都有介紹
代碼的執(zhí)行
Vue 中,“鉤子”就是一個(gè)生命周期方法
Vue Composition API?的?setup()?晚于?beforeCreate?鉤子,早于?created?鉤子被調(diào)用React hooks 會(huì)在組件每次渲染時(shí)候運(yùn)行,而 Vue?setup()?只在組件創(chuàng)建時(shí)運(yùn)行一次
由于 React hooks 會(huì)多次運(yùn)行,所以 render 方法必須遵守某些規(guī)則,比如:
不要在循環(huán)內(nèi)部、條件語(yǔ)句中或嵌套函數(shù)里調(diào)用 Hooks
// React 文檔中的示例代碼:function Form() {// 1. Use the name state variableconst [name, setName] = useState('Mary');// 2. Use an effect for persisting the formif (name !== '') {useEffect(function persistForm() {localStorage.setItem('formData', name);});}// 3. Use the surname state variableconst [surname, setSurname] = useState('Poppins');// 4. Use an effect for updating the titleuseEffect(function updateTitle() {document.title = `${name} ${surname}`;});// ...}
如果想要在 name 為空時(shí)也運(yùn)行對(duì)應(yīng)的副作用, 可以簡(jiǎn)單的將條件判斷語(yǔ)句移入 useEffect 回調(diào)內(nèi)部:
useEffect(function persistForm() {if (name !== '') {localStorage.setItem('formData', name);}});
對(duì)于以上的實(shí)現(xiàn),Vue 寫(xiě)法如下:
export default {setup() {// 1. Use the name state variableconst name = ref("Mary");// 2. Use a watcher for persisting the formif(name.value !== '') {watch(function persistForm() => {localStorage.setItem('formData', name.value);});}// 3. Use the surname state variableconst surname = ref("Poppins");// 4. Use a watcher for updating the titlewatch(function updateTitle() {document.title = `${name.value} ${surname.value}`;});}}
Vue 中 setup() 只會(huì)運(yùn)行一次,可以將 Composition API 中不同的函數(shù) (reactive、ref、computed、watch、生命周期鉤子等) 作為循環(huán)或條件語(yǔ)句的一部分
但 if 語(yǔ)句 和 react hooks 一樣只運(yùn)行一次,所以它在 name 改變時(shí)也無(wú)法作出反應(yīng),除非我們將其包含在 watch 回調(diào)的內(nèi)部
watch(function persistForm() => {if(name.value !== '') {localStorage.setItem('formData', name.value);}});
聲明狀態(tài)(Declaring state)
react
useState?是 React Hooks 聲明狀態(tài)的主要途徑
可以向調(diào)用中傳入一個(gè)初始值作為參數(shù) 如果初始值的計(jì)算代價(jià)比較昂貴,也可以將其表達(dá)為一個(gè)函數(shù),就只會(huì)在初次渲染時(shí)才會(huì)被執(zhí)行
useState() 返回一個(gè)數(shù)組,第一項(xiàng)是 state,第二項(xiàng)是一個(gè) setter 函數(shù)
const [name, setName] = useState("Mary");const [age, setAge] = useState(25);console.log(`${name} is ${age} years old.`);
useReducer?是個(gè)有用的替代選擇,其常見(jiàn)形式是接受一個(gè) Redux 樣式的 reducer 函數(shù)和一個(gè)初始狀態(tài):
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?就會(huì)變?yōu)?{count: 1}
useReducer?還有一種?延遲初始化?的形式,傳入一個(gè) init 函數(shù)作為第三個(gè)參數(shù)
Vue
Vue 使用兩個(gè)主要的函數(shù)來(lái)聲明狀態(tài):ref 和 reactive。
ref()?返回一個(gè)反應(yīng)式對(duì)象,其內(nèi)部值可通過(guò)其 value 屬性被訪問(wèn)到。可以將其用于基本類型,也可以用于對(duì)象
const name = ref("Mary");const age = ref(25);watch(() => {console.log(`${name.value} is ${age.value} years old.`);});
reactive()?只將一個(gè)對(duì)象作為其輸入并返回一個(gè)對(duì)其的反應(yīng)式代理
const state = reactive({name: "Mary",age: 25,});watch(() => {console.log(`${state.name} is ${state.age} years old.`);});
注意:
使用?ref?時(shí)需要 用?value?屬性訪問(wèn)其包含的值(除非在 template 中,Vue 允許你省略它) 用 reactive 時(shí),要注意如果使用了對(duì)象解構(gòu)(destructure),會(huì)失去其反應(yīng)性。所以需要定義一個(gè)指向?qū)ο蟮囊茫⑼ㄟ^(guò)其訪問(wèn)狀態(tài)屬性。
總結(jié)使用這兩個(gè)函數(shù)的處理方式:
像在正常的 JavaScript 中聲明基本類型變量和對(duì)象變量那樣去使用?ref?和?reactive?即可 只要用到 reactive 的時(shí)候,要記住從 composition 函數(shù)中返回反應(yīng)式對(duì)象時(shí)得使用 toRefs()。這樣做減少了過(guò)多使用 ref 時(shí)的開(kāi)銷
// toRefs() 則將反應(yīng)式對(duì)象轉(zhuǎn)換為普通對(duì)象,該對(duì)象上的所有屬性都自動(dòng)轉(zhuǎn)換為 reffunction useFeatureX() {const state = reactive({foo: 1,bar: 2})return toRefs(state)}const {foo, bar} = useFeatureX();
如何跟蹤依賴(How to track dependencies)
React 中的?useEffect?hook 允許在每次渲染之后運(yùn)行某些副作用(如請(qǐng)求數(shù)據(jù)或使用 storage 等 Web APIs),并在下次執(zhí)行回調(diào)之前或當(dāng)組件卸載時(shí)運(yùn)行一些清理工作
默認(rèn)情況下,所有用?useEffect?注冊(cè)的函數(shù)都會(huì)在每次渲染之后運(yùn)行,但可以定義真實(shí)依賴的狀態(tài)和屬性,以使 React 在相關(guān)依賴沒(méi)有改變的情況下(如由 state 中的其他部分引起的渲染)跳過(guò)某些?useEffect?hook 執(zhí)行
// 傳遞一個(gè)依賴項(xiàng)的數(shù)組作為?useEffect?hook 的第二個(gè)參數(shù),只有當(dāng)?name?改變時(shí)才會(huì)更新?localStoragefunction Form() {const [name, setName] = useState('Mary');const [surname, setSurname] = useState('Poppins');useEffect(function persistForm() {localStorage.setItem('formData', name);}, [name]);// ...}
顯然,使用 React Hooks 時(shí)忘記在依賴項(xiàng)數(shù)組中詳盡地聲明所有依賴項(xiàng)很容易發(fā)生,會(huì)導(dǎo)致?useEffect?回調(diào) "以依賴和引用了上一次渲染的陳舊數(shù)據(jù)而非最新數(shù)據(jù)" 從而無(wú)法被更新而告終
解決方案:
eslint-plugin-react-hooks?包含了一條 lint 提示關(guān)于丟失依賴項(xiàng)的規(guī)則useCallback?和?useMemo?也使用依賴項(xiàng)數(shù)組參數(shù),以分別決定其是否應(yīng)該返回緩存過(guò)的( memoized)與上一次執(zhí)行相同的版本的回調(diào)或值。
在 Vue Composition API 的情況下,可以使用 watch()?執(zhí)行副作用以響應(yīng)狀態(tài)或?qū)傩缘母淖儭R蕾嚂?huì)被自動(dòng)跟蹤,注冊(cè)過(guò)的函數(shù)也會(huì)在依賴改變時(shí)被反應(yīng)性的調(diào)用
export default {setup() {const name = ref("Mary");const lastName = ref("Poppins");watch(function persistForm() => {localStorage.setItem('formData', name.value);});}}
訪問(wèn)組件生命周期(Access to the lifecycle of the component)
Hooks 在處理 React 組件的生命周期、副作用和狀態(tài)管理時(shí)表現(xiàn)出了心理模式上的完全轉(zhuǎn)變。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."); };}, []);
強(qiáng)調(diào)的是,使用 React Hooks 時(shí)停止從生命周期方法的角度思考,而是考慮副作用依賴什么狀態(tài),才更符合習(xí)慣
Vue Component API?通過(guò)?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 的情況下的心理模式轉(zhuǎn)變更多在停止通過(guò)組件選項(xiàng)(data、computed, watch、methods、生命周期鉤子等)管理代碼,要轉(zhuǎn)向用不同函數(shù)處理對(duì)應(yīng)的特性
自定義代碼(Custom code)
React 團(tuán)隊(duì)聚焦于 Hooks 上的原因之一,Custom Hooks 是可以替代之前社區(qū)中采納的諸如?Higher-Order Components?或?Render Props?等提供給開(kāi)發(fā)者編寫(xiě)可復(fù)用代碼的,一種更優(yōu)秀的方式
Custom Hooks 就是普通的 JavaScript 函數(shù),在其內(nèi)部利用了 React Hooks。它遵守的一個(gè)約定是其命名應(yīng)該以 use 開(kāi)頭,以明示這是被用作一個(gè) hook 的。
// custom hook - 用于當(dāng) value 改變時(shí)向控制臺(tái)打印日志export function useDebugState(label, initialValue) {const [value, setValue] = useState(initialValue);useEffect(() => {console.log(`${label}: `, value);}, [label, value]);return [value, setValue];}// 調(diào)用const [name, setName] = useDebugState("Name", "Mary");
Vue 中,組合式函數(shù)(Composition Functions)與 Hooks 在邏輯提取和重用的目標(biāo)上是一致的在 Vue 中實(shí)現(xiàn)一個(gè)類似的 useDebugState 組合式函數(shù)
export function useDebugState(label, initialValue) {const state = ref(initialValue);watch(() => {console.log(`${label}: `, state.value);});return state;}// elsewhere:const name = useDebugState("Name", "Mary");
注意:根據(jù)約定,組合式函數(shù)也像 React Hooks 一樣使用 use 作為前綴以明示作用,并且表面該函數(shù)用于 setup() 中
Refs
React 的 useRef 和 Vue 的 ref 都允許你引用一個(gè)子組件 或 要附加到的 DOM 元素。
React:
const MyComponent = () => {const divRef = useRef(null);useEffect(() => {console.log("div: ", divRef.current)}, [divRef]);return (My div
)}
Vue:
export default {setup() {const divRef = ref(null);onMounted(() => {console.log("div: ", divRef.value);});return () => (My div
)}}
附加的函數(shù)(Additional functions)
React Hooks 在每次渲染時(shí)都會(huì)運(yùn)行,沒(méi)有 一個(gè)等價(jià)于 Vue 中 computed 函數(shù)的方法。所以你可以自由地聲明一個(gè)變量,其值基于狀態(tài)或?qū)傩裕⒅赶蛎看武秩竞蟮淖钚轮担?/p>const [name, setName] = useState("Mary");const [age, setAge] = useState(25);const description = `${name} is ${age} years old`;
Vue 中,setup() 只運(yùn)行一次。因此需要定義計(jì)算屬性,其應(yīng)該觀察某些狀態(tài)更改并作出相應(yīng)的更新:
const name = ref("Mary");const age = ref(25);const description = computed(() => `${name.value} is ${age.value} years old`);
計(jì)算一個(gè)值開(kāi)銷比較昂貴。你不會(huì)想在組件每次渲染時(shí)都計(jì)算它。React 包含了針對(duì)這點(diǎn)的?useMemo hook:
function fibNaive(n) {if (n <= 1) return n;return fibNaive(n - 1) + fibNaive(n - 2);}const Fibonacci = () => {const [nth, setNth] = useState(1);const nthFibonacci = useMemo(() => fibNaive(nth), [nth]);return (Number:type="number"value={nth}onChange={e => setNth(e.target.value)}/>nth Fibonacci number: {nthFibonacci}
);};React 建議你使用?useMemo?作為一個(gè)性能優(yōu)化手段, 而非一個(gè)任何一個(gè)依賴項(xiàng)改變之前的緩存值
React advice you to use useMemo as a performance optimization and not as a guarantee that the value will remain memoized
Vue 的?computed?執(zhí)行自動(dòng)的依賴追蹤,所以它不需要一個(gè)依賴項(xiàng)數(shù)組
Context 和 provide/inject
React 中的 useContext hook,可以作為一種讀取特定上下文當(dāng)前值的新方式。返回的值通常由最靠近的一層??祖先樹(shù)的?value?屬性確定
// context objectconst ThemeContext = React.createContext('light');// provider// consumerconst theme = useContext(ThemeContext);
Vue 中類似的 API 叫?provide/inject。在 Vue 2.x 中作為組件選項(xiàng)存在,在 Composition API 中增加了一對(duì)用在 setup() 中的?provide 和 inject?函數(shù):
// key to provideconst ThemeSymbol = Symbol();// providerprovide(ThemeSymbol, ref("dark"));// consumerconst value = inject(ThemeSymbol);
如果你想保持反應(yīng)性,必須明確提供一個(gè) ref/reactive 作為值
在渲染上下文中暴露值(Exposing values to render context)
在 React 的情況下??
所有 hooks 代碼都在組件中定義 且你將在同一個(gè)函數(shù)中返回要渲染的 React 元素
所以你對(duì)作用域中的任何值擁有完全訪問(wèn)能力,就像在任何 JavaScript 代碼中的一樣:
const Fibonacci = () => {const [nth, setNth] = useState(1);const nthFibonacci = useMemo(() => fibNaive(nth), [nth]);return (Number:type="number"value={nth}onChange={e => setNth(e.target.value)}/>nth Fibonacci number: {nthFibonacci}
);};Vue 的情況下??
第一,在?template?或?render?選項(xiàng)中定義模板 第二,使用單文件組件,就要從?setup()?中返回一個(gè)包含了你想輸出到模板中的所有值的對(duì)象
由于要暴露的值很可能過(guò)多,返回語(yǔ)句也容易變得冗長(zhǎng)
Number:type="number"v-model="nth"/>nth Fibonacci number: {{nthFibonacci}}
}要達(dá)到 React 同樣簡(jiǎn)潔表現(xiàn)的一種方式是從 setup() 自身中返回一個(gè)渲染函數(shù)。不過(guò),模板在 Vue 中是更常用的一種做法,所以暴露一個(gè)包含值的對(duì)象,是你使用 Vue Composition API 時(shí)必然會(huì)多多遭遇的情況。
總結(jié)(Conclusion)
React 和 Vue都有屬于屬于自己的“驚喜”,無(wú)優(yōu)劣之分,自 React Hooks 在 2018 年被引入,社區(qū)利用其產(chǎn)出了很多優(yōu)秀的作品,自定義 Hooks 的可擴(kuò)展性也催生了許多開(kāi)源貢獻(xiàn)。
Vue 受 React Hooks 啟發(fā)將其調(diào)整為適用于自己框架的方式,這也成為這些不同的技術(shù)如何擁抱變化且分享靈感和解決方案的成功案例
最后, 希望大家早日實(shí)現(xiàn):成為前端高手的偉大夢(mèng)想!!!
歡迎交流~
《Composition API RFC》-?https://composition-api.vuejs.org/#summary 《React hooks》-?https://reactjs.org/docs/hooks-intro.html 《Comparing React Hooks with Vue Composition API》-?https://dev.to/voluntadpear/comparing-react-hooks-with-vue-composition-api-4b32
推薦閱讀
