前言

本文写于 25 年 1 月,杭州云谷,这两天气候干冷转暖,宜学习,我已断更大半年,恰逢手头无事,就借这个机会写点学习笔记吧。我将尝试以 React 开发者视角,去学习 Vue,找出二者的共性,形成框架,同时将区别填充在框架中。

代码组织形式

在 React 中,其实有一个事实标准,一个组件会将代码组织在单个 .jsx.tsx 文件中,Vue 在这一点上是相同的,Vue 的单文件组件(Single-File Component)会将代码组织在 .vue 文件中。

1
2
3
4
5
6
7
8
9
10
11
// Counter.tsx
import { useState } from "react";

function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
You clicked me {count} times.
</button>
);
}
1
2
3
4
5
6
7
8
9
<!-- Counter.vue -->
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>

<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>

抛开工程实践,React 组件就是本地定义的 Class 或 Function,所以其实一个文件中可以包含多个组件,只不过工程上考虑到模块化,一般不会这么干。与之对应的,就是对象式的 Vue 组件。

1
2
3
4
5
6
7
8
9
10
11
12
import { ref } from "vue";

export default {
setup() {
const count = ref(0);
return { count };
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`,
};

模板语法

  1. 模板语言

    • Vue 使用基于 HTML 的模板语法,也支持 JSX 语法(但会失去编译时优化)。
    • React 使用 JSX.
  2. 控制结构

    • Vue 提供特定的指令(如 v-ifv-forv-bindv-on 等)来操作 DOM。
    • React 则使用 JavaScript 的控制结构(如条件表达式和循环)来实现同样的功能。
  3. 数据绑定

    • Vue 使用双大括号 {{}} 进行文本插值,并提供 .sync.model 修饰符用于双向绑定
    • React 中的数据绑定是通过花括号 {} 内嵌 JavaScript 表达式实现的,通常为单向数据流

响应式状态

  1. 状态定义

    • Vue 使用 refreactive 定义响应式状态
    • React 使用 useStatethis.state
  2. 状态更新

    • Vue 直接修改响应式状态的值即可。
    • React 必须使用 useStatethis.setState 更新状态,且严格遵循不可变更新。
  3. 状态比较

    • Vue 的 refreactive 默认为深比较,修改深层嵌套的属性或数组元素,就能触发更新。
    • React 默认为浅比较,修改深层嵌套属性或数组元素后,还必须更新对象或数组的引用。
  4. 更新批处理

    • Vue 的 ref 的更新不会同步提交到 Dom 上,而是统一在下一次 tick 中批量处理。
    • React(concurrent mode)的更新也不会同步提交到 Dom 上,而是被添加至更新队列,在下次事件循环开始时,一次性处理掉。
  5. 状态更新感知

    • Vue 通过 ES6 Proxy 语法特性实现对响应式状态的监听,进而触发更新。
    • React 通过调用 useState 钩子函数或 setState 方法触发更新。
  6. 状态派生

    • Vue 通过计算属性 computed 从状态派生出复杂数据,自动收集响应式依赖,响应式状态变化时重新计算,可以通过 setter 函数直接修改,且 getter 函数可以拿到上一个计算属性值。
    • React 通过 useMemo 缓存复杂计算,依赖改变时重新计算,需手动管理依赖,只能通过更新依赖间接修改,可以间接通过 useRef 缓存上一个 useMemo 值(即自己维护一个快照)。

[注] 关于 React 批处理,此处是简化的说法,是为了方便理解,React 原生事件或异步代码回调中的更新并不会批处理,除非显式调用 batchUpdate API,详见我另一篇关于 React v17 源码解析的文章 React v17 源码解析

[注] 遗留一个问题,Vue 能否通过 computed 实现类似 React useCallback 的效果?

class & style

对于样式开发的优化,是 Vue 的一大亮点。

  1. 类名定义的样式

    • Vue 通过字符串、对象或数组 class 定义类名。当使用对象时,key 为类名,value 为布尔值,表示是否添加该类名;当使用数组时,数组中的元素为类名,且数组元素也可以为对象。
    • React 通过字符串 className 定义类名,需要通过字符串拼接实现动态样式。
  2. 类名的传递

    • Vue 会自动将组件接收的类名,默认传递到该组件模板的根元素上,多个根元素时通过 $attr.class 指定类名接收元素。
    • React 需要手动将类名 prop 传递到组件的指定元素上。
  3. 内联样式

    • Vue 通过对象或数组 style 定义内联样式。当使用数组时,数组元素为 style 对象,数组元素会进行合并。
    • React 通过对象 style 定义内联样式。
  4. style 样式兼容性

    • Vue 优化了 style 对象的兼容性。对于 key,自动检测并使用浏览器支持的样式前缀;对于 value,可以手动设为数组,定义多个值,运行时会使用浏览器支持的值。
    • React 需要引入额外的类库来批量处理样式兼容性问题。

组件

  1. 组件定义

    • Vue 支持单文件组件(SFC),允许将模板、脚本和样式封装在一个 .vue 文件中。
    • React 组件通常是用纯 JavaScript 定义的函数或类,样式和其他资源需要单独管理。
  2. 子元素占位符

    • Vue 使用 <slot /> 作为子元素占位符
    • React 使用 props.children 作为子元素占位符
  3. 作用域

    • Vue 中存在模板作用域,指令作用域。
    • React 中的作用域同 JS 规范。
  4. 错误捕获

    • Vue 使用 app.config.errorHandler 全局处理
    • React 使用 ErrorBoundary 组件
  5. HTML 片段

    • Vue 使用 v-html 指令来渲染 HTML 内容。
    • React 使用 dangerouslySetInnerHTML 属性来渲染 HTML 内容。
  6. 批量绑定属性

    • Vue 使用 <div v-bind={props}></div> 批量绑定属性到元素上。
    • React 使用解构赋值的方式来批量绑定属性<div {...props} />.
  7. 条件渲染

    • 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 属性来控制样式显隐。
  8. 列表渲染

    • Vue 使用 v-for="item in items" 来渲染列表,在 v-for 所在的元素上使用 :key 指定子元素标识符。
    • React 使用 Array.prototype.map 或 ReactNode 数组,在子元素上使用 key 指定组件标识符。
  9. 空元素

    • Vue 使用 <template><template/> 标签来定义空元素,不会生成 DOM 元素。
    • React 使用 <Fragment></Fragment> 或其简写 <></> 标签来定义不生成 DOM 的空元素。
  10. Props

    • Vue 无论是选项式还是组合式组件,都需要显式声明 Props,否则模板中无法访问 Props.
      • 选项式,{ props: ["prop1", "prop2"] }
      • 组合式,defineProps(["prop1", "prop2"])
    • React 不必显式声明 Props
  11. 动态组件

  • Vue 可使用 <component :is="component" /> 在多个组件中动态切换,并可以通过包裹一层 <KeepAlive> 避免销毁其他组件。
  • React 需要手动实现。
  1. 组件注册
  • Vue 组合式组件导入其他组件时,无需注册这些组件;选项式组件则必须通过 components 选项显式注册被导入的组件。
  • React 使用其他组件时无需注册。
  1. 全局组件
  • Vue 可以通过 app.component(name, component) 全局注册组件,在该应用中即可直接使用。
  • React 不支持全局组件。

事件

Vue 的事件回调开发起来相当便捷高效,主要体现在以下方面。

  1. 事件绑定

    • Vue 使用 v-on@ 来绑定事件回调,如 v-on:click="handleClick".
    • React 使用驼峰命名的事件属性名,如 onClick={handleClick} 等。
  2. 事件回调

    • Vue 的事件回调支持内联的 JavaScript 语句,如 v-on:click="count++",同时也支持传递函数引用。
    • React 仅支持传递函数引用。
  3. 事件修饰符

    • Vue 支持事件、按键、鼠标键修饰符,且修饰符支持链式调用,如 v-on:click.prevent.stop="handleClick".
    • React 不支持事件修饰符,需要手动实现。
  4. 组件自定义事件

    • Vue 可通过 defineEmits$emit 定义或发出自定义事件,有助于维持代码结构的清晰。
    • React 组件不支持自定义事件,父组件将事件回调以 props 形式传递给子组件,以此实现类似效果,但大量事件回调和其他 props 混合在一起堪称噩梦。

表单

Vue 同样对于常用的表单操作进行了简化。

  1. 输入监听与状态绑定

    • Vue 通过 v-model 简化了表单输入监听与状态绑定,其等价于 v-bindv-on.
    • React 需要手动实现。
  2. 常用行为抽象

    • Vue 通过修饰符 .lazy.number.trim 等简化了常用行为。
    • React 需要手动实现。

生命周期

Vue 的生命周期 API,就我个人观点而言,是有点过时的,这种复杂的生命周期 Hooks 会带来额外的维护成本与心智负担,相比之下,React 已经事实上放弃了类组件基于生命周期开发的心智模型,改为拥抱 useEffect/useLayoutEffect,大大降低了开发者的心智负担,而心智负担和维护成本往往是一回事:)

生命周期

副作用

  1. 依赖管理

    • Vue 使用 watch 来收集并执行副作用函数,需手动管理依赖;使用简化版的 watchEffect 自动收集依赖。
    • React 使用 useEffect 收集并执行副作用函数,需要手动管理依赖。
  2. 首次执行 & 再次执行

    • Vue 默认不会在首次渲染后执行 watch 的回调,可通过配置 immediate: true 指定首次渲染后执行(等价于 watchEffect);后续被监听对象更新时,再次执行副作用函数。
    • React 默认会在首次渲染后执行 useEffect 的回调;后续依赖项更新时,再次执行副作用函数。
  3. 依赖比较

    • Vue watch 传递响应式对象时,通过深比较判断依赖是否变化;也支持 getter 函数这种浅比较,同时支持 deep: true 配置项强制深比较;
    • React 仅支持浅比较,但可以将所有属性填入依赖数组,间接实现深比较(并非常见做法)。
  4. once 模式

    • Vue watch 可以配置 once: true 指定副作用函数仅执行一次。
    • React 没法完全做到相同行为,存在各种限制,如下:
      • 业务逻辑仅执行一次:函数内部实现,非首次执行时跳出,确保业务逻辑仅执行一次,但副作用函数可能执行多次
      • 副作用函数仅执行一次:依赖项设为 [],确保副作用函数仅执行一次,但只能是首次执行时执行一次,无法监听状态
  5. 副作用清理

    • Vue 可以通过 watch/watchEffect 副作用回调中同步调用 onWatcherCleanup(() => { ... }) 来实现副作用清理(v3.5 之后),或副作用回调也会接收 cleanup 回调,效果同前者相同。
    • React 更为直观,直接在副作用回调函数中 return 一个清理函数即可。
  6. 副作用执行时机

    • Vue 的副作用回调执行于状态更新之后,DOM 更新之前。
    • React 的副作用回调执行于状态更新之后,DOM 更新之前。
  7. 副作用回调中访问 DOM

    • Vue watch/watchEffect 配置 flush: 'post' 指定在 DOM 更新之后执行(等价于 watchPostEffect),以便访问 DOM.
    • React 通过 useLayoutEffect 在 DOM 更新后执行副作用,以便访问 DOM。
  8. 状态更新前触发副作用

    • Vue watch/watchEffect 配置 flush: 'sync' 指定在状态更新前执行(等价于 watchSyncEffect)。
    • React 函数组件不支持该行为,但可通过类组件的 UNSAFE_componentWillUpdate 实现,注意该方法已废弃,因为 concurrent mode 中,workLoop 可能被打断而多次执行。
  9. 主动停止监听

    • Vue 执行 watch/watchEffect/watchPostEffect/watchSyncEffect 会返回一个回调,用于主动停止监听。
    • React 不支持主动停止监听。
  10. 监听死循环

    • Vue watch 在副作用中更新被监听的值,会触发抛出错误,但实际仅执行一次。watchEffect 允许循环更新,不会抛出错误。
    • React 不允许在副作用中更新被监听的值,会导致死循环。

DOM 引用

  1. 绑定

    • Vue 将 useTemplateRef 创建的变量绑定到指定元素的 ref 属性。
    • React 通过 useRef 创建的变量绑定到指定元素的 ref 属性。
  2. 访问

    • Vue 访问模版引用变量的 value 属性。
    • React 访问引用变量的 current 属性。
  3. 限制

    • Vue 会在组件挂载后更新模板引用,因此需要额外判断引用是否为空,不过在 onMounted 中对于非条件渲染的 ref 是可以安全访问的,无需判空。
    • React 会在组件挂载后更新模板引用,因此也需要额外判断引用是否为空,不过 useEffect 中对于非条件渲染的 ref 是可以安全访问的,无需判空。
  4. 列表渲染

    • Vue 允许在列表渲染的元素上绑定 ref,该 ref 实际是一个数组。
    • React 没有类似用法,建议获取父元素的 ref,然后通过索引访问子元素。
  5. ref 绑定函数

    • Vue 可通过 :ref 传递一个函数或方法,该函数或方法的首个参数就是 DOM 引用,且该函数或方法每次更新或卸载都会调用一次。
    • React 不支持这种用法。

组件引用

  1. 绑定

    • Vue 获取组件实例引用的方式和获取 DOM 引用一致,组件无需特殊处理。
    • React 分为两种情况:
      • 函数组件,需要通过 forwardRef 包装,而且获取的并非组件实例,而是组件通过 useImperativeHandle 显式声明的属性或方法。
      • 类组件,同获取 DOM 引用一致,无需特殊处理。
  2. 对外暴露属性或方法

    • Vue 分为两种:
      • 组合式定义的组件,<script setup> 定义的方法是默认私有的,必须通过 defineExpose 暴露指定的属性给外部使用。
      • 选项式定义的组件,属性或方法都可以通过组件实例引用访问。
    • React 也分为两种:
      • 函数组件,通过 useImperativeHandle 可以在组件内部暴露指定的属性给外部使用。
      • 类组件,类组件的公共方法、属性都可以通过组件实例引用访问。

Vue useComposable VS React Custom Hook

Vue 与 React 在响应式状态及相关逻辑封装、复用上达成了一致,不同点在于 Vue 的 useComposable 以 use 开头,实际是一种社区约定或范式,不具备强制性,而 React 的自定义 Hook 则必须以 use 开头,否则编译报错。

写在最后

综合来看,Vue 抽象层次高,带来的好处就是减少重复代码,使用很方便,开发效率我认为是比 React 要高的。但坏处就是大量的隐式行为让人觉得摸不着头脑,在了解其底层机制前,会有一种割裂感,难以从语法角度建立联系,对于 React 开发者来说,割裂感尤为强烈。