GMTC全球大前端技术大会(北京站)门票9折特惠截至本周五,点击立减¥480 了解详情
写点什么

我用 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:154890
用户头像
小智 前 InfoQ 主编

发布了 402 篇内容, 共 321.6 次阅读, 收获喜欢 1773 次。

关注

评论 1 条评论

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

突破存储瓶颈,打通高性能计算的“最后一公里“

浪潮云

高性能 存储

架构师week9 总结

Geek_xq

Soul网关源码阅读(九)插件配置加载初探

Java 源码阅读 网关

19年末我从外包辞职了,10000+小时后,走进字节跳动拿了offer

Java架构追梦

Java 架构 字节跳动 面试题 面试经历

数据库周刊56丨17家数据库厂商2020大事件盘点;第十届PG中国技术大会圆满举办;pg wal目录膨胀异常分析;Oracle RAC等待事件总结;云和恩墨技术通讯2020年刊特辑……

墨天轮

MySQL 数据库 oracle postgre

存币生息钱包APP系统开发|存币生息钱包软件开发

开發I852946OIIO

系统开发

从Gartner 最新“客户之选”报告,看国内外RPA的差异化竞争

王吉伟频道

RPA SaaS 机器人流程自动化 Gartner

突破开源Redis的内存限制,存算分离的GaussDB到底有多能“装”?

华为云开发者社区

redis 存储 华为云 GaussDB 存算分离

CWE 4.3:强化你的数据自我保护能力

华为云开发者社区

网络安全 安全 数据保护 cwe gdpr

云课堂开发实践:白板教程

拍乐云Pano

音视频 在线教育 RTC 互动白板 在线白板

17家国产数据库厂商的2020年度事件大盘点:项目签约与验收、新版本发布等

墨天轮

数据库 国产化

远程探视正在取代亲自探视

anyRTC开发者

ios android 音视频 WebRTC 直播

盘点12个Python数据可视化库,通吃任何领域

博文视点Broadview

Apay矿机系统开发

v16629866266

Docker 容器健康检查

K8sCat

Docker Dockerfile HEALTHCHECK

全网最全!彻底弄透Java处理GMT/UTC日期时间

YourBatman

GMT UTC JSR310 TimeZone ZoneId

智能社区警务管理平台开发解决方案,智慧平安小区建设

WX13823153201

智能社区警务管理平台开发

Materialize MySQL引擎:MySQL到Click House的高速公路

华为云开发者社区

MySQL 数据 Clickhouse 存储 materialize

借朋友钱后,对方不还怎么办?

石云升

28天写作 借钱

【转载】Springboot2.x的AOP默认代理方式

程序员架构进阶

Java aop SpringBoot 2 动态代理

【CSS】带边框的三角形(position)

学习委员

css3 前端 html/css CSS小技巧 28天写作

Elasticsearch document id 生成方式

escray

elastic 七日更 28天写作 死磕Elasticsearch 60天通过Elastic认证考试

Java 程序经验小结:接口优先于反射机制

后台技术汇

28天写作

架构师week9 作业

Geek_xq

六步带你完成博流wifi模组对接华为云流程

华为云开发者社区

网络 华为云 sdk 博流 wifi模组

2021年国产数据库名录和产品信息一览

墨天轮

数据库 大数据 程序员 运维 SQL优化

JUST技术:提升基于GPS轨迹的路网推测精确度

京东科技开发者

人工智能

响应号召,开始14天的居家隔离 | 视频号 28 天 (14)

赵新龙

28天写作

春节无法线下社交聚会,来线上“一起X”共享体验

ZEGO即构

创造高效能源管理方案,3D 可视化技术成为进展新思路

一只数据鲸鱼

物联网 数据可视化 3D可视化 绿色能源 智慧能源

一个不兼容的 JS 方法,让你的网站发生崩溃

老魚

JavaScript 网站 程序猿 移动端

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