Vue Composition API 和 React Hooks 對比

連鵬飛,微醫雲服務團隊前端開發工程師,帶着「偏見」去理解技術的世界javascript

場景

先理解什麼是 Hook,拿 React 的介紹來看,它的定義是:html

它可讓你在不編寫 Class 的狀況下,讓你在函數組件裏「鉤入」 React state 及生命週期等特性的函數前端

對於 Vue 提出的新的書寫 Vue 組件的 API:Composition API RFC,做用也是相似,因此咱們也能夠像 React 同樣叫作 Vue Hooksvue

  • 該 API 受到 React Hooks 的啓發
  • 但有一些有趣的差別,規避了一些 React 的問題

Hook 的時代意義

框架是服務於業務的,業務中很難避免的一個問題就是 -- 邏輯複用,一樣的功能,一樣的組件,在不同的場合下,咱們有時候不得不去寫 2+次,爲了不耦合,後來各大框架紛紛想出了一些辦法,好比 minix, render props, 高階組件等實現邏輯上的複用,可是都有一些額外的問題java

  • minix 與組件之間存在隱式依賴,可能產生衝突。傾向於增長更多狀態,下降了應用的可預測性
  • 高階組件 多層包裹嵌套組件,增長了複雜度和理解成本,對於外層是黑盒
  • Render Props 使用繁瑣,很差維護, 代碼體積過大,一樣容易嵌套過深
  • ...

Hook 的出現是劃時代的,經過 function 抽離的方式,實現了複雜邏輯的內部封裝:react

  • 邏輯代碼的複用
  • 減少了代碼體積
  • 沒有 this 的煩惱

React Hooks

React Hooks 容許你 "勾入" 諸如組件狀態和反作用處理等 React 功能中。Hooks 只能用在函數組件中,並容許咱們在不須要建立類的狀況下將狀態、反作用處理和更多東西帶入組件中。web

React 核心團隊奉上的採納策略是不反對類組件,因此你能夠升級 React 版本、在新組件中開始嘗試 Hooks,並保持既有組件不作任何更改api

例子:數組

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

例子:

<template>
 <form @submit="handleSubmit">  <label>  <span>Note:</span>  <input v-model="currentNote" @input="handleNoteInput">  </label>  <button type="submit">Send</button>  </form> </template>  <script> import { ref, watch } from "vue"; export default {  props: ["divRef"],  setup(props, context) {  const currentNote = ref("");  const handleNoteInput = e => {  const val = e.target.value && e.target.value.toUpperCase()[0];  const validNotes = ["A", "B", "C", "D", "E", "F", "G"];  currentNote.value = validNotes.includes(val) ? val : "";  };  const handleSubmit = e => {  context.emit("note-sent", currentNote.value);  currentNote.value = "";  e.preventDefault();  };   return {  currentNote,  handleNoteInput,  handleSubmit,  };  } }; </script> 複製代碼

差異

原理

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 的做用,這些官網都有介紹

代碼的執行

Vue 中,「鉤子」就是一個生命週期方法

  • Vue Composition API 的  setup() 晚於 beforeCreate 鉤子,早於 created 鉤子被調用
  • React Hooks 會在組件每次渲染時候運行,而 Vue setup() 只在組件建立時運行一次

因爲 React Hooks 會屢次運行,因此 render 方法必須遵照某些規則,好比:

不要在循環內部、條件語句中或嵌套函數裏調用 Hooks

// React 文檔中的示例代碼:
function Form() {  // 1. Use the name state variable  const [name, setName] = useState('Mary');   // 2. Use an effect for persisting the form  if (name !== '') {  useEffect(function persistForm() {  localStorage.setItem('formData', name);  });  }  // 3. Use the surname state variable  const [surname, setSurname] = useState('Poppins');   // 4. Use an effect for updating the title  useEffect(function updateTitle() {  document.title = `${name} ${surname}`;  });  // ... } 複製代碼

若是想要在 name 爲空時也運行對應的反作用, 能夠簡單的將條件判斷語句移入 useEffect 回調內部:

useEffect(function persistForm() {
 if (name !== '') {  localStorage.setItem('formData', name);  } }); 複製代碼

對於以上的實現,Vue 寫法以下:

export default {
 setup() {  // 1. Use the name state variable  const name = ref("Mary");  // 2. Use a watcher for persisting the form  if(name.value !== '') {  watch(function persistForm() => {  localStorage.setItem('formData', name.value);  });  }  // 3. Use the surname state variable  const surname = ref("Poppins");  // 4. Use a watcher for updating the title  watch(function updateTitle() {  document.title = `${name.value} ${surname.value}`;  });  } } 複製代碼

Vue 中 setup() 只會運行一次,能夠將 Composition API 中不一樣的函數 (reactive、ref、computed、watch、生命週期鉤子等) 做爲循環或條件語句的一部分

但 if 語句 和 React Hooks 同樣只運行一次,因此它在 name 改變時也沒法做出反應,除非咱們將其包含在 watch 回調的內部

watch(function persistForm() => {
 if(name.value !== '') {  localStorage.setItem('formData', name.value);  } }); 複製代碼

聲明狀態(Declaring state)

React

useState 是 React Hooks 聲明狀態的主要途徑

  • 能夠向調用中傳入一個初始值做爲參數
  • 若是初始值的計算代價比較昂貴,也能夠將其表達爲一個函數,就只會在初次渲染時纔會被執行

useState() 返回一個數組,第一項是 state,第二項是一個 setter 函數

const [name, setName] = useState("Mary");
const [age, setAge] = useState(25); console.log(`${name} is ${age} years old.`); 複製代碼

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 函數做爲第三個參數

Vue

Vue 使用兩個主要的函數來聲明狀態:ref 和 reactive。

ref() 返回一個反應式對象,其內部值可經過其 value 屬性被訪問到。能夠將其用於基本類型,也能夠用於對象

const name = ref("Mary");
const age = ref(25); watch(() => {  console.log(`${name.value} is ${age.value} years old.`); }); 複製代碼

reactive() 只將一個對象做爲其輸入並返回一個對其的反應式代理

const state = reactive({
 name: "Mary",  age: 25, }); watch(() => {  console.log(`${state.name} is ${state.age} years old.`); }); 複製代碼

注意

  • 使用 ref 時須要 用 value 屬性訪問其包含的值(除非在 template 中,Vue 容許你省略它)
  • 用 reactive 時,要注意若是使用了對象解構(destructure),會失去其反應性。因此須要定義一個指向對象的引用,並經過其訪問狀態屬性。

總結使用這兩個函數的處理方式:

  • 像在正常的 JavaScript 中聲明基本類型變量和對象變量那樣去使用 ref 和 reactive 便可
  • 只要用到 reactive 的時候,要記住從 composition 函數中返回反應式對象時得使用 toRefs()。這樣作減小了過多使用 ref 時的開銷
// toRefs() 則將反應式對象轉換爲普通對象,該對象上的全部屬性都自動轉換爲 ref
function useFeatureX() {  const state = reactive({  foo: 1,  bar: 2  })   return toRefs(state) }  const {foo, bar} = useFeatureX(); 複製代碼

如何跟蹤依賴(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);  });  } } 複製代碼

訪問組件生命週期(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、生命週期鉤子等)管理代碼,要轉向用不一樣函數處理對應的特性

自定義代碼(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() 中

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:

export default {
 setup() {  const divRef = ref(null);  onMounted(() => {  console.log("div: ", divRef.value);  });   return () => (  <div ref={divRef}>  <p>My div</p>  </div>  )  } } 複製代碼

附加的函數(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

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 (  <section>  <label>  Number:  <input  type="number"  value={nth}  onChange={e => setNth(e.target.value)}  />  </label>  <p>nth Fibonacci number: {nthFibonacci}</p>  </section>  ); }; 複製代碼

React 建議你使用 useMemo 做爲一個性能優化手段, 而非一個任何一個依賴項改變以前的緩存值

React advice you to use useMemo as a performance optimization and not as a guarantee that the value will remain memoized

Vue 的 computed 執行自動的依賴追蹤,因此它不須要一個依賴項數組

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 做爲值

在渲染上下文中暴露值(Exposing values to render context)

在 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>  ); }; 複製代碼

Vue 的狀況下

  • 第一,在 template 或 render 選項中定義模板
  • 第二,使用單文件組件,就要從 setup() 中返回一個包含了你想輸出到模板中的全部值的對象

因爲要暴露的值極可能過多,返回語句也容易變得冗長

<template>
 <section>  <label>  Number:  <input  type="number"  v-model="nth"  />  </label>  <p>nth Fibonacci number: {{nthFibonacci}}</p>  </section> </template> <script> export default {  setup() {  const nth = ref(1);  const nthFibonacci = computed(() => fibNaive(nth.value));  return { nth, nthFibonacci };  } }; </script> } 複製代碼

要達到 React 一樣簡潔表現的一種方式是從 setup() 自身中返回一個渲染函數。不過,模板在 Vue 中是更經常使用的一種作法,因此暴露一個包含值的對象,是你使用 Vue Composition API 時必然會多多遭遇的狀況。

總結(Conclusion)

React 和 Vue 都有屬於屬於本身的「驚喜」,無優劣之分,自 React Hooks 在 2018 年被引入,社區利用其產出了不少優秀的做品,自定義 Hooks 的可擴展性也催生了許多開源貢獻。

Vue 受 React Hooks 啓發將其調整爲適用於本身框架的方式,這也成爲這些不一樣的技術如何擁抱變化且分享靈感和解決方案的成功案例


參考

  1. https://composition-api.vuejs.org/#summary
  2. https://Reactjs.org/docs/Hooks-intro.html
  3. https://dev.to/voluntadpear/comparing-React-Hooks-with-vue-composition-api-4b32

相關文章
相關標籤/搜索