前幾天,我們曾和大家探討了React、Angular、Vue.js三大框架的優缺點。在留言中,許多朋友提到Vue在國內的市場應用廣泛,最近Vue 3.0 Pre-Alpha的發佈也讓它成爲前端圈關注的焦點。而React則易於學習,有網友評論,不關注框架本身提供的API,更容易讓開發者形成靠譜的代碼風格。理論上的梳理或許還不夠直觀,今天的文章作者就進行了這樣一場實驗,他也在好奇,如果用React和Vue構建同樣一款應用,開發過程與效果會是怎樣的?
我在工作中使用Vue有一段時間了,對它的瞭解也相當深刻,但我很想知道圍牆另一邊的草是什麼樣的——這裏所說的草指的就是React。
我閱讀了很多React文檔並觀看了一些教學視頻,它們的確很棒,但我真正想知道的是React與Vue有何不同。我所說的“不同”並不是它們是否都有虛擬DOM,或者它們是怎樣渲染頁面之類的問題。我希望的是有人從代碼層面給出解釋。我希望能有一篇文章涵蓋這樣的內容,幫助那些對Vue或React(乃至整個Web開發工作)都不熟悉的人更好地理解兩者之間的區別。
可惜我找不到現成的答案。因此我意識到我得自己動手解決這個問題才能看清楚兩者之間的異同。於是我記錄下了整個對比過程,終於完成了這樣一篇文章,填補了這方面的空白。
我決定嘗試構建一款比較典型的To Do類應用,允許用戶在列表中添加和刪除項目。兩款應用都是使用默認CLI構建的(React用create-react-app,Vue則是vue-cli)。順帶一提,CLI的意思是命令行界面。🤓
好了,開頭的部分比我想象的還要長,我們可以切入正題了。先來大致看一下兩款應用的外觀:
兩個應用程序的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組件長什麼樣:
看過之後我們來深入瞭解細節吧!
我們如何突變數據?
首先,“突變數據”到底是什麼意思呢?聽起來是不是有點高深?其實它基本上就是指更改我們已存儲的數據。如果我們想將一個人名的值從John更改爲Mark,我們就是在“突變“這份數據。這就是React和Vue之間的關鍵區別所在。Vue本質上創建了一個數據對象,可以在其中自由更新數據,而React通過所謂的狀態hook來處理數據突變。
從下面的圖片中可以看到兩者的設置,然後我們會具體說明:
於是你看到我們將相同的數據傳遞給了兩者,但是各自的結構有所不同。
在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。”
現在我們已經搞明白了數據突變,接下來看看在兩個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**()函數作爲
<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:
針對簡單事件(例如單擊事件)的事件偵聽器很好做。下面是爲創建新的ToDo項目的按鈕創建click事件的示例:
<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:https://github.com/sunil-sandhu/vue-todo-2019
React ToDo:https://github.com/sunil-sandhu/react-todo-2019