作者|Sunil Sandhu
譯者|王強
編輯|王文婧
可惜我找不到現成的答案。因此我意識到我得自己動手解決這個問題才能看清楚兩者之間的異同。於是我記錄下了整個對比過程,終於完成了這樣一篇文章,填補了這方面的空白。
哪個更好看?
我決定嘗試構建一款比較典型的 To Do 類應用,允許使用者在列表中新增和刪除專案。兩款應用都是使用預設 CLI 構建的(React 用 create-react-app,Vue 則是 vue-cli)。順帶一提,CLI 的意思是命令列介面。
好了,開頭的部分比我想象的還要長,我們可以切入正題了。先來大致看一下兩款應用的外觀:
React vs Vue。不可移動的物件遇到了不可抗拒的力量!
兩個應用程式的 CSS 程式碼完全相同,但程式碼所處的位置有所不同。記住這一點,接下來讓我們看一下兩個應用程式的檔案結構:
你會發現它們的結構也幾乎相同。唯一的區別是 React 應用有三個 CSS 檔案,而 Vue 應用沒有任何 CSS 檔案。這是因為在 create-react-app 中,每個 React 元件都會附帶一個檔案來儲存其樣式,而 Vue CLI 採用了一種 包含式 的方法,具體來說是在元件檔案中宣告樣式。
最後他們倆都達成了同樣的目標,也沒什麼可多說的,因為在 React 或 Vue 中你都不能改變 CSS 的結構。這確實取決於個人喜好。開發人員社群關於 CSS 的結構化方式這個話題有大量的討論,尤其是 React 這塊,因為有許多 CSS-in-JS 解決方案,諸如樣式元件和 emotion 等。順便說一句,CSS-in-JS 就是字面上的意思。雖然這些都很有用,但這裡我們只用兩邊的 CLI 給出的結構。
在進一步深入之前,我們先來看一下典型的 Vue 和 React 元件長什麼樣:
左邊是 React,右邊是 Vue。
看過之後我們來深入了解細節吧!
我們如何突變資料?
首先,“突變資料”到底是什麼意思呢?聽起來是不是有點高深?其實它基本上就是指更改我們已儲存的資料。如果我們想將一個人名的值從 John 更改為 Mark,我們就是在“突變“這份資料。這就是 React 和 Vue 之間的關鍵區別所在。Vue 本質上建立了一個數據物件,可以在其中自由更新資料,而 React 通過所謂的狀態 hook 來處理資料突變。
從下面的圖片中可以看到兩者的設定,然後我們會具體說明:
左邊是 React,右邊是 Vue。
於是你看到我們將相同的資料傳遞給了兩者,但是各自的結構有所不同。
在 Vue 中,通常會將元件的所有可變資料放置在 data() 函式內,該函式返回一個物件,其中包含你的資料。
在 React 中,至少從 2019 年開始,我們一般會通過一系列 Hooks 處理狀態。你可能以前沒接觸過這種概念,一開始它看起來可能有點奇怪。它的工作機制基本上是這個樣子:假設我們要建立一個待辦事項列表,我們可能需要建立一個名為 list 的變數,它可能需要一個由字串或物件組成的陣列(比如說給每個 todo 字串一個 ID 或其他一些東西)。我們需要寫的程式碼是 const [list, setList] = useState([])。這裡我們用的就是 React 裡面的 Hook,稱為 useState。它本質上是讓我們能夠在元件中保留區域性狀態。
另外,你可能已經注意到我們在 useState() 內部傳入了一個空陣列 []。放在其中的是我們希望 list 最初設定的內容,這裡我們希望是一個空陣列。但從上圖可以看到,我們在陣列內傳入了一些資料,這些資料最後成了 list 的初始化資料。想知道 setList 是做什麼的?稍後會進一步說明!
如何在應用程式中引用可變資料?
假設我們有一些資料名為 name,被分配了“Sunil”值。
在 Vue 中,這部分內容位於 data() 物件內部,寫成 name: ‘Sunil'。在我們的應用程式中,我們將呼叫 this.name 來引用它。我們還可以呼叫 this.name = 'John'來更新它,把我的名字改為 John。
在 React 中,由於我們使用 useState() 建立了較小的狀態,因此很可能已經用 const [name, setName] = useState('Sunil') 建立了一些東西。在應用程式中,我們將簡單地呼叫 name 來引用同一段資料。這裡的主要區別在於我們不能簡單地寫上 name = 'John',因為 React 有一些限制來預防這種簡單且無所顧忌的突變。在 React 中,我們要寫成 setName('John')。這裡用到了 setName。在 const [name, setName] = useState('Sunil') 中,它建立兩個變數,一個變數變為 const name = 'Sunil',而第二個 const setName 被分配了一個函式,該函式使 name 可以用新值重新建立。
實際上,React 和 Vue 在這裡做的是同樣的事情,也就是建立可以更新的資料。Vue 本質上會在每次更新一條資料時預設結合它自己的 name 和 setName 版本。簡單來說,React 要求你使用內部值呼叫 setName() 來更新狀態,而如果你曾嘗試更新資料物件內部的值,Vue 就會假設你要這麼做。那麼為什麼 React 會費勁地將值與函式分開,還要使用 useState() 呢?這裡引用 Revanth Kumar 的解釋:
“這是因為當狀態改變時,React 希望重新執行某些生命週期 hooks。當你呼叫 useState 函式時,它將知道狀態已更改。如果你直接改變狀態,React 將不得不做更多的工作來跟蹤更改以及要執行的生命週期 hooks 等。因此為了簡單起見,React 使用 useState。”
Bean 最懂了。
現在我們已經搞明白了資料突變,接下來看看在兩個 To Do 應用中新增新專案的方法。
如何建立新的待辦事項?
React:
const createNewToDoItem = () => { const newId = Math.max.apply(null, list.map((t) => t.id)) + 1 const newToDo = { id: newId, text: toDo }; setList([...list, newToDo]); setToDo("");};
在 React 中,我們的輸入欄位有一個名為 value 的屬性。每次通過 onChange 事件偵聽器更改它的值時,都會自動更新此值。JSX(基本上是 HTML 的變體)如下所示:
<input type="text" value={toDo} onChange={handleInput}/>
每次更改值時,它都會更新狀態。handleInput 函式如下所示:
const handleInput = (e) => { setToDo(e.target.value);};
現在,每當使用者按下頁面上的 + 按鈕新增新專案時,都會觸發 createNewToDoItem 函式。我們再來看一下這個函式,搞清楚具體發生了什麼:
const createNewToDoItem = () => { const newId = Math.max.apply(null, list.map((t) => t.id)) + 1 const newToDo = { id: newId, text: toDo }; setList([...list, newToDo]); setToDo("");};
本質上,newId 函式是在建立一個新 ID,該 ID 將提供給我們的新 toDo 專案。newToDo 變數是一個物件,有一個 id 鍵,其值由 newID 確定。它還有一個 text 鍵,其值由 toDo 確定。這個 toDo 就是輸入值更改時要更新的那個 toDo。
setList 函式到此為止,然後我們傳入一個包含整個 list 以及新建立的 newToDo 的陣列。
你可能覺得…list 看起來很奇怪:開頭的三個點稱為散佈運算子,負責將 list 中的所有值作為單獨的專案傳遞,而不是簡單地把所有專案打包在一起作為陣列傳遞。感覺有些糊塗嗎?那我強烈建議你仔細閱讀散佈運算子的相關介紹,因為它很有用!
最後我們執行 setToDo() 並傳入一個空字串。這樣我們的輸入值為空,可以輸入新的 toDo 了。
Vue:
createNewToDoItem() { const newId = Math.max.apply(null, this.list.map(t => t.id)) + 1; this.list.push({ id: newId, text: this.todo }); this.todo = "";}
在 Vue 中,我們的 input 欄位有一個稱為 v-model 的控制代碼。這使我們能夠執行稱為 雙向繫結 的操作。下面來看一下 input 欄位,搞清楚到底發生了什麼:
<input type="text" v-model="todo"/>
V-Model 將這個欄位的輸入與我們在資料物件 toDoItem 中的鍵相關聯。頁面載入後,我們必須將 toDoItem 設定為空字串,例如:todo:''。如果其中已經有一些資料,例如 todo: ‘add some text here’,則我們的輸入欄位將載入輸入欄位內已有的 add some text here。那麼輸入欄位為空時,無論我們在輸入欄位中鍵入什麼文字都將繫結到 todo 的值。這實際上是雙向繫結(輸入欄位可以更新資料物件,反過來資料物件也可以更新輸入欄位)。
回顧一下前面的 createNewToDoItem**()程式碼塊,可以看到,我們將todo的內容推送到list陣列中,然後將todo** 更新為空字串。
我們還使用了與 React 示例中相同的 newId() 函式。
React:
const deleteItem = (item) => { setList(list.filter((todo) => todo !== item));};
因為 deleteItem() 函式位於 ToDo.js 內,我可以很容易地在 ToDoItem.js 裡引用它,首先將 deleteItem**()函式作為** 的 prop,如下所示:
<ToDoItem deleteItem={deleteItem}/>
這裡首先將該函式傳遞下去,使其能被子級訪問。然後在 ToDoItem 元件內執行以下操作:
<button className="ToDoItem-Delete" onClick={() => deleteItem(item)}> - </button>
我要引用位於父元件內的函式,只需引用 props.deleteItem。你可能發現在程式碼示例中,我們只寫了 deleteItem,而不是 props.deleteItem。這是因為我們使用了一種稱為 解構 的技術,該技術允許我們獲取 props 物件的一部分並將其分配給變數。因此在我們的ToDoItem.js 檔案中有以下內容:
const ToDoItem = (props) => { const { item, deleteItem } = props;}
這為我們建立了兩個變數,其中一個稱為 item,它被賦予與 props.item 相同的值,而 deleteItem 則根據 props.deleteItem 賦值。我們也可以簡單地使用 props.item 和 props.deleteItem 來避免解構的操作,但我認為這裡值得單獨介紹一下!
Vue:
onDeleteItem(item){ this.list = this.list.filter(todo => todo !== item);}
Vue 需要的方法稍微有一些不同。這裡我們必須做三件事:
首先,在我們要呼叫函式的元素上:
<div class=”ToDoItem-Delete” @click=”deleteItem(item)”>-</div>
然後我們必須在子元件(在本例中為 ToDoItem.vue)中建立一個 emit 函式作為方法,如下所示:
deleteItem(item) { this.$emit('delete', item)}
與此同時你會發現,當我們在 ToDo.vue 中新增 ToDoItem.vue 時,我們實際上引用了一個 函式:
<ToDoItem v-for="todo in list" :todo="todo" @delete="onDeleteItem" // <-- this :) :key="todo.id" />
這就是所謂的自定義事件偵聽器。它會偵聽使用字串“delete”觸發 emit 的所有情況。如果聽到此訊息,它將觸發一個名為 onDeleteItem 的函式。此函式位於 ToDo.vue 內部,而不是在 ToDoItem.vue 中。如前所述,此函式僅過 濾 data 物件 內的 todo 陣列 即可刪除單擊的專案。
在這裡還需注意的是,在 Vue 示例中,我可以簡單地將 $emit 部分寫在 @click 監聽器中,如下所示:
<div class=”ToDoItem-Delete” @click=”$emit(‘delete’, item)”>-</div>
這樣就能把步驟從 3 步減少到 2 步,選哪個完全取決於個人喜好。
簡而言之,React 中的子元件可以通過 props 來訪問父函式(前提是你要向下傳遞 props,這是相當標準的做法,其他 React 工作中也非常常見);而在 Vue 中,你需要從子級發射事件,這些事件通常會在父元件內部回收。
怎樣傳遞事件偵聽器?
React:
<button className=”ToDo-Add” onClick={createNewToDoItem}>+</div>.
這裡非常簡單,和在一般的 JS 裡處理內聯 onClick 差不多。如 Vue 部分所述,設定一個事件偵聽器來偵聽按下 Enter 鍵的動作有點複雜。這需要由 input 標籤處理 onKeyPress 事件,如下:
<input type=”text” onKeyPress={handleKeyPress}/>.
只要識別出已按下“enter”鍵,此函式就觸發了 createNewToDoItem 函式,如下:
handleKeyPress = (e) => { if (e.key === ‘Enter’) { createNewToDoItem(); }};
Vue:
在 Vue 中寫起來非常直觀。我們只使用 @符號,後面是我們想要做的事件監聽器的型別。例如要新增一個 click 事件監聽器,我們可以編寫以下程式碼:
<button class=”ToDo-Add” @click=”createNewToDoItem()”>+</div>
注意:@click 實際上是 v-on:click 的簡寫。Vue 事件偵聽器很好用的是你還可以繫結很多東西,例如.once,它可以防止事件偵聽器被多次觸發。在編寫處理按鍵的特定事件偵聽器時還有許多捷徑。我發現在 React 中建立一個事件偵聽器,做到每當按下 enter 鍵就建立新的 ToDo 專案寫起來比較麻煩。在 Vue 中,我只需編寫:
<input type=”text” v-on:keyup.enter=”createNewToDoItem”/>
如何將資料傳遞給子元件?
React:
在 React 中,我們將 props 傳遞到子元件的建立位置。如:
<ToDoItem key={key.id} item={todo} />
這裡我們看到兩個傳遞給 ToDoItem 元件的 props。從這裡開始,我們就可以通過 this.props 在子元件中引用它們。因此要訪問 item.todo prop 時,我們只需呼叫props.item。
Vue:
在 Vue 中,我們將 props 傳遞到子元件的建立位置。如:
<ToDoItem v-for="item in list" :item="item" @delete="onDeleteItem" :key="item.id" />
完成此操作後,我們將它們傳遞到子元件的 props 陣列中,如下所示:props:['todo']。然後它們就可以在子元件中用名稱引用——這裡的名稱就是 'todo'。
如何將資料傳送回父元件?
React:
我們首先將函式向下傳遞給子元件,在呼叫子元件的位置將其作為 prop 引用。然後我們向子元件的函式新增呼叫,比如說 onClick 就引用 props.whateverTheFunctionIsCalled——或者 whateverTheFunctionIsCalled(如果用解構)。然後將觸發位於父元件中的函式。我們可以在“如何從列表中刪除專案”部分中檢視全過程。
Vue:
在子元件中,我們只需要編寫一個將值返回給父函式的函式即可。在父元件中我們編寫一個函式,該函式偵聽何時發出該值,然後可以觸發函式呼叫。可以在“如何從列表中刪除專案”部分中檢視全過程。
終於完成了
我們已經研究了如何新增、刪除和更改資料,以 props 形式將資料從父級傳遞到子級,以及以事件偵聽器的形式將資料從子級傳送到父級。當然,React 和 Vue 之間還有許多其他的小差異和特殊要素,但我希望本文的內容有助於大家理解這兩個框架是如何處理事物的。
如果你有興趣 fork 本文中使用的樣式,並想製作自己的類似作品,請自便!
兩個應用的 Github 連結:
Vue ToDo:
http://github.com/sunil-sandhu/vue-todo-2019
React ToDo:
http://github.com/sunil-sandhu/react-todo-2019
英文原文:http://medium.com/javascript-in-plain-english/i-created-the-exact-same-app-in-react-and-vue-here-are-the-differences-2019-edition-42ba2cab9e56