原文作者:Sunil Sandhu
译者:UC 国际研发 Jothy
在工作中应用 Vue 之后,我对它有了相当深刻的理解。 不过,俗话说「外国的月亮比较圆」,我好奇「外国的」 React 是怎么样的。
我阅读了 React 文档并观看了一些教程视频,虽然它们很棒,但我真正想知道的是 React 与 Vue 的不同之处。 这里的“不同”,并不是指它们是否具有虚拟 DOM 或者它们如何渲染页面。 我希望有人能向我解释代码并告诉我其中发生了什么! 我想找一篇能解释这些差异的文章,以便 Vue 或 React(或整个 Web 开发)的新手可以更好地理解两者之间的差异。
但我没有找到任何解决这个问题的方法。 所以我意识到我必须自己动手,来发现相似/不同之处。 在做的时候,我想我会记录整个过程,以便产出一篇关于此的文章。
我决定尝试构建一个相当标准的 To Do 应用,它允许用户添加和删除列表中的项目。 这两个应用都是使用默认的 CLI 构建的(React 的 create-react-app 和 Vue 的 vue-cli)。 BTW,CLI 代表命令行界面。
好吧,这个介绍已经超出了我的预期。 我们先快速看看两个应用的界面:
两个应用的 CSS 代码完全相同,不过它们所在的位置有差别。 考虑到这一点,我们接下来看看两个应用的文件结构:
你会发现它们的结构几乎完全相同。 唯一的区别是 React 应用有三个 CSS 文件,而 Vue 应用一个都没有。 这样做的原因是,在 create-react-app 中,React 组件会有一个附带文件来控制其样式,而 Vue CLI 采用全包方法,具体样式在实际组件文件中声明。
最终,他们都达到了同样的目的,对于不能在 React 或 Vue 中以不同的方式构建 CSS 的问题,目前还真没什么办法。这真的取决于个人偏好 - 你会听到开发者社区关于如何构建 CSS 的大量讨论。 现在,我们将遵循两个 CLI 中列出的结构。
但在我们进一步讨论之前,我们先快速看一下典型的 Vue 和 React 组件的是什么样的:
扯远了。现在让我们开始研究细节中的细节!
如何变异数据?
首先我们要搞清楚什么是“变异数据”? 听起来有点科技感是吧? 它基本上只是意味着改变我们存储的数据。 因此,如果我们想将一个人的名字取值从 John 改为 Mark,我们就是在“变异数据”。 这就是 React 和 Vue 之间的关键区别所在。 虽然 Vue 本质上创建了一个 data 对象,对象数据可以*更新,而 React 创建了一个 state 对象,要实现更新必须多做一点工作。 现在 React 有充分的理由实现额外的 legwork,我们稍微深入一下。 但首先,让我们看看 Vue 中的 data 对象和 React 中的 state 对象:
Vue data 对象在左侧。 React state 对象在右侧。
你可以看到我们已经将相同的数据传递到两者中,只是标记不同。 因此,将初始数据传递到我们的组件非常非常相似。 但正如上文提到的,我们对这些数据的修改方式在两个框架之间有所不同。
假设我们有一个name 为 'Sunil'的数据元素 。
在 Vue 中,我们通过调用 this.name 来引用它。 我们也可以调用 this.name ='John' 来更新它。 这会把我的名字改成 John。 我不确定被称为John的感觉如何,但嘿嘿,它就是发生了!
在 React 中,我们通过调用 this.state.name 来引用同一份数据。 现在关键的区别在于我们不能简单地写 this.state.name='John',因为 React 做了限制来防止这种简单、无忧无虑的 mutate。 所以在 React 中,我们会使用 this.setState({name:'John'}) 的方式来编写。
虽然这基本上与我们在 Vue 中实现的相同,但是额外的写入是因为 Vue 基本上在每次更新数据时都会合并自己的 setState 版本。 简而言之,React 需要 setState 并传入需要更新的内部数据,而 Vue 假设当你把更新的值传入 data 对象时,你是想更新它的。那么为什么 React 不愿意这样做,为什么还需要 setState 呢? 让 Revanth Kumar 来为我们解释下:
“这是因为 React 希望在状态发生变化时重新运行某些生命周期钩子,[如] componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate,render,componentDidUpdate。 当你调用 setState 函数时,它会知道状态已经改变了。 如果你直接改变状态,React 将需要做更多工作来跟踪更改以及运行生命周期钩子等等。所以为了简单起见,React 使用 setState。“
现在我们已经完成了 mutations,让我们研究下如何在我们的 To Do 应用中添加新项目,以深入了解细节。
如何创建新的 To Do 项
React:
React 是如何做的?
在 React 中,我们的输入字段有一个叫 value 的属性。 这个值通过使用几个函数自动更新,这些函数结合起来会创建一个非常类似于双向绑定的东西(如果你以前从未听说过这个,你可以看看之后的 How did Vue do that 部分)。 我们通过在 input 上添加一个 onChange 事件监听器来创建这种形式的双向绑定。 我们简单看一下 input 字段,以便你了解发生了什么:
只要 input 框的值发生更改,handleInput 函数就会运行。 它会将 input 框中的任意输入更新到 state 中的 todo。 这个函数看起来像这样:
现在,只要用户按下页面上的 + 按钮添加新 item,createNewToDoItem 函数就会运行 this.setState 并向其传递一个函数。 这个函数接收两个参数,第一个是来自状态对象的 list 数组,第二个是 todo(由 handleInput 函数更新)。 该函数返回一个新对象,该对象包含之前的整个 list,然后在其末尾添加 todo。 整个列表是通过使用扩展运算符添加的(如果你之前没有看过这个 ES6 语法,Google 一下吧)。
最后,我们将 todo 设置为空字符串,它会自动更新 input 框中的值。
Vue:
Vue 是如何做的?
在 Vue 中,我们的 input 框有一个名为 v-model 的指令。 它允许我们做一些称为双向绑定的事情。 我们先看看 input 框,然后解释下发生了什么:
V-Model 将此字段的输入绑定到名为 toDoItem 的 data 对象中的 key。 当页面加载时,toDoItem 会被置为空字符串,如 todo: ''。 如果 todo 非空,例如 todo: ‘add some text here’,我们的 input 框将加载‘add some text’。 无论如何,回到将其作为空字符串,我们在输入字段中输入的任何文本都将绑定到 todo 的值。 这正是双向绑定(input 可以更新 data 对象,data 对象可以更新 input)。
回到前面的 createNewToDoItem() 代码块,我们看到我们将 todo 的内容 push 到 list 数组中,然后将 todo 更新为空字符串。
如何删除列表数据?
React:
React 是如何做的?
虽然 deleteItem 函数位于 ToDo.js 内部,但我可以直接在 ToDoItem.js 中引用它,首先将 deleteItem() 函数作为 上的 prop 传递:
首先将该功能传递给子组件,使其可以被访问。 你可以看到我们也绑定了 this 以及传递了 key 参数,因为 key 是函数将用于区分 ToDoItem 在单击时尝试删除的内容。 然后,在 ToDoItem 组件内部,我们执行以下操作:
我需要做的就是引用一个位于父组件内部的函数来引用 this.props.deleteItem。
Vue:
Vue 是如何做的?
Vue 的方法稍有不同。 我们基本上要做三件事:
首先,在我们想要调用函数的元素上:
然后我们必须创建一个 emit 函数作为子组件内部的方法(在本例中为 ToDoItem.vue),如下所示:
除此之外,你会发现我们在 ToDo.vue 中引入 ToDoItem.vue 时实际引用了一个函数:
这就是所谓的自定义事件监听器。 它会监听任何使用 'delete' 字符串触发 emit 的操作。 如果监听到,它会触发一个名为 onDeleteItem 的函数。 此函数位于 ToDo.vue 内部,而不是 ToDoItem.vue。 如前所述,此函数只是过滤 data 对象内的 todo 数组,以删除被点击的 item。
这里也值得注意的是,在 Vue 示例中,我可以简单地在 @click 监听器中编写 $emit 部分,如下所示:
这会将步数从 3 减少到 2,这仅仅取决于个人偏好。
简而言之,React 中的子组件可以通过 this.props 访问父组件方法(假设你正在传递 props,这是相当标准的做法,你会在其他 React 示例中遇到许多次),而在 Vue 中, 你必须从子组件中触发由父组件所有的事件。
如何传递事件监听器?
React:
简单事件(如点击事件)的事件监听器是直截了当的。 以下是我们如何为创建新 ToDo item 的按钮创建 click 事件的示例:
这里非常简单,就像我们使用原生 JS 处理内联 onClick 一样。 正如 Vue 部分所述,在按下回车按钮的情况下设置事件监听器就需要花点时间了。 我们可以在 input 标签中处理一下 onKeyPress 事件,如下:
只要它识别出按下了 '回车(Enter)' 键,这个函数就会触发 createNewToDoItem 函数,如下所示:
Vue:
在 Vue,它是超级直接的。 我们只使用 @ 符号,然后使用我们想要事件监听器的类型。 例如,要添加 click 事件侦听器,我们可以这样:
注意:@click 实际上是 v-on: click 的简写。 Vue 事件监听器的一个很酷的事情是,在它之后我们可以链式调用许多方法,例如 .once,它可以防止事件监听器被多次触发。 在编写用于处理键击的特定事件侦听器时,还有一些快捷方式。 我发现在按下回车按钮的情况下,在 React 中创建一个事件监听器需要花费很长时间来创建新的 ToDo item。而在Vue,我能够简单地这样写:
如何向子组件传递数据?
React:
在 react 中,我们将 props 传递到子组件。 如:
在这里,我们看到两个传递给 ToDoItem 组件的 props。 从现在开始,我们就可以通过 this.props 在子组件中引用它们了。 因此,要访问 item.todo prop,我们只需调用 this.props.item。
Vue:
在 Vue 中,我们将 props 传递到子组件。 如:
完成后,我们将它们传递给子组件中的 props 数组,如 props: ['todo']。 然后可以通过他们的名字在子组件中引用它们 - 所以在我们的例子中,'todo'。
如何将数据发送回父组件?
React:
我们首先将函数传递给子组件,方法是在我们调用子组件的位置将其引用为 prop。 然后,我们通过引用 this.props.whateverTheFunctionIsCalled,通过任何方式(例如 onClick)添加对子函数的调用。 然后,这将触发位于父组件中的函数。 我们可以在“如何从列表中删除”一节中看到整个过程的示例。
Vue:
在我们的子组件中,我们只需编写一个函数,将一个值发送回父函数。 在我们的父组件中,我们编写一个函数来侦听何时 emit 该值,然后可以触发函数调用。 我们可以在“如何从列表中删除”一节中看到整个过程的示例。
大功告成!
我们研究了如何添加,删除和更改数据,将数据以表单 props 形式从父组件传递到子组件,以及以事件侦听器的形式将数据从子组件发送到父组件。 当然,在 React 和 Vue 之间存在许多其他的小差别,但希望本文的内容能作为理解两个框架如何处理东西的基础。
Github links to both apps:
Vue ToDo: https://github.com/sunil-sandhu/vue-todo
React ToDo: https://github.com/sunil-sandhu/react-todo