前幾年我一直在使用 React。最初僅有 React,後來使用 Redux 和 React 的其他庫(react-router、react-redux、prop-types 等)配合使用。我喜歡 React 的簡單和方便,使用 React 的時光一直都很快樂。我喜歡這個時代,有太多的好工具幫助我們更快更好地開發應用。
近三個月我在用 Vue 構建 Web 應用,在此我想分享一些我作為一名 React 擁護者的 Vue 使用經驗。我不想寫成一篇 Vue/React 比較的文章,這種文章太多了,包括官方的 Vue 文件(https://vuejs.org/v2/guide/comparison.html)。它只是一些關於切換庫的個人觀點。
如果你使用過 Vue 和 React,或者像我一樣剛剛從 React 切換到 Vue 正在適應,或者只是想多一些瞭解,我希望這篇文章能對你有幫助。
React 和 Angular 相似的地方
相比 React,Vue 有時更多地被拿來和 Angular 比較。實際上,瀏覽 Vue 模板時我們首先看到的就是雙向繫結和 directive,與 Angular 非常類似:
1 2 3 4 5 6 7 8 9 10 11 |
<div id="app-3"> <span v-if="seen">Now you see me</span> <ol> <todo-item v-for="item in groceryList" v-bind:todo="item" v-bind:key="item.id"> </todo-item> </ol> <button v-on:click="reverseMessage">Reverse Message</button> </div> |
儘管 Vue 支援 JSX,但通常的方法還是將模板和 JavaScript 分開。雖然React JSX 的語法很像原生語法,並且反映了通常的 JavaScript 語法,但 Vue 的模板語法非常高階,它包含 directive、快捷方式和條件渲染,使得 Vue 更像 Angular。不過,相似性也就到此為止了。
當然,前後端使用同一種模板可能會有很大好處(比如 node.js/Pug + Vue/Pug),而且儘管 Vue 提供的眾多 directive 可能很有用,但對於我來說,從 React 的 JSX 切換到 Vue 的模板依然很痛苦。
Redux vs. Vuex
在應用中,React 通常會與某種資料流庫結合使用,最流行的就是 Redux。Vue 也有個類似的資料流庫,叫做 Vuex,我很高興地發現它和 Redux 非常相似。實際上,從 Redux 切換到 Vuex 沒有任何痛苦,因為與 React 跟 Vue 相比,這兩個庫有更多的共同點。
主要的區別就是 Redux 嚴重依賴於狀態的不可修改性。原因就是 Redux 從 React 的思想而來(https://redux.js.org/faq/immutable-data#why-is-immutability-required-by-redux),而且儘管 React 本身能處理可改變的資料,但在 React 中的推薦做法是不要修改 props 或 state 的資料(https://reactjs.org/docs/optimizing-performance.html#the-power-of-not-mutating-data),以便 React 獲得最好的效率。
在 React 中,元件 state 的變化會觸發該元件以下的整個元件子樹的重新渲染。為了避免不必要的子元件重新渲染, 我們需要使用 PureComponent,或儘量實現 shouldComponentUpdate。還需要使用不可變的資料結構讓 state 的變化更容易被優化。(https://vuejs.org/v2/guide/comparison.html#Optimization-Efforts)
然而 Vuex 完全不關心 state 是否不可修改。
在 Vue 中,元件的依賴會自動在渲染過程中跟蹤,因此當 state 發生變化時,系統可以精確地知道哪個元件需要渲染。(https://vuejs.org/v2/guide/comparison.html#Optimization-Efforts)
因此,React/Vue 與元件互動的方式有一些區別,下面我來介紹下這些區別。
Dispatch 和 Commit
Redux 中的資料流十分嚴格且直接。元件會 dispatch action,而 action 由 action 的建立器函式返回。然後 reducer 會根據收到的 action 返回新的 state。最後,元件會通過 store 監聽 state 的變化,並在 connect() 函式的幫助下訪問state中的屬性。
每個 action 都會通過 action 建立器。儘管理論上來說可以直接從元件中 dispatch 一個 action,但通常不這樣做。action 的語法本身就鼓勵我們將 action 的邏輯封裝在 action 建立器函式中,即使是最簡單的 action:
1 2 3 4 5 6 7 |
import { ADD_TODO, REMOVE_TODO } from '../actionTypes' function addTodo(text) { return { type: ADD_TODO, text } } |
儘管 Vuex 的資料流很相似,但它並不嚴格要求元件與 state 互動的方式。首先,元件可以 dispatch action。這通常是一些非同步動作,比如從後臺獲取資料等。之後,action 會 commit 一個 mutation。mutation 函式與 reducer 的相似之處就是,它是唯一能夠改變 state 的東西。但還有另一種方法:元件可以直接 commit 一個 mutation,有時候跳過 action 直接修改資料是很方便的 。
從元件 commit mutation 的行為不僅沒有被嚴格禁止,Vuex 的文件甚至鼓勵在非同步的情況下直接使用 action(https://vuex.vuejs.org/en/mutations.html#committing-mutations-in-components)。由於我習慣了 React 的更嚴格的資料流,我更主張嚴格分離的概念——不管什麼情況下,即使是同步或者非常簡單的情況,commit mutation 也應該只能由 action 實施。
如果元件只能通過 action 來建立 mutation,那麼元件和 mutation 之間就會有一個額外的層,保證元件和 mutation 之間的低耦合,最終使得程式碼更容易維護和修改。
從 store 中獲取資料
為了與 React 元件內部的 store 互動,我們需要使用 connect() 函式。我認為 React/Redux 最讓人不爽的一點就是,你不得不判斷哪些元件該用 connect(),哪些不該用。使用 connect() 的元件通常被稱為容器,而不使用 connect() 的一般稱為表現元件,或者“笨”元件。
但 connect() 不能用得太多,因為它的效能很差。但如果只在頂層元件使用的話,需要傳遞給下層元件的 props 就會迅速增多。這個問題曾多次被討論(如這裡https://redux.js.org/docs/faq/ReactRedux.html#react-multiple-components和這裡https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0),但實際上,即使容器元件的數量還算合理,傳遞給下層的 props 也挺讓人頭疼的。
我很意外地返現,在 Vue 中我根本不需要考慮這個問題。store 可以從任何 Vue 元件中訪問,非常簡單:
1 2 3 4 5 6 7 8 |
const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return this.$store.state.count } } } |
也就是說,從一個元件傳遞給另一個元件的 props 數量非常少,而且只需要傳遞那些沒有儲存在 store 中的資料。不過,在 Vue 中傳遞 props 的語法卻非常不方便:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<template> <div> <todo-item :todo="todo"></todo-item> </div> </template> <script> import TodoItem from './TodoItem.vue' export default { components: { TodoItem }, data () { return { todo: { text: 'Learn Vue', isComplete: false } } } } </script> |
這裡我們要給子元件(TodoItem)傳遞 props,但卻不能在子元件定義的位置傳遞,而必須在模板裡傳遞。相比之下,React 中的 props 傳遞更加自然,是在子元件渲染時完成的:
1 2 3 4 5 6 7 |
class TodoList extends React.Component { render() { <div> <TodoItem todo={this.props.todo} /> </div> } } |
更新:新的 React Context API(https://reactjs.org/docs/context.html)提供了一種在元件樹中直接訪問資料而不需要在每層手動傳遞 props。
結論
如開頭所說,本文只是一些我在從 React 遷移到 Vue 時發現的一些最重要的問題。這並不是一篇嚴謹的比較,不能作為選擇庫的依據。但如果你也像我一樣不得不從一個庫切換到另一個庫,或者只是想了解更多的關於兩個庫的資訊,這篇文章也許會有幫助。
總結一下重點:
- Vue 預設不包含 JSX,很強調指令碼和模板分離;
- Redux 和 Vuex 背後的資料流思想很相似;
- Redux 十分依賴於 state 的不可改變性,而 Vuex 不關心 state 是否不可改變;
- Vue 允許 dispatch,也允許直接從元件中 commit,但最好還是嚴格些,只允許 dispatch 會比較好;
- 任何 Vue 元件都可以直接訪問 store。