[譯] 對比 React Hooks 和 Vue Composition API

原文:dev.to/voluntadpea…html

Vue 最近提出了 Composition API RFC,一種新的書寫 Vue 組件的 API;該 API 受到 React Hooks 的啓發,但有一些有趣的差別,也就是本文要探討的內容。該 RFC 始自於在社區某些部分受到 大量非議 的以前一個叫作 Function-based Component API 的版本 -- 人們擔憂 Vue 開始變得更復雜而不像你們最初喜歡它時那樣是個簡單的庫了。vue

參閱《在 React 和 Vue 中嚐鮮 Hooks》一文react

Vue 核心團隊解決了圍繞首個 RFC 的困惑並在新的版本中提出了一些引人關注的調整,也對提案改變的背後動機提供了進一步的看法。若是你對向 Vue 核心團隊給出一些關於新提案反饋方面感興趣,能夠參與到 github.com/vuejs/rfcs/… 中。git

注意: Vue Composition API 仍在不斷改進,會收到特性改變的影響。在 Vue 3.0 到來以前不要把 Vue Composition API 視爲 100% 肯定的。github

React Hooks 容許你 "勾入" 諸如組件狀態和反作用處理等 React 功能中。Hooks 只能用在函數組件中,並容許咱們在不須要建立類的狀況下將狀態、反作用處理和更多東西帶入組件中。自從 2018 年被引入,社區對其一見鍾情。npm

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

那麼,開始學習 React Hooks 和 Vue Composition API 不一樣的方面並記錄某些咱們會遇到的區別吧 ⏯數組

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

useStateuseEffect 是 React Hooks 中的一些例子,使得函數組件中也能增長狀態和運行反作用;稍後咱們還會看到其餘 hooks,甚至能自定義一個。這些 hooks 打開了代碼複用性和擴展性的新大門。性能優化

Vue Composition 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>

複製代碼

Vue Composition API 圍繞一個新的組件選項 setup 而建立。setup() 爲 Vue 組件提供了狀態、計算值、watcher 和生命週期鉤子。

這個新的 API 並無讓原來的 API(如今被稱做 "Options-based API")消失。提案的當前迭代甚至容許開發者 結合使用新舊兩種 APIs

注意:能夠在 Vue 2.x 中經過 @vue/composition-api 插件嘗試新 API。

代碼的執行

Vue Composition API 的 setup() 晚於 beforeCreate 鉤子(在 Vue 中,「鉤子」就是一個生命週期方法)而早於 created 鉤子被調用。這是咱們能夠分辨 React Hooks 和 Vue Composition API 的首個區別, React hooks 會在組件每次渲染時候運行,而 Vue setup() 只在組件建立時運行一次。由於前者能夠屢次運行,因此 render 方法必須遵照 某些規則,其中之一是:

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

直接貼一段 React 文檔中的代碼來展現這一點:

function Form() {
  // 1. 使用 name 狀態變量
  const [name, setName] = useState('Mary');

  // 2. 使用一個持久化表單的反作用
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }
  // 3. 使用 surname 狀態變量
  const [surname, setSurname] = useState('Poppins');

  // 4. 使用一個更新 title 的反作用
  useEffect(function updateTitle() {
    document.title = `${name} ${surname}`;
  });

  // ...
}

複製代碼

React 在內部保持了對咱們用於組件中全部 hooks 的跟蹤。在本例中,咱們用了四個 hooks。注意第一個 useEffect 調用是如何條件性的完成的,因爲首次渲染中 name 會被默認值 'Mary' 賦值,條件會被評估爲 true,React 也會知道須要按順序的保持對全部四個 hooks 的跟蹤。但如若在另外一次渲染中 name 爲空會發生什麼?在那種狀況下,React 將不知道第二個 useState hook 該返回什麼 😱(譯註:React 默認靠 hook 調用的順序爲其匹配對應的狀態,連續兩個 useState 會形成後面的 hook 提早執行)。要避免相似的問題,強烈推薦在處理 React Hooks 時使用一個 eslint-plugin-react-hooks 插件,它也默認包含在了 Create React App 中。

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

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

回過頭看看 Vue,和上例等價的寫法大概是這樣:

export default {
  setup() {
    // 1. 使用 name 狀態變量
    const name = ref("Mary");
    // 2. 使用一個 watcher 以持久化表單
    if(name.value !== '') {
      watch(function persistForm() => {
        localStorage.setItem('formData', name.value);
      });
    }
   // 3. 使用 surname 狀態變量
   const surname = ref("Poppins");
   // 4. 使用一個 watcher 以更新 title
   watch(function updateTitle() {
     document.title = `${name.value} ${surname.value}`;
   });
  }
}
複製代碼

由於 setup() 只會運行一次,咱們是能夠將 Composition API 中不一樣的函數 (reactiverefcomputedwatch、生命週期鉤子等) 做爲循環或條件語句的一部分的。

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

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

聲明狀態

useState 是 React Hooks 聲明狀態的主要途徑。你能夠向調用中傳入一個初始值做爲參數;而且若是初始值的計算代價比較昂貴,也能夠將其表達爲一個函數,這樣就只會在初次渲染時纔會被執行了。

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

useState() 返回一個數組,第一項是 state,第二項是一個 setter 函數。一般可使用 Array destructuring 語法獲得它們。

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 則因爲其自然的反應式特性,有着不一樣的作法。存在兩個主要的函數來聲明狀態:refreactive

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),會失去其反應性(譯註:由於是對整個對象作的代理)。因此你須要定義一個指向對象的引用,並經過其訪問狀態屬性。

Composition API 提供了兩個助手函數以處理 refs 和 reactive 對象。

若是必要的話,isRef() 可被用來條件性地獲取 value 屬性(好比 isRef(myVar) ? myVar.value : myVar)。

toRefs() 則將反應式對象轉換爲普通對象,該對象上的全部屬性都自動轉換爲 ref。這對於從自定義組合式函數中返回對象時特別有用(這也容許了調用側正常使用結構的狀況下還能保持反應性)。

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })

  return toRefs(state)
}

const {foo, bar} = useFeatureX();
複製代碼

RFC 中用 一整個章節 比較了 refreactive,在其結尾總結了使用這兩個函數時可能的處理方式:

  1. 像你在正常的 JavaScript 中聲明基本類型變量和對象變量那樣去使用 refreactive 便可。在這種方式下,推薦使用一個 IDE 支持的類型系統。

  2. 只要用到 reactive 的時候,要記住從 composition 函數中返回反應式對象時得使用 toRefs()。這樣作減小了過多使用 ref 時的開銷,但並不會消減熟悉該概念的必要。

如何跟蹤依賴

React 中的 useEffect hook 容許咱們在每次渲染以後運行某些反作用(如請求數據或使用 storage 等 Web APIs),並視須要在下次執行回調以前或當組件卸載時運行一些清理工做。默認狀況下,全部用 useEffect 註冊的函數都會在每次渲染以後運行,但咱們能夠定義真實依賴的狀態和屬性,以使 React 在相關依賴沒有改變的狀況下(如由 state 中的其餘部分引發的渲染)跳過某些 useEffect hook 執行。回到以前 Form 的例子,咱們能夠傳遞一個依賴項的數組做爲 useEffect hook 的第二個參數:

function Form() {
  const [name, setName] = useState('Mary');
  const [surname, setSurname] = useState('Poppins');
  useEffect(function persistForm() {
      localStorage.setItem('formData', name);
  }, [name]);

  // ...
}
複製代碼

這樣一來,只有當 name 改變時纔會更新 localStorage。使用 React Hooks 時一個常見的 bug 來源就是忘記在依賴項數組中詳盡地聲明全部依賴項;這可能讓 useEffect 回調以依賴和引用了上一次渲染的陳舊數據而非最新數據從而沒法被更新而了結。幸運的是,eslint-plugin-react-hooks 也包含了一條 lint 提示關於丟失依賴項的規則。

useCallbackuseMemo 也使用依賴項數組參數,以分別決定其是否應該返回緩存過的( memoized)與上一次執行相同的版本的回調或值。

在 Vue Composition API 的狀況下,可使用 watch() 執行反作用以響應狀態或屬性的改變。多虧了 Vue 的反應式系統,依賴會被自動跟蹤,註冊過的函數也會在依賴改變時被反應性的調用。回到例子中:

export default {
  setup() {
    const name = ref("Mary");
    const lastName = ref("Poppins");
    watch(function persistForm() => {
      localStorage.setItem('formData', name.value);
    });
  }
}
複製代碼

在 watcher 首次運行後,name 會做爲一個依賴項被跟蹤,而稍後當其值改變時,watcher 會再次運行。

訪問組件生命週期

Hooks 在處理 React 組件的生命週期、反作用和狀態管理時表現出了心理模式上的徹底轉變。React 社區中的一位活躍分子 Ryan Florence,曾表示從類組件切換到 hooks 有一個心理轉換過程,而且 React 文檔中也指出:

若是你熟悉 React 類生命週期方法,那麼能夠將 useEffect Hook 視爲 componentDidMountcomponentDidUpdatecomponentWillUnmount 的合集

但其實也有可能控制 useEffect 什麼時候運行,並讓咱們更接近生命週期中運行反作用的心理模式:

useEffect(() => {
  console.log("這段只在初次渲染後運行");
  return () => { console.log("這裏會在組件將要卸載時運行"); };
}, []);
複製代碼

但要再次強調的是,使用 React Hooks 時中止從生命週期方法的角度思考,而是考慮反作用依賴什麼狀態,纔是更符合習慣的。順便一提的是,Svelte 的建立者 Rich Harris 發表了他在 NYC React meetup 上演講的 some insightful slides,其間他探究了 React 爲了未來的新特性(好比 concurrent mode)可用所作的妥協以及 Svelte 何其的區別。這將幫助你理解從思考反作用發生在組件生命週期何處到 做爲渲染自己一部分的反作用 的轉變。來自 React 核心團隊的 Sebastian Markbåge 寫的 further expands here 也解釋了 React 前進的方向和爲相似 Svelte 或 Vue 式的反應性系統做出的妥協。

另外一方面的 Vue Component API,讓咱們經過 onMountedonUpdatedonBeforeUnmount 等仍能夠訪問 生命週期鉤子 (Vue 世界中對生命週期方法的等價稱呼):

setup() {
  onMounted(() => {
    console.log(`這段只在初次渲染後運行`);
  });
  onBeforeUnmount(() => {
    console.log(`這裏會在組件將要卸載時運行`);
  });
}
複製代碼

故而在 Vue 的狀況下的心理模式轉變動多在中止經過組件選項(datacomputed, watchmethods、生命週期鉤子等)管理代碼這點上,要轉向用不一樣函數處理對應的特性。RFC 包含一個經過選項 vs. 經過邏輯關注點管理代碼的 示例和對照大全

自定義代碼

React 團隊意圖聚焦於 Hooks 上的一方面,是比之於先前社區中採納的諸如 Higher-Order ComponentsRender Props 等替代方式,提供給開發者編寫可複用代碼的更佳方式。Custom Hooks 正是他們帶來的答案。

Custom Hooks 就是普通的 JavaScript 函數,在其內部利用了 React Hooks。它遵照的一個約定是其命名應該以 use 開頭,以明示這是被用做一個 hook 的。

export function useDebugState(label, initialValue) {
  const [value, setValue] = useState(initialValue);
  useEffect(() => {
    console.log(`${label}: `, value);
  }, [label, value]);
  return [value, setValue];
}
複製代碼

這個 Custom Hook 的小例子可被做爲一個 useState 的替代品使用,用於當 value 改變時向控制檯打印日誌:

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;
}

// 在其餘某處:
const name = useDebugState("Name", "Mary");
複製代碼

注意:根據約定,組合式函數也像 React Hooks 同樣使用 use 做爲前綴以明示做用,而且表面該函數用於 setup()

Refs

React 的 useRef 和 Vue 的 ref 都容許你引用一個子組件(若是是 React 則是一個類組件或是被 React.forwardRef 包裝的組件)或要附加到的 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>
    )
  }
}
複製代碼

注意 Vue 2.x 且用 @vue/composition-api 插件的狀況下,不支持setup() 返回的渲染函數中經過 JSX 分配模版 refs, 但根據 當前的 RFC,以上語法在 Vue 3.0 中是合法的。

React 中的 useRef Hook 不止對於取得 DOM 元素的訪問有用。亦可用在你想保持在渲染函數中但並非 state 一部分的(也就是它們的改變觸發不了從新渲染)任何類型的可變值(mutable value)上。可將這些可變值視爲類組件中的 "實例變量" 。 這是一個例子:

const timerRef = useRef(null);
useEffect(() => {
  timerRef.current = setInterval(() => {
    setSecondsPassed(prevSecond => prevSecond + 1);
  }, 1000);
  return () => {
    clearInterval(timerRef.current);
  };
}, []);

return (
  <button onClick={() => { clearInterval(timerRef.current); }} > 中止 timer </button>
)
複製代碼

在 Vue Composition API 中,如咱們在幾乎全部文中以前的例子中所見,ref 可被用於定義反應式狀態。使用 Composition API 的時候,模版 refs 和反應式 refs 是一致的。

附加的函數

因爲 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`);
複製代碼

照例,記住 refs 是容器,而值要經過訪問 value 屬性得到 :p

若是計算一個值開銷比較昂貴又如何呢?你不會想在組件每次渲染時都計算它。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> ); }; 複製代碼

useMemo 一樣指望一個依賴項數組以獲知其在什麼時候應該計算一個新值。React 建議你使用 useMemo 做爲一個性能優化手段而非一個直到任何一個依賴項改變以前的緩存值

做爲一個補充說明:Kent C. Dodds 有一篇很是棒的文章 "useMemo 和 useCallback" 說明了不少 useMemouseCallback 非必要的場景。

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

useCallback 相似於 useMemo,但它是用來緩存一個回調函數的。事實上 useCallback(fn, deps) 等價於 useMemo(() => fn, deps)。其理想用例是當咱們須要在屢次渲染間保持引用相等性時,好比將回調傳遞給一個用 React.memo 定義的已優化子組件,而咱們想要避免其沒必要要的重複渲染時。

鑑於 Vue Composition API 的自然特性,並無等同於 useCallback 的函數。setup() 中的任何回調函數都只會定義一次。

Context 和 provide/inject

React 中的 useContext hook,能夠做爲一種讀取特定上下文當前值的新方式。返回的值一般由最靠近的一層 <MyContext.Provider> 祖先樹的 value 屬性肯定。其等價於一個類中的 static contextType = MyContext ,或是 <MyContext.Consumer> 組件。

// context 對象
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() 中的 provideinject 函數:

// key to provide
const ThemeSymbol = Symbol();

// provider
provide(ThemeSymbol, ref("dark"));

// consumer
const value = inject(ThemeSymbol);
複製代碼

注意,若是你想保持反應性,必須明確提供一個 ref/reactive 做爲值。

在渲染上下文中暴露值

在 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 的狀況下,你要在 templaterender 選項中定義模板;若是你使用單文件組件,就要從 setup() 中返回一個包含了你想輸出到模板中的全部值的對象。因爲要暴露的值極可能過多,你的返回語句也容易變得冗長,這一點在 RFC 的 Verbosity of the Return Statement 章節 中有所說起:

<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() 自身中返回一個渲染函數:

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

不過,模板在 Vue 中是更流行的一種作法,因此暴露一個包含值的對象,是你使用 Vue Composition API 時必然會多多遭遇的狀況。

總結

每一個框架都有驚喜時刻。自從 React Hooks 在 2018 年被引入,社區利用它們傑做頻出,而且自定義 Hooks 的可擴展性也催生了 許多開源貢獻 ,讓咱們能夠輕易的加入本身的項目中。Vue 受 React Hooks 啓發並將其調整爲適用於其框架的方式,這也成爲這些不一樣的技術如何擁抱變化並分享靈感和解決方案的成功案例。我對 Vue 3 的到來已經急不可耐,迫切想看到它的解鎖能帶來的可能性了。



--End--

搜索 fewelife 關注公衆號

轉載請註明出處

相關文章
相關標籤/搜索