前言
本文写于 25 年 1 月,杭州云谷,这两天气候干冷转暖,宜学习,我已断更大半年,恰逢手头无事,就借这个机会写点学习笔记吧。我将尝试以 React 开发者视角,去学习 Vue,找出二者的共性,形成框架,同时将区别填充在框架中。
代码组织形式
在 React 中,其实有一个事实标准,一个组件会将代码组织在单个 .jsx
或 .tsx
文件中,Vue 在这一点上是相同的,Vue 的单文件组件(Single-File Component)会将代码组织在 .vue
文件中。
1 | // Counter.tsx |
1 | <!-- Counter.vue --> |
抛开工程实践,React 组件就是本地定义的 Class 或 Function,所以其实一个文件中可以包含多个组件,只不过工程上考虑到模块化,一般不会这么干。与之对应的,就是对象式的 Vue 组件。
1 | import { ref } from "vue"; |
模板语法
模板语言:
- Vue 使用基于 HTML 的模板语法,也支持 JSX 语法(但会失去编译时优化)。
- React 使用 JSX.
控制结构:
- Vue 提供特定的指令(如
v-if
、v-for
、v-bind
、v-on
等)来操作 DOM。 - React 则使用 JavaScript 的控制结构(如条件表达式和循环)来实现同样的功能。
- Vue 提供特定的指令(如
数据绑定:
- Vue 使用双大括号
{{}}
进行文本插值,并提供.sync
和.model
修饰符用于双向绑定。 - React 中的数据绑定是通过花括号
{}
内嵌 JavaScript 表达式实现的,通常为单向数据流。
- Vue 使用双大括号
响应式状态
状态定义
- Vue 使用
ref
或reactive
定义响应式状态 - React 使用
useState
或this.state
- Vue 使用
状态更新
- Vue 直接修改响应式状态的值即可。
- React 必须使用
useState
或this.setState
更新状态,且严格遵循不可变更新。
状态比较
- Vue 的
ref
或reactive
默认为深比较,修改深层嵌套的属性或数组元素,就能触发更新。 - React 默认为浅比较,修改深层嵌套属性或数组元素后,还必须更新对象或数组的引用。
- Vue 的
更新批处理
- Vue 的
ref
的更新不会同步提交到 Dom 上,而是统一在下一次 tick 中批量处理。 - React(concurrent mode)的更新也不会同步提交到 Dom 上,而是被添加至更新队列,在下次事件循环开始时,一次性处理掉。
- Vue 的
状态更新感知
- Vue 通过
ES6 Proxy
语法特性实现对响应式状态的监听,进而触发更新。 - React 通过调用
useState
钩子函数或setState
方法触发更新。
- Vue 通过
状态派生
- Vue 通过计算属性
computed
从状态派生出复杂数据,自动收集响应式依赖,响应式状态变化时重新计算,可以通过 setter 函数直接修改,且 getter 函数可以拿到上一个计算属性值。 - React 通过
useMemo
缓存复杂计算,依赖改变时重新计算,需手动管理依赖,只能通过更新依赖间接修改,可以间接通过 useRef 缓存上一个 useMemo 值(即自己维护一个快照)。
- Vue 通过计算属性
[注] 关于 React 批处理,此处是简化的说法,是为了方便理解,React 原生事件或异步代码回调中的更新并不会批处理,除非显式调用 batchUpdate API,详见我另一篇关于 React v17 源码解析的文章 React v17 源码解析
[注] 遗留一个问题,Vue 能否通过 computed
实现类似 React useCallback
的效果?
class & style
对于样式开发的优化,是 Vue 的一大亮点。
类名定义的样式
- Vue 通过字符串、对象或数组
class
定义类名。当使用对象时,key 为类名,value 为布尔值,表示是否添加该类名;当使用数组时,数组中的元素为类名,且数组元素也可以为对象。 - React 通过字符串
className
定义类名,需要通过字符串拼接实现动态样式。
- Vue 通过字符串、对象或数组
类名的传递
- Vue 会自动将组件接收的类名,默认传递到该组件模板的根元素上,多个根元素时通过
$attr.class
指定类名接收元素。 - React 需要手动将类名 prop 传递到组件的指定元素上。
- Vue 会自动将组件接收的类名,默认传递到该组件模板的根元素上,多个根元素时通过
内联样式
- Vue 通过对象或数组
style
定义内联样式。当使用数组时,数组元素为 style 对象,数组元素会进行合并。 - React 通过对象
style
定义内联样式。
- Vue 通过对象或数组
style 样式兼容性
- Vue 优化了 style 对象的兼容性。对于 key,自动检测并使用浏览器支持的样式前缀;对于 value,可以手动设为数组,定义多个值,运行时会使用浏览器支持的值。
- React 需要引入额外的类库来批量处理样式兼容性问题。
组件
组件定义:
- Vue 支持单文件组件(SFC),允许将模板、脚本和样式封装在一个
.vue
文件中。 - React 组件通常是用纯 JavaScript 定义的函数或类,样式和其他资源需要单独管理。
- Vue 支持单文件组件(SFC),允许将模板、脚本和样式封装在一个
子元素占位符:
- Vue 使用
<slot />
作为子元素占位符 - React 使用
props.children
作为子元素占位符
- Vue 使用
作用域
- Vue 中存在模板作用域,指令作用域。
- React 中的作用域同 JS 规范。
错误捕获
- Vue 使用 app.config.errorHandler 全局处理
- React 使用 ErrorBoundary 组件
HTML 片段
- Vue 使用
v-html
指令来渲染 HTML 内容。 - React 使用
dangerouslySetInnerHTML
属性来渲染 HTML 内容。
- Vue 使用
批量绑定属性
- Vue 使用
<div v-bind={props}></div>
批量绑定属性到元素上。 - React 使用解构赋值的方式来批量绑定属性
<div {...props} />
.
- Vue 使用
条件渲染
- Vue 使用
v-if / v-else-if / v-else
实现条件渲染,且v-show
通过 display 来控制样式显隐。 - React 使用
{ executor && <div>Hello World</div> }
或{ executor ? <div>Hello World</div> : null }
,通过动态类名或 style display 属性来控制样式显隐。
- Vue 使用
列表渲染
- Vue 使用
v-for="item in items"
来渲染列表,在v-for
所在的元素上使用:key
指定子元素标识符。 - React 使用
Array.prototype.map
或 ReactNode 数组,在子元素上使用key
指定组件标识符。
- Vue 使用
空元素
- Vue 使用
<template><template/>
标签来定义空元素,不会生成 DOM 元素。 - React 使用
<Fragment></Fragment>
或其简写<></>
标签来定义不生成 DOM 的空元素。
- Vue 使用
Props
- Vue 无论是选项式还是组合式组件,都需要显式声明 Props,否则模板中无法访问 Props.
- 选项式,
{ props: ["prop1", "prop2"] }
- 组合式,
defineProps(["prop1", "prop2"])
- 选项式,
- React 不必显式声明 Props
- Vue 无论是选项式还是组合式组件,都需要显式声明 Props,否则模板中无法访问 Props.
动态组件
- Vue 可使用
<component :is="component" />
在多个组件中动态切换,并可以通过包裹一层<KeepAlive>
避免销毁其他组件。 - React 需要手动实现。
- 组件注册
- Vue 组合式组件导入其他组件时,无需注册这些组件;选项式组件则必须通过
components
选项显式注册被导入的组件。 - React 使用其他组件时无需注册。
- 全局组件
- Vue 可以通过
app.component(name, component)
全局注册组件,在该应用中即可直接使用。 - React 不支持全局组件。
事件
Vue 的事件回调开发起来相当便捷高效,主要体现在以下方面。
事件绑定
- Vue 使用
v-on
或@
来绑定事件回调,如v-on:click="handleClick"
. - React 使用驼峰命名的事件属性名,如
onClick={handleClick}
等。
- Vue 使用
事件回调
- Vue 的事件回调支持内联的 JavaScript 语句,如
v-on:click="count++"
,同时也支持传递函数引用。 - React 仅支持传递函数引用。
- Vue 的事件回调支持内联的 JavaScript 语句,如
事件修饰符
- Vue 支持事件、按键、鼠标键修饰符,且修饰符支持链式调用,如
v-on:click.prevent.stop="handleClick"
. - React 不支持事件修饰符,需要手动实现。
- Vue 支持事件、按键、鼠标键修饰符,且修饰符支持链式调用,如
组件自定义事件
- Vue 可通过
defineEmits
或$emit
定义或发出自定义事件,有助于维持代码结构的清晰。 - React 组件不支持自定义事件,父组件将事件回调以 props 形式传递给子组件,以此实现类似效果,但大量事件回调和其他 props 混合在一起堪称噩梦。
- Vue 可通过
表单
Vue 同样对于常用的表单操作进行了简化。
输入监听与状态绑定
- Vue 通过
v-model
简化了表单输入监听与状态绑定,其等价于v-bind
和v-on
. - React 需要手动实现。
- Vue 通过
常用行为抽象
- Vue 通过修饰符
.lazy
、.number
、.trim
等简化了常用行为。 - React 需要手动实现。
- Vue 通过修饰符
生命周期
Vue 的生命周期 API,就我个人观点而言,是有点过时的,这种复杂的生命周期 Hooks 会带来额外的维护成本与心智负担,相比之下,React 已经事实上放弃了类组件基于生命周期开发的心智模型,改为拥抱 useEffect/useLayoutEffect,大大降低了开发者的心智负担,而心智负担和维护成本往往是一回事:)
副作用
依赖管理
- Vue 使用
watch
来收集并执行副作用函数,需手动管理依赖;使用简化版的watchEffect
自动收集依赖。 - React 使用
useEffect
收集并执行副作用函数,需要手动管理依赖。
- Vue 使用
首次执行 & 再次执行
- Vue 默认不会在首次渲染后执行
watch
的回调,可通过配置immediate: true
指定首次渲染后执行(等价于watchEffect
);后续被监听对象更新时,再次执行副作用函数。 - React 默认会在首次渲染后执行
useEffect
的回调;后续依赖项更新时,再次执行副作用函数。
- Vue 默认不会在首次渲染后执行
依赖比较
- Vue
watch
传递响应式对象时,通过深比较判断依赖是否变化;也支持 getter 函数这种浅比较,同时支持deep: true
配置项强制深比较; - React 仅支持浅比较,但可以将所有属性填入依赖数组,间接实现深比较(并非常见做法)。
- Vue
once 模式
- Vue
watch
可以配置once: true
指定副作用函数仅执行一次。 - React 没法完全做到相同行为,存在各种限制,如下:
- 业务逻辑仅执行一次:函数内部实现,非首次执行时跳出,确保业务逻辑仅执行一次,但副作用函数可能执行多次。
- 副作用函数仅执行一次:依赖项设为
[]
,确保副作用函数仅执行一次,但只能是首次执行时执行一次,无法监听状态。
- Vue
副作用清理
- Vue 可以通过
watch/watchEffect
副作用回调中同步调用onWatcherCleanup(() => { ... })
来实现副作用清理(v3.5 之后),或副作用回调也会接收cleanup
回调,效果同前者相同。 - React 更为直观,直接在副作用回调函数中
return
一个清理函数即可。
- Vue 可以通过
副作用执行时机
- Vue 的副作用回调执行于状态更新之后,DOM 更新之前。
- React 的副作用回调执行于状态更新之后,DOM 更新之前。
副作用回调中访问 DOM
- Vue
watch/watchEffect
配置flush: 'post'
指定在 DOM 更新之后执行(等价于watchPostEffect
),以便访问 DOM. - React 通过
useLayoutEffect
在 DOM 更新后执行副作用,以便访问 DOM。
- Vue
状态更新前触发副作用
- Vue
watch/watchEffect
配置flush: 'sync'
指定在状态更新前执行(等价于watchSyncEffect
)。 - React 函数组件不支持该行为,但可通过类组件的
UNSAFE_componentWillUpdate
实现,注意该方法已废弃,因为 concurrent mode 中,workLoop
可能被打断而多次执行。
- Vue
主动停止监听
- Vue 执行
watch/watchEffect/watchPostEffect/watchSyncEffect
会返回一个回调,用于主动停止监听。 - React 不支持主动停止监听。
- Vue 执行
监听死循环
- Vue
watch
在副作用中更新被监听的值,会触发抛出错误,但实际仅执行一次。watchEffect
允许循环更新,不会抛出错误。 - React 不允许在副作用中更新被监听的值,会导致死循环。
- Vue
DOM 引用
绑定
- Vue 将
useTemplateRef
创建的变量绑定到指定元素的ref
属性。 - React 通过
useRef
创建的变量绑定到指定元素的ref
属性。
- Vue 将
访问
- Vue 访问模版引用变量的
value
属性。 - React 访问引用变量的
current
属性。
- Vue 访问模版引用变量的
限制
- Vue 会在组件挂载后更新模板引用,因此需要额外判断引用是否为空,不过在
onMounted
中对于非条件渲染的 ref 是可以安全访问的,无需判空。 - React 会在组件挂载后更新模板引用,因此也需要额外判断引用是否为空,不过
useEffect
中对于非条件渲染的 ref 是可以安全访问的,无需判空。
- Vue 会在组件挂载后更新模板引用,因此需要额外判断引用是否为空,不过在
列表渲染
- Vue 允许在列表渲染的元素上绑定 ref,该 ref 实际是一个数组。
- React 没有类似用法,建议获取父元素的 ref,然后通过索引访问子元素。
ref 绑定函数
- Vue 可通过
:ref
传递一个函数或方法,该函数或方法的首个参数就是 DOM 引用,且该函数或方法每次更新或卸载都会调用一次。 - React 不支持这种用法。
- Vue 可通过
组件引用
绑定
- Vue 获取组件实例引用的方式和获取 DOM 引用一致,组件无需特殊处理。
- React 分为两种情况:
- 函数组件,需要通过
forwardRef
包装,而且获取的并非组件实例,而是组件通过useImperativeHandle
显式声明的属性或方法。 - 类组件,同获取 DOM 引用一致,无需特殊处理。
- 函数组件,需要通过
对外暴露属性或方法
- Vue 分为两种:
- 组合式定义的组件,
<script setup>
定义的方法是默认私有的,必须通过defineExpose
暴露指定的属性给外部使用。 - 选项式定义的组件,属性或方法都可以通过组件实例引用访问。
- 组合式定义的组件,
- React 也分为两种:
- 函数组件,通过
useImperativeHandle
可以在组件内部暴露指定的属性给外部使用。 - 类组件,类组件的公共方法、属性都可以通过组件实例引用访问。
- 函数组件,通过
- Vue 分为两种:
Vue useComposable VS React Custom Hook
Vue 与 React 在响应式状态及相关逻辑封装、复用上达成了一致,不同点在于 Vue 的 useComposable 以 use
开头,实际是一种社区约定或范式,不具备强制性,而 React 的自定义 Hook 则必须以 use
开头,否则编译报错。
写在最后
综合来看,Vue 抽象层次高,带来的好处就是减少重复代码,使用很方便,开发效率我认为是比 React 要高的。但坏处就是大量的隐式行为让人觉得摸不着头脑,在了解其底层机制前,会有一种割裂感,难以从语法角度建立联系,对于 React 开发者来说,割裂感尤为强烈。