React 中只有一个语法糖,那就是 JSX,将结构与执行逻辑以及表现都融入到 JacaScript 中,这也就是为什么说 React 相比起来较为灵活的原因。这种 all in js 的方式有一定的弊端,会让 html 与 js 强耦合,导致组件内代码混乱,不利于维护。但是另一方面,这样的形式能在类型提示、自动检查,以及调试时候能精确跳转到定义,这种开发体验在可维护性上又弥补了许多。
<template>
<h1>My name is {{ name }}, I'm {{ age }} years old.</h1>
</template>
<script>
export default {
name: "Student",
props: ["name", "age"],
};
</script>
前言
之前学习或工作经历中都是 React 技术栈相关的,现来到新公司后需要使用 Vue2 相关技术栈维护项目,开发需求。大概花了一周时间左右刷了刷 Vue2 的官方文档,现在为了加强自己在 Vue2 使用上的熟练度,也为了防止因为以后 React 不太常用但是特定时刻又要切换回去的时候能快速记忆起用法,于是就有了这篇讲解 Vue2 与 React 在基础使用上的对应关系的文章。
强调一下,这篇文章不会在两个框架的原理上有过多深入(我还没读过源码 😂),仅仅是从我们常规开发中需要用到的实现上做了比对,简单来说就是,我在 React 中的实现如何用 Vue2 去实现。
设计理念
一个庞大而复杂的项目拥有分工明确的代码结构是很重要的,这对于项目维护具有非常重要的意义,所以 React 和 Vue 都推崇组件化的方式去组织我们的项目,就像一台完整的计算机一样,打散开来各个模块都可以独立设计、开发、互不耦合,最后按照大家统一的协议去设计好接口,最终才能组装成一台强大、完整的计算机。
但是在整体的写法上,两个框架的设计理念是不太一样的:
JSX
React 中只有一个语法糖,那就是 JSX,将结构与执行逻辑以及表现都融入到 JacaScript 中,这也就是为什么说 React 相比起来较为灵活的原因。这种 all in js 的方式有一定的弊端,会让 html 与 js 强耦合,导致组件内代码混乱,不利于维护。但是另一方面,这样的形式能在类型提示、自动检查,以及调试时候能精确跳转到定义,这种开发体验在可维护性上又弥补了许多。
template 模板
Vue 拥抱了比较传统且经典的思想,将 html、css、js 分离开来,这就意味着开发者在编写代码时会将结构、执行逻辑和表现分开进行,这对于项目的可维护性上有很大的提升。但是在 Vue 中我们使用
template
模板,并借助提供的v-if
、v-show
、v-for
等语法糖去编写代码时,在类型提示、定义跳转等等方面又是非常不友好的,这对于项目维护又是一个减分项。一个例子 🌰
现在我们有一个简单的场景,根据某个状态来决定渲不渲染某个“小”组件,这个状态可随按钮点击进行布尔值切换。
在 React 中这样写:
我们使用 JSX 语法
{ show && ...}
去判断后面的渲染逻辑是否执行, 并且将渲染逻辑单独抽离了出来(即renderContent
,没有直接在后面写渲染,这种抽离在我以前的开发经验中是很常见的,一是为了结构复用,比如当前文件内其他地方也用到了这种渲染结构,但是该“小组件”的体量又不足以让我去单独创建一个 jsx 文件来写成一个独立的组件;二是为了保持return
中的代码简洁。但是随着需求的持续迭代,当前这个 App 组件会变得无比臃肿,比如充斥大量类似
renderContent
这种渲染结构散落在组件内。假如你现在是一个接手该项目的人,你会发现,你根据已展现的页面结构来对应代码中的渲染结构,会非常累!!!以前维护过一个页面内写了几千行代码的 jsx 文件,各种渲染逻辑、执行逻辑大量穿插在这个页面的各处,我当时直接 emo 了。在 Vue 中使用
template
模板我们可以这样写:可以看到使用模板去写组件时,你的渲染结构全部都在
template
模板内,页面与代码结构相一致,这对于初步接手的开发者来说是很友好的。另外,如果想要在该组件内复用带v-show
这部分的渲染逻辑,将会被被强迫封装为另一个组件。在 React 中不这么做是因为实在是太自由了,大多数时候大多数人不想这么麻烦。使用
template
模板写法的弊端就是,写在指令后的内容都是以字符串形式去书写的,定义跳转这种实用的功能被掐的死死的,似乎可以和 TypeScript 配合达到,但是听说 Vue2 和 Ts 配合蛮困难的。组件结构
React 使用
.jsx
文件来定义组件,一般样式是单独引入的文件,在该组件内强耦合了 HTML 和 JS,使用{}
来解析表达式,写法如下:使用组件:
Vue 使用
.vue
文件来定义组件,在此文件中同时编写 HTML、CSS、JS,template
内使用{{}}
来解析表达式或值,写法如下:使用组件:
数据管理
React 和 Vue 都是单向数据流,父组件的数据可向下流入子组件,反过来则不行。组件的数据来源一般包括两个部分,一个是通过
props
传入的,另一个是自身的数据。react
在 React 中支持向下传递静态或动态的
prop
,静态prop
一般直接传字符串。props
函数组件获取
props
的方式如下:动态
prop
可以这也写:state
React 16.8 以前的
class component
使用state
来管理组件内的数据状态,16.8 后的hooks
使函数式组件也有了管理state
的能力。useState
返回一个state
,以及更新state
的函数。如果新的state
需要使用到上一次的state
,可以传递一个函数给setState
。该函数第一个参数接收的即为上一次的state
,处理并返回一个更新后的值。vue
在 Vue 中同样支持静态和动态的
prop
传递,不过在动态传递的情况下,要用指令v-bind
,简写为:
。props
静态
prop
一般传递字符串,获取props
方式如下:传递动态
prop
就不太一样,需要用到v-bind
:在 Vue 中对
prop
中可以做类型约束,比如:但是在 React 中要借助 prop-types 这个库才行(使用 TypeScript 只是编译时检查)。
data
Vue 中组件内部的数据状态由
data
来管理,当一个组件被定义,data
必须声明为返回一个初始数据对象的函数。可直接通过 vue 实例来对状态进行修改:
class 和 style
在
class
和style
的写法上,React 和 Vue 之间有比较大的差异。react
React 中使用
className
关键字来代替真实 dom 中的class
属性。className
React 中
className
一般传字符串常量或者字符串变量,不支持传递数组或者对象。React 里面直接采用 JS 的模板字符串语法,样式太多的情况下可以采用 classnames 这个包,优雅传递各种状态,使用非常简单:
style
React 中
style
接收一个对象:vue
与 React 不同的是,Vue 中对
class
和style
做了功能上的增强,可以传字符串、数组、对象。 另外,v-bind:class
还会与class
进行合并,v-bind:style
还会与style
进行合并。class
真实的 dom 结构渲染出来如下:
真实的 dom 结构渲染出来如下:
class
能直接作为组件的属性传递给组件内部最外层元素:使用时额外添加
class
:真实的 dom 结构渲染出来如下:
style
真实的 dom 结构渲染出来如下:
真实的 dom 结构渲染出来如下:
条件渲染
条件渲染就是根据某一条件去判断是否渲染某个内容。
react 实现
在 React 中常使用与运算符
&&
、三目运算符? :
、判断语句if...else
来实现条件渲染。1. 与(
&&
)运算符与运算符
&&
,左边值为真时,就会渲染右边的内容。2. 三目运算符(
? :
)和 js 中语法一样,条件满足就渲染
:
前面的内容,反之渲染后面。3. 多重判断语句
在
return
语句中不要写太多的条件嵌套判断,比如用三目运算符尽量不要使用超过一层,不然代码会变得非常难读,所以一般我们会把这种多重判断渲染内容的放到外部函数去做,函数内通过if...else
或switch case
去做筛选。vue 实现
在 vue 中实现条件渲染只需要使用指令
v-if
、v-else-if
、v-else
即可。1. v-if、v-else-if、v-else
和 js 的语法一致。
2. template 上使用
v-if
在
<template>
上使用v-if
可以决定是否渲染已被分组的整块内容,与 React 中使用<Fragment>
类似。元素显示隐藏
与条件渲染不同的是,我们还可以通过样式对元素的显隐进行控制,这也可以降低因 dom 节点的频繁增删对性能的影响。
react 实现
在 React 中我们通过修改内联样式(
style
)或增删选择器(class
)的方式来实现,主要是修改display
属性。修改内联样式
style
方式:增删类名方式:
vue 实现
Vue 中提供了
v-show
指令用于快捷操作元素是否显示,本质上也只是修改内联样式display
属性。在
showName
为false
时,style
的display
为none
:在
showName
为true
时,style
的display
属性被删除,使用该元素默认值:列表渲染
React 中使用原生 js 数组语法
map
来渲染列表,而 Vue 中使用指令v-for
来渲染列表。这一块儿 React 灵活一些,比如可以进行链式调用lists.filter(...).map(...)
进行过滤。react 实现
渲染数组:
渲染对象:
其实就是 js 的语法。
vue 实现
渲染数组:
渲染对象:
事件处理
无论是 React 还是 Vue 都对原生 dom 事件做了封装,但在使用上有挺大差异。
react
React 元素的事件处理使用方式和原生 dom 使用比较类似,但是在语法上有一定的不同:
onClick
),而不是纯小写。不传参数时,会隐式传递一个事件
event
对象作为处理函数的第一个参数,一般我们会这样写:大多数时候我们是需要往处理函数中传递其它参数的,我们可以这也写:
vue
Vue 中处理事件需要用到一个指令
v-on
,简写为@
,接受一个方法名,并以字符串形式传入。$event
占位访问事件event
对象)组件通信
在开发组件时不可避免会遇到父子组件、跨多层级组件之间的通信问题,无论在 React 还是 Vue 中,它们都有对应适合的解决方案。
父子组件通信
有一种很常见的场景:父组件维护了一组数据,并且某个数据的变动也是由父组件定义的函数来执行进行变更的,这个函数可以在父组件及其子组件中去调用,由子组件调用时还可以拿到子组件的数据。
举个例子:点击“改变随机数”按钮产生一个新的随机数,并更新页面值。
react 实现
React 中通过
props
+ 回调函数实现。父组件
App.jsx
:子组件
RandomNum.jsx
:一方面是父组件的
randomNum
数据通过num
传给了子组件,另一方面子组件又通过changeNum
传递的父组件回调函数接收子组件的数据,从而达到父子组件通信的效果。Tips:父组件调子组件方法可以通过
forwardRef
和useImperativeHandle
实现。vue 实现
第一种方式与上面 react 实现类似,同样的思路也可以在 vue 中实现,也就是通过
props
+ 回调函数。父组件
App.vue
:子组件
RandomNum.vue
:第二种方式通过
props
+ 自定义事件方式。父组件通过
props
传递数据给子组件,子组件使用$emit
触发自定义事件,父组件中监听子组件的自定义事件从而获取子组件传递来的数据。其本质也是通过回调函数实现子组件给父组件传数据。父组件
App.vue
:子组件
RandomNum.vue
:Tips:父组件调子组件方法可以通过
this.$refs
来实现。跨多层级组件通信
理论上我们可以通过共同的父组件实现兄弟组件通信,多层
props
传递实现祖孙级组件通信,但是这样会非常麻烦,写到后面代码也别维护了,因为没人维护的来~(我开始学 React 时候做的项目就没有用任何状态管理手段,做到后面我自己都维护不下去了)所以我们需要更高效且更可具维护性的方案,React 还是 Vue 都提供了这种能力。
react 实现
React 中实现主要借助
React.createContext
和useContext
这两个 API 来实现。根组件
App.jsx
:根组件下一层的组件
Title.jsx
:孙子组件
RandomNum.jsx
:vue 实现
Vue 中实现跨级组件间通信的方式实在是有点多(除了 vuex 这种工具),主要分析下以下两种怎么用:
$attrs
与$listeners
provide
与inject
第一种方式通过
provide
与inject
。根组件
App.vue
:这里一定要注意,
provide
要使用函数返回对象形式,不然拿不到this.randomNum
和this.handleUpdateNum
,这时候this
是undefined
。而且在
data
中定义的randomNum
必须是对象,原因在于将注入的数据变成可响应式的,看官网这段话:根组件下一层的组件
Title.vue
:孙子组件
RandomNum.vue
:第二种方式通过
$attrs
与$listeners
。 其实我觉得这种方式有点类似于props
的逐层传递,$attrs
可以使子组件通过props
拿到根组件通过v-bind
传递的数据,$listeners
可以使子组件通过this.$emit
触发根组件通过v-on
绑定的自定义事件回调。根组件
App.vue
:根组件下一层的组件
Title.vue
:孙子组件
RandomNum.vue
:缓存优化
在组件内,某个数据值的获取要经过大量复杂的计算,耗时较多时,React 和 Vue 都提供了优化的方法,对于相同输入,必定是同一输出的函数来说,这种结果是可缓存的,不必每次重新渲染时都重新计算一次。
react 的 useMemo 和 useCallback
在 React 中主要提供了两个钩子
useMemo
和useCallback
。 使用useMemo
来缓存值,使用useCallback
来缓存函数。假如现在有以下场景:点击“产生随机数”按钮,
randomNum
会被更改,组件重新渲染。点击“改变小西瓜数量”按钮,列表长度会递增。App.jsx
:List.jsx
:如果这时候你打开控制台,疯狂点击“产生随机数”按钮,你会发现这种情况:
理论上来说我们是不希望
List
组件在点击“产生随机数”时被重新渲染的,因为randomNum
这个状态与List
组件是无关的。可以看到即使我们在子组件中使用了React.memo
也是没用的,因为每次生成的都是一个在内存中全新的数组。这时候只需要将依赖
listLen
计算列表的地方使用useMemo
包裹起来就行了,这样只有在listLen
变化时,才会去重新计算list
。效果如下:
vue 的 computed
Vue 中用
computed
来表示计算属性,计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。App.vue
:List.vue
:另外,如果将
list
的求值放到methods
中的话,子组件也会重新渲染。侦听器
watch
的概念其实只有 Vue 才有,它的作用是监听props
、data
、computed
的变化,执行异步或开销较大的操作。在 React 中通过自定义 hook 也能实现类似 Vuewatch
的功能,具体的实现都可以另写一篇了,有机会研究下再做分享。在 Vue 中用法如下:
ref
React 和 Vue 都提供了访问原生 DOM 的特性,使用
ref
实现。react 实现
在当前组件中使用
ref
获得实际 DOM。但是有时候我们想要在父组件中获得子组件中的某个实际 DOM 元素就需要用到
React.forwardRef
了。父组件
App.jsx
:子组件
InputComponent.jsx
:vue 实现
在当前组件中使用
ref
获得实际 DOM。在 vue 中要获取子组件的实际 DOM 需要先获取子组件实例,再通过子组件实例的
$refs
拿到 DOM。父组件
App.vue
:子组件
InputComponent.vue
:受控与 v-model
React 中
input
、textarea
等非受控组件通过onChange
事件获取当前输入内容,将当前输入内容作为value
传入,此时它们就成为受控组件,这样做的目的是可以通过onChange
事件控制用户输入,比如使用正则表达式过滤不合理输入。Vue 中使用
v-model
实现数据双向绑定。react 实现
vue 实现
v-model
用于表单数据的双向绑定,其实它就是一个语法糖,这个背后就做了两个操作:v-bind
绑定一个value
属性。v-on
指令给当前元素绑定input
事件。对于自定义组件也可以使用
v-model
,子组件应该有如下操作:value
作为prop
。input
事件,并传入新值。父组件
App.vue
:子组件
InputComponent.vue
:插槽
这部分得先从 Vue 中的插槽讲起,个人觉得 Vue 中默认插槽、具名插槽、作用域插槽的这种划分已经覆盖了至少我所接触到的所有场景了,而 React 并没有这种划分,万物皆
props
。vue
vue 中通过
<slot>
实现插槽功能,包含默认插槽、具名插槽、作用域插槽。默认插槽
默认插槽使用
<slot></slot>
在组件中占了一个预留位置,使用该组件的起始标签和结束标签内包含的所有内容都会被渲染到这个占位的地方。父组件
App.vue
:子组件
Title.vue
:渲染出来的真实 DOM 结构验证了我们所说的“占位”:
具名插槽
默认插槽只能插入一个插槽,当插入多个插槽时需要使用具名插槽。使用
<slot name="xxx">
形式来定义具名插槽。子组件
Page.vue
:父组件
App.vue
:作用域插槽
有时让插槽内容能够访问子组件中才有的数据是很有用的。
子组件
Page.vue
:父组件
App.vue
:注意,这里
v-slot:default
没有简写。只有明确定义了name
的才能使用,比如#header="xxx"
。react
React 中可以通过
props.children
或Render Props
实现 Vue 中的插槽功能。props.children
其实
props.children
就是子组件起始和结束标签包裹的任何元素,和默认插槽没区别。父组件
App.jsx
:子组件
Title.jsx
:render props
记住在 React 中万物皆
props
就行了,我们来模拟实现作用域插槽。子组件
Page.jsx
:父组件
App.jsx
:我个人在 React 开发中迄今为止没用到类似作用域插槽的这种功能,不知道为什么在 Vue 中那么推崇。😂
逻辑复用
无论是什么框架,逻辑代码的复用这件事都是必须考虑的,在 React 中可以使用自定义 hook 抽离出经常使用到的逻辑达到复用效果,在 Vue2 中可以使用
mixins
来实现。react
在 React 16.8 以前,我们复用代码逻辑的常用方式是 HOC,现在我们常用自定义 Hook 来实现代码复用。 假设现在有以下场景,
A
组件和B
组件中都要在初次渲染时请求数据,但是又不想分别在两个组件中都去写请求的代码逻辑。HOC
首先我们定义一个高阶函数
withData
,它接收组件并返回一个接收props
的匿名函数,在该函数内写我们要复用的代码逻辑,然后将props
和复用代码的“结果”同样作为prop
传给被高阶函数包裹的组件,这样在该组件中就能通过props
拿到复用代码的“结果”了。
自定义 hook
自定义 hook 看起来就简单很多了,本质上就是把原本要在组件中写的代码包装到另一个 hook 中,要用的时候在组件里面调一下就行了。
vue
mixins
一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
根组件
App.vue
:子组件
A.vue
:子组件
B.vue
:混入
mixins.js
:通过观察我们会发现这和 React 中自定义 hook 很像,就是把原本要写在组件本身里的逻辑换了个地方写而已。
结语
通篇读下来会发现内容还是比较简单的,本文目的就像开头说的,通过对比的方式让大家能在两个框架之间建立起一个认知桥梁,这样子切换技术栈时会更容易接受点。我个人也认为这样的学习方式对于工作来说会更有效率些,首先让自己能干活,再去思考深入的问题。
参考
Vue2 官方文档 React 官方文档 为什么我们放弃了 Vue?Vue 和 React 深度对比