写点什么

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

发布了 398 篇内容, 共 308.8 次阅读, 收获喜欢 1721 次。

关注

评论 1 条评论

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

9-2 秒杀系统的挑战和问题

burner

架构师训练营 -week09 学习总结

GunShotPanda

学习总结(训练营第九课)

看山是山

架构师训练营-week09 作业

GunShotPanda

获得高手的精英思维,从写作开始。

叶小鍵

高手 万维钢 得到精英日课

JVM系列之:从汇编角度分析NullCheck

程序那些事

Java JVM JIT

女博士年薪156万入职华为!网友:实力演绎美貌与智慧并存

程序员生活志

华为 少年天才

NIO的组成有哪些——奈学

奈学教育

nio

华为云的研究成果又双叒叕被MICCAI收录了!

华为云开发者社区

学习 AI 计算机视觉 医疗 华为云

换一种方式构建镜像

北漂码农有话说

架构师0期第九周命题作业

何伟敏

充分释放数据价值:安全、可信6到飞起

华为云开发者社区

区块链 数据共享 华为云 可信安全计算 数据价值

百度大脑人脸离线识别SDK升级盘点,Linux ARM版本上线

百度大脑

人工智能 人脸识别 百度大脑 sdk

产品经理【三句半】,说清你的【酸甜苦辣】

HPioneer

产品经理 产品设计

「查缺补漏」巩固你的Redis知识体系

Kerwin

Java redis

《RabbitMQ》如何保证消息不被重复消费

Java旅途

RabbitMQ 消息队列

中国四大银行正在大规模内测数字货币APP|可凭手机号完成转账

CECBC区块链专委会

数字货币 DCEP 中国人民银行

架构师训练营第九周

WW

Week09作业

熊威

打造高转化率网站不得不遵循的3条规范

姜奋斗

网站架构 网站 网站搭建 高转化率 转化

Week 09 学习总结

Jeremy

浙江上线市场监管区块链电子取证平台,武汉出台“区块链八条”,

CECBC区块链专委会

区块链 行业资讯 产业落地

一文教会你嵌入式网络模块的联网操作

良知犹存

物联网 网络 嵌入式

《深度工作》学习笔记(5)

石云升

读书笔记 专注 深度工作

NIO的组成有哪些——奈学

古月木易

nio

日入斗金,稳赚不赔?小心泛滥网络的兼职刷单让你钱尽财空

360安全卫士

【架构师训练营】第九期作业

云064

Week 09 命题作业

Jeremy

未来云原生世界的“领头羊”:容器批量计算项目Volcano 1.0版本发布

华为云开发者社区

Kubernetes 容器 华为云 Volcano 元原生

week9 学习总结

任小龙

如何保存我们的资产

不在调上

NLP领域的2020年大事记及2021展望

NLP领域的2020年大事记及2021展望

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