免费活动:声网/伴鱼/新东方技术大咖邀您参加实时音视频技术专场 了解详情
写点什么

我用 React 和 Vue 构建了同款应用,来看看哪里不一样(2020 版)

2020 年 9 月 03 日

我用React和Vue构建了同款应用,来看看哪里不一样(2020版)

几年前,我决定试着分别在 React 和 Vue 中构建一个相当标准的 To Do(待办事项)应用。这两个应用都是使用默认的 CLI 构建的(React 的 create-react-app 和 Vue 的 vue-cli)。我想尽量保持中立,通过这样的例子来告诉大家这两种技术执行特定任务时是怎样做的。


当 React Hooks 发布时,我为这篇文章更新了“2019 版”,用函数式 Hooks 取代了类组件。随着 Vue 3 及其组合(Composition)API 的发布,现在是时候更新这篇文章的“2020 版”了。


先来大致看一下两款应用的外观:


两款应用的 CSS 代码完全相同,但代码所处的位置有所不同。记住这一点,接下来让我们看一下它们的文件结构:



你会发现它们的结构也几乎相同。唯一的区别是 React 应用有两个 CSS 文件,而 Vue 应用没有任何 CSS 文件。这是因为在 create-react-app 中,默认每个 React 组件都会附带一个单独文件来保存其样式,而 Vue CLI 用单一的文件来为默认组件包含 HTML、CSS 和 JavaScript。


最后它们俩都达成了同样的目标,也没什么可多说的,因为在 React 或 Vue 中你都不能改变文件结构。选择哪个确实取决于个人喜好。开发社区关于 CSS 的结构化方式这个话题有大量的讨论,尤其是 React 这块,因为有许多 CSS-in-JS 解决方案,诸如样式化组件和 emotion 等。顺便说一句,CSS-in-JS 就是字面上的意思。虽然这些都很有用,但这里我们只用两边的 CLI 给出的结构。


在进一步深入之前,我们先来看一下典型的 Vue 和 React 组件长什么样:


典型的 React 文件



典型的 Vue 文件



看过之后我们来深入了解细节吧!


我们如何突变数据?

首先,“突变数据”到底是什么意思呢?听起来是不是有点高深?其实它基本上就是指更改我们已存储的数据。如果我们想将一个人名的值从 John 更改为 Mark,我们就是在“突变“这份数据。这就是 React 和 Vue 之间的关键区别所在。Vue 本质上创建了一个数据对象,可以在其中自由更新数据,而 React 通过所谓的状态 Hook 来处理数据突变。


从下面的图片中可以看到两者的设置,然后我们会具体说明:


React 状态



Vue 状态



于是你看到我们将相同的数据传递给了两者,但各自的结构有所不同。


在 React 中,至少从 2019 年开始,我们一般会通过一系列 Hooks 处理状态。你可能以前没接触过这种概念,一开始它看起来可能有点奇怪。它的工作机制基本上是这个样子:


假设我们要创建一个待办事项列表,我们可能需要创建一个名为 list 的变量,它可能需要接收一个由字符串或对象组成的数组(比如说给每个 todo 字符串一个 ID 或其他一些东西)。我们需要写的代码是const [list, setList] = useState([])。这里我们用的就是 React 里面的 Hook,称为 useState。它本质上是让我们能够在组件中保留局部状态。


另外,你可能已经注意到我们在 useState() 内部传入了一个空数组 []。放在其中的是我们希望 list 最初设置的内容,这里我们希望是一个空数组。但从上图可以看到,我们在数组内传入了一些数据,这些数据最后成了 list 的初始化数据。想知道 setList 是做什么的?稍后会进一步说明!


在 Vue 中,通常会将组件的所有突变数据放置在一个 setup() 函数内,该函数返回一个对象,其中包含要公开的数据和函数(就是那些你要在应用中使用的东西)。你会注意到,应用中的每个状态数据(也就是我们希望能够突变的数据)都包装在一个 ref() 函数内部。这个 ref() 函数是我们从 Vue 导入的,可让我们的应用在这些数据更改 / 更新时完成更新。简而言之,如果你想在 Vue 中创建突变数据,请为 ref() 函数分配一个变量,并在其中放入默认数据。


如何在应用中引用突变数据?

假设我们有一些数据名为 name,被分配了 Sunil 值。


在 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 可以用新值重新创建。


在 Vue 中,它位于 setup() 函数内部,并且被称为const name = ref('Sunil')。在应用中,我们将调用 name.value 来引用它。如果要使用在 ref() 函数内部创建的值,我们将在变量上寻找.value 而不是简单地调用该变量。换句话说,如果我们想要一个持有状态的变量值,我们将寻找 name.value 而不是 name。如果要更新 name 的值,可以通过更新 name.value 来完成。例如,假设我想将我的名字从 Sunil 更改为 John, 可以写name.value = "John"来做到这一点。


实际上,React 和 Vue 在这里做的是同样的事情,也就是创建可以更新的数据。Vue 本质上会在每次更新一条包装在 ref() 函数内的数据时默认结合它自己的 name 和 setName 版本。React 要求你使用内部值调用 setName() 来更新状态,而如果你曾尝试更新数据对象内部的值,Vue 就会假设你要这么做。那么为什么 React 会费劲地将值与函数分开,还要使用 useState() 呢?这是因为当状态改变时,React 希望重新运行某些生命周期 Hooks。在我们的例子中,当你调用 setName() 时,React 会知道有些状态已更改,所以可以运行它们的生命周期 Hooks。如果你直接改变状态,React 将不得不做更多的工作来跟踪更改以及要运行的生命周期 Hooks 等。


现在我们已经搞明白了数据突变,接下来看看在两个 To Do 应用中添加新项目的方法。


我们如何创建新的待办事项?

React:

const createNewToDoItem = () => {    const newId = generateId();    const newToDo = { id: newId, text: toDo };    setList([...list, newToDo]);    setToDo("");};
复制代码


在 React 里是怎么做的?


在 React 中,我们的输入字段有一个名为 value 的属性。每次通过 onChange 事件侦听器 更改它的值时,都会自动更新此值。JSX(基本上是 HTML 的变体)如下所示:


<input    type="text"    placeholder="I need to..."    value={toDo}    onChange={handleInput}    onKeyPress={handleKeyPress}/>
复制代码


每次更改值时,它都会更新状态。handleInput 函数如下所示:


const handleInput = (e) => {    setToDo(e.target.value);};
复制代码


现在,每当用户按下页面上的 + 按钮添加新项目时,都会触发 createNewToDoItem 函数。我们再来看一下这个函数,搞清楚具体发生了什么:


const createNewToDoItem = () => {    const newId = generateId();    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 看起来很奇怪:开头的三个点称为 spread 运算符,负责将 list 中的所有值作为单独的项目传递,而不是简单地把所有项目打包在一起作为数组传递。感觉有些糊涂吗?那我强烈建议你仔细阅读 spread 运算符的相关介绍,因为它很有用!


最后我们运行 setToDo() 并传入一个空字符串。这样我们的输入值为空,可以输入新的 toDo 了。


Vue:

function createNewToDoItem() {    const newId = generateId();    list.value.push({ id: newId, text: todo.value });    todo.value = "";}
复制代码


在 Vue 里是怎么做的?


在 Vue 中,我们的 input 字段有一个称为 v-model 的句柄。这使我们能够执行称为 双向绑定 的操作。下面来看一下 input 字段,搞清楚到底发生了什么:


<input    type="text"    placeholder="I need to..."    v-model="todo"    v-on:keyup.enter="createNewToDoItem"/>
复制代码


V-Model 将这个字段的输入与我们在 setup() 函数上创建的一个变量相关联,然后公开为一个返回对象内的键。到目前为止我们还没有介绍对象返回的内容,所以先说一下,这是我们从 ToDo.vue 内部的 setup() 函数返回的内容:


return {    list,    todo,    showError,    generateId,    createNewToDoItem,    onDeleteItem,    displayError};
复制代码


这里,list、todo 和 showError 是我们的有状态值,而其他所有内容都是我们希望能在应用其他位置调用的函数。在页面加载时,我们必须将 todo 设置为一个空字符串,例如:const todo = ref("")。如果其中已经有一些数据,例如 const todo = ref(“add some text here”):我们的输入字段将在内部已有 add some text here 的情况下加载。不管怎样,回到空字符串的状态,无论我们在输入字段中键入什么文本都必须绑定到 todo.value。这实际上就是双向绑定——输入字段可以更新 ref() 值,反过来后者也可以更新输入字段。


回顾一下前面的 createNewToDoItem () 代码块,可以看到,我们将 todo.value 的内容推送到 list 数组中,然后将前者更新为一个空字符串。


我们还使用了与 React 示例中相同的 newId() 函数。


如何从列表中删除项目?

React:

const deleteItem = (id) => {    setList(list.filter((item) => item.id !== id));};
复制代码


在 React 里是怎么做的?


因为 deleteItem() 函数位于 ToDo.js 内,我可以很容易地在 ToDoItem.js 里引用它,首先将 deleteItem () 函数作为一个 prop,如下所示:


<ToDoItem key={item.id} item={item} deleteItem={deleteItem} />
复制代码


这里首先将该函数传递下去,使其能被子级访问。然后在 ToDoItem 组件内执行以下操作:


<button className="ToDoItem-Delete" onClick={() => deleteItem(item.id)}>    -</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:

function onDeleteItem(id) {    list.value = list.value.filter(item => item.id !== id);}
复制代码


在 Vue 里是怎么做的?


Vue 需要的方法稍微有一些不同。这里我们必须做三件事:


首先,在我们要调用函数元素上:


<button class="ToDoItem-Delete" @click="deleteItem(item.id)">    -</button>
复制代码


然后我们必须在子组件(在本例中为 ToDoItem.vue)中创建一个 emit 函数作为方法,如下所示:


function deleteItem(id) {    emit("delete", id);}
复制代码


与此同时你会发现,当我们在 ToDo.vue 中添加 ToDoItem.vue 时,我们实际上引用了一个 函数


<ToDoItem v-for="item in list" :item="item" @delete="onDeleteItem" :key="item.id" />
复制代码


这就是所谓的自定义事件侦听器 event-listener。它会侦听使用字符串“delete”触发 emit 的所有情况。如果听到此消息,它将触发一个名为 onDeleteItem 的函数。此函数位于 ToDo.vue 内部,而不是在 ToDoItem.vue 中。如前所述,此函数仅过滤来自 list.value 数组内的 id。


在这里还需注意的是,在 Vue 示例中,我可以简单地将 $emit 部分写在 @click 侦听器中,如下所示:


<buttonclass="ToDoItem-Delete"@click="emit("delete", item.id)">    -</button>
复制代码


这样就能把步骤从 3 步减少到 2 步,选哪个完全取决于个人喜好。简而言之,React 中的子组件可以通过 props 来访问父函数(前提是你要向下传递 props,这是相当标准的做法,其他 React 工作中也非常常见);而在 Vue 中,你需要从子级发射事件,这些事件通常会在父组件内部回收。


怎样传递事件侦听器?

React:

针对简单事件(例如单击事件)的事件侦听器很好做。下面是为创建新的 ToDo 项目的按钮创建 click 事件的示例:


<button className="ToDo-Add"onClick={createNewToDoItem}>    +</button>
复制代码


这里非常简单,和在一般的 JS 里处理内联 onClick 差不多。如 Vue 部分所述,设置一个事件侦听器来侦听按下 Enter 键的动作有点复杂。这需要由 input 标签处理 onKeyPress 事件,如下:


<input   type="text"    placeholder="I need to..."    value={toDo}    onChange={handleInput}   onKeyPress={handleKeyPress}/>
复制代码


只要识别出已按下“enter”键,此函数就触发了 createNewToDoItem 函数,如下:


const handleKeyPress = (e) => {    if (e.key === "Enter") {    createNewToDoItem();    }};
复制代码


Vue:

在 Vue 中写起来非常直观。我们只需使用 @符号,后面是我们想要做的事件监听器的类型。例如要添加一个 click 事件监听器,我们可以编写以下代码:


<button class="ToDo-Add" @click="createNewToDoItem">    +</button>
复制代码


注意:@click 实际上是 v-on:click 的简写。Vue 事件侦听器很好用的是你还可以绑定很多东西,例如.once,它可以防止事件侦听器被多次触发。在编写处理按键的特定事件侦听器时还有许多捷径。我发现在 React 中创建一个事件侦听器,做到每当按下 enter 键就创建新的 ToDo 项目,写起来比较麻烦。在 Vue 中,我只需编写:


<input type=”text” v-on:keyup.enter=”createNewToDoItem”/>
复制代码


如何将数据传递给子组件?

React:

在 React 中,我们将 props 传递到子组件的创建位置。如:


<ToDoItem key={item.id} item={item} deleteItem={deleteItem} />;
复制代码


这里我们看到两个传递给 ToDoItem 组件的 props。从这里开始,我们就可以通过 this.props 在子组件中引用它们。因此要访问 item.todo prop 时,我们只需调用 props.item。你可能已经注意到还有一个 key prop(因此从技术上讲,我们实际上正在传递三个 props)。这主要用于 React 的内部,因为它简化了同一组件的多个版本之间更新和跟踪更改的工作(我们这里每个 todo 是 ToDoItem 组件的一个副本)。确保你的组件具有唯一键也很重要,否则 React 会在控制台中发出警告。


Vue:

在 Vue 中,我们将 props 传递到子组件的创建位置。如:


<ToDoItem v-for="item in list" :item="item" @delete="onDeleteItem" :key="item.id" />
复制代码


完成此操作后,我们将它们传递到子组件的 props 数组中,如下所示:props: [ "todo" ]。然后它们就可以在子组件中用名称引用——这里的名称就是 todo。如果你不知道在哪里放 prop 键,下面是我们的子组件中整个 export default 对象的样子:


export default {    name: "ToDoItem",    props: ["item"],    setup(props, { emit }) {        function deleteItem(id) {        emit("delete", id);        }        return {        deleteItem,        };    },};
复制代码


你可能注意到 Vue 中遍历数据时,我们实际上遍历的是 list 而非 list.value。遍历后者这里是行不通的。


如何将数据发射回父组件?

React:

我们首先将函数向下传递给子组件,在调用子组件的位置将其作为 prop 引用。然后我们向子组件的函数添加调用,比如说 onClick 就引用 props.whateverTheFunctionIsCalled——或者 whateverTheFunctionIsCalled(如果用解构)。然后将触发位于父组件中的函数。我们可以在“如何从列表中删除项目”部分中查看全过程。


Vue:

在子组件中,我们只需要编写一个将值返回给父函数的函数即可。在父组件中我们编写一个函数,该函数侦听何时发射出该值,然后可以触发一个函数调用。可以在“如何从列表中删除项目”部分中查看全过程。


终于完成了!

我们已经研究了如何添加、删除和更改数据,以 props 形式将数据从父级传递到子级,以及以事件侦听器的形式将数据从子级发送到父级。当然,React 和 Vue 之间还有其他许多小差异和癖好,但我希望本文的内容有助于大家理解这两个框架是如何处理事物的。


如果你有兴趣 fork 本文中使用的样式,并想制作自己的类似作品,请自便!


  • 两个应用的 Github 链接 Vue ToDo:


https://github.com/sunil-sandhu/vue-todo-2020


  • React ToDo:


https://github.com/sunil-sandhu/react-todo-2020


英文原文

I created the exact same App in React and Vue


2020 年 9 月 03 日 11:154851
用户头像
小智 前 InfoQ 主编

发布了 400 篇内容, 共 318.5 次阅读, 收获喜欢 1762 次。

关注

评论 1 条评论

发布
用户头像
不错,升级为3.0时,可以照着作者的代码手敲一遍;
2020 年 09 月 04 日 10:54
回复
没有更多了
发现更多内容

工作 vs 生活

shengjk1

你会写测试用例吗

鱼贩

如何使用 Apache CXF 快速实现一个 WebService

Rayjun

Java WebService CXF

后疫情时代,区块链的发展迎来曙光!

CECBC区块链专委会

CECBC 区块链技术

【openlayers】在vue中使用ol

学习委员

Java html Vue 地图 openlayers

ARTS week 2

刘昱

愚蠢写作术(1):怎么让你的标题被读者忽视

史方远

个人成长 写作

区块链技术大显身手,仅用20分钟就打完一场官司!

CECBC区块链专委会

CECBC 区块链技术 数字版权 存证

Java日志门面系统

泛泛之辈

Java 日志 slf4j

Kafka系列9:面试题是否有必要深入了解其背后的原理?我觉得应该刨根究底(上)

z小赵

大数据 kafka 实时计算

MAC OS 下 HomeBrew 使用

耳东

macos brew homebrew

[ARTS打卡] week 01

Mau

ARTS 打卡计划

不吹不黑!GitHub 上帮助人们学习编码的 12 个资源,错过血亏...

JackTian

GitHub 学习 程序员 编码 开源项目

1 ARTS 2020-05-31

3.141516

LeetCode

ARTS(2020-05-25/2020-05-31)

天行者

正确阅读

John 易筋 ARTS打卡Week 02

John(易筋)

ARTS 打卡计划 ARTS活动 arts

游戏夜读 | 关于构图的困难

game1night

时代在变,产品运营能力很重要

夜来妖

程序员人生 程序人生 产品经理 产品推荐 程序媛

Element-UI实战系列:Table+Pagination组件实现已选和全选功能

brave heart

Vue 前端 Element

如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。

why技术

Java 源码分析 面试题 线程池

写博客的那些事

shengjk1

【ARTS打卡】Week01

Rex

学习

ARTS week2

紫枫

ARTS 打卡计划

ARTS 打卡 WEEK2

编程之心

ARTS 打卡计划

clang-format 使用与集成介绍

Geek_101627

ARTS打卡计划_第一周

叫不醒装睡的人

ARTS 打卡计划

ARTS打卡Week 02

teoking

objective-c LeetCode WebRTC

如何用CSS选择符(数字开头) 杀死队友

学习委员

Java html css3 前端 Web

ARTS Week1

姜海天

Apache DolphinScheduler新特性与Roadmap路线

海豚调度

数据中台 大数据任务调度 工作流调度 海豚调度 数据湖调度

2万字长文带你细细盘点五种负载均衡策略。

why技术

Java 负载均衡 源码分析 dubbo java面试

边缘计算隔离技术的挑战与实践

边缘计算隔离技术的挑战与实践

我用React和Vue构建了同款应用,来看看哪里不一样(2020版)-InfoQ