为什么我们放弃了 Vue?Vue 和 React 深度对比

2,172 阅读12分钟

原文:为什么我们放弃了 Vue?Vue 和 React 深度对比
经作者授权转载

我使用 Vue 和 React 已经很长一段时间了,两个框架上实践代码量都在 10 万行以上。不得不说两者都是很 nice 的,帮助开发者减少很多工作量,这类框架是现代化前端开发必备的。然而 Vue 和 React 两者之间的选择并不像选择苹果或香蕉一样简单,两者在工程实践上的差距让我们逐渐放弃了 Vue。本文以不一样的角度对两者进行深度对比。

react vs vue

常见摇摆问题、观点

首先,我简短谈谈常见对比项目、观点的看法,这些部分内容可以通过一些文章或者 Vue 官方对比文档查到,主要目的是帮助小白解决入门摇摆问题。我会根据长期实践经验直接下结论,如果你反对,欢迎评论区留言 battle,反正我不会回答你这类问题。

Vue 或 React 文档更丰富?

两者都有丰富的文档(包括中文文档),Vue 文档React 中文,所以不用担心你四六级都过不了,看不懂文档,这都是有眼就行的事~当然,如果你提前懂点 javascript 相关知识也是大大滴好,ES6 语法更佳,可以在这里跟阮老师学习,免费的电子书。文档和持续进阶不是你在两个框架间做选择的原因。

Vue 的话需要记住各种指令,还有属性细节,免不了经常查文档。React 相对简单,记住:“函数入口是 props,出口是 html”就行了。

jquery 搞笑

React 学习门槛高?

这个也不是你选择框架的原因,如果这个也可以作为原因的话,我觉得是因为你懒,给自己找了借口。据我自己学习、实践总结,两个框架都很简单,有手就行,有脑就会,不见得会 React 就比 Vue 牛逼很多。两者都提供了相应脚手架,方便用户使用:

Vue

npm install -g @vue/cli
vue create my-project

React

npx create-react-app my-app
cd my-app
npm start

傻瓜式使用,无限 div 就完事了。

大项目用 React,小项目用 Vue

这是我以前在华为的时候,内部讨论两个框架对比时下的一个结论。 怎么说呢?这个就是万精油结论,没有参考意义。你如何定义一个项目是大项目?超过 xx 万行代码?后端 API 超过 xxx 个?无论什么项目,都有做“大”的可能,只要正常运营,你就得持续维护,补充新增的需求。框架的可持续演进更为重要。 当然我可以这么跟你说,React 适不适合“小”项目我不知道,但是 Vue 不适合“大”项目,业务代码超过 5 万行之后问题明显,后面会详细说这点。

XX 大公司也在用 Vue,我们跟随就行了

很多新手在入门一些框架,或者选型组件、方案时会看看哪些大公司已经用了,避免自己踩坑。当然啦,我也这么做滴。但是大公司可能只是“尝鲜”、“实验性”使用,这些项目对他们来说无关紧要,选型失败了,压力给到开发工程师,996 重构就行了,而你,╮(╯▽╰)╭ 项目和屎一样也得维护下去。

举个选型错误的例子,看看大公司怎么拯救的。

以前在华为做硬件项目的时候,用的原理图软件,那叫一个辣鸡,用着用着总想砸掉电脑。历史问题,选型错误,但无奈很多项目、基础库都在上面,只能硬着头皮搞,迁移的成本太高,软件厂商水平太差,但是华为牛逼啊,把那个软件大改特改,各种内部数据库都集成在上面,各种自开发的辅助工具,还是可以开发出很牛逼的产品,部门做的单板连续11年全球第一。

如果你觉得你能搞定选型错误带来的问题,或者你在华为,那当我没说。(PS:以前还用 tcl 写脚本呢,你也可以试试蛋不蛋疼)

Vue 模板简单,React jsx 有学习成本

同上。两者都很简单,一学就会。连这点东东都叫学习成本,我只能说:“我不是针对你,我是说在座的各位都是......” ( Vue 的模板有很多工程实践问题,后面详说。)

性能对比

可以看看这个第三方基准测试,两者都挺快的。不过我们实践过程中发现有差异,大列表渲染、大量数据加载,不做进一步优化的话 Vue 明显比 React 慢。TaskHub 这个网站我们以前就是用 Vue 写的,后来直接迁移到 React 前端性能大大提升,用户体验有明显差距(数据结构、后台不变)。

深度对比

本来想简单写写,没想到前面写了那么多了,╮(╯▽╰)╭,下面是重头戏,写写实践过程中发现的问题,两个框架的解决思路。如果你还是小白,下面的一些东西可能没接触过,可以看下这篇文章:【译】通过创建相同的 APP,对比 React 和 Vue,亲自实现一下,了解基础知识。

市场占比

相关 npm 下载量见上图,市场已经用脚投票了。看到这里,如果你只想知道选型结论,你可以走了。如果你还说 xx 大公司在用 Vue,跟着就行。可以这么说吧,大公司更多用的是 React,用 Vue 更多的目的是保留相关技术栈能力,多一个选择,避免 React license 事件再次发生。

当然,尤大也在这里说过,看npm下载量没用,实际使用应该参考 devTool 的下载量。但是...为啥我打开的很多网站下面这个标都是亮的?

开发生态

客观来说,作为核心团队成员,显然我们会更偏爱 Vue,认为对于某些问题来讲用 Vue 解决会更好。如果没有这点信念,我们也就不会整天为此忙活了。但是在此,我们想尽可能地公平和准确地来描述一切。其他的框架也有显著的优点,例如 React 庞大的生态系统

by Vue 官方

生态上的差距是明显的,这点 Vue 官方也承认的,很多人因为生态这点迁移到 React,不过我本人不是很在意,Vue 生态也不差,如果说你用了 React 生态的东西就觉得很牛逼,你的竞争对手也会用,这点并不能给你产品带来多大增值,竞争力还是要靠自己手码出来的好。下面简单带过:

UI 组件

两者的周边 UI 库都挺丰富的,React 稍微多一点,不过这不是选型的关键,自己手写 UI 库也不是什么难事,偶尔封装一下原生标签也是很简单的。以前用 Vue 的时候还没有太多 UI 库,手动写了一个功能比较全的 UI 库,用 rollup 打包,也就 2 万行代码左右,有手就行。

dom 相关的第三方库

Vue 和 React 都有 ref 可以操作 dom,自己封装一下不是什么难事。可以找找有没别人封装好的,拿来主义。

小程序(划重点)

有小程序开发经验的同学都知道,小程序原生开发是很蛋疼的,通常需要借助框架封装,代码转换。常见的有几个框架:

  • Taro (React 技术栈,推荐使用)
  • wepy(Vue 技术栈,强烈不推荐使用)
  • uni-app(Vue 技术栈,可以使用)

这些小程序开发框架都是基于 Vue 或者 React 的二次封装,简化小程序开发。

vue 的一些周边库和 Vue 强绑定,而不是以一个独立 js 库的形式存在。导致代码难以复用,相关 Bug、问题也带到了二次开发的框架中。

这种强依赖导致的问题会给以后项目升级、迁移带来很多问题。 比如 vuex 作为 Vue 官方推荐的状态管理方案,只能在 Vue 上面使用,不能在 React 上面使用。Redux 状态管理在 React 上用的多,这个却能用在 Vue 上面。 类似的问题很多,你会发现 React 周边的东西可以用于 Vue,Vue 的东西不能用在 React 上。

如果你觉得这个问题不严重,当你把 Vue 代码迁移到小程序 wepy 框架时发现,wepy 不支持 Vuex (bug 异常多),状态管理只能用 redux,欲哭无泪。 同样的问题,如果你用的是 React 相关技术栈,React 迁移到 Taro 小程序框架异常简单,而且还能一次性生成微信小程序、支付宝小程序、字节跳动小程序等,代码复用率高。

APP 生态

weex、rn 这块我没有比较好的实践经验,两者用于生产方案都要慎重考虑。rn 比 weex 成熟这点是明确的。

逻辑代码组织

茴字的写法

Vue 三种组件写法对比(Js 部分)

Object API 29 lines

import Vue, { PropOptions } from 'vue'

interface User {
  firstName: string
  lastName: number
}

export default Vue.extend({
  name: 'YourComponent',

  props: {
    user: {
      type: Object,
      required: true
    } as PropOptions<User>
  },

  data () {
    return {
      message: 'This is a message'
    }
  },

  computed: {
    fullName (): string {
      return `${this.user.firstName} ${this.user.lastName}`
    }
  }
})

Class API 17 lines

import { Vue, Component, Prop } from 'vue-property-decorator'

interface User {
  firstName: string
  lastName: number
}

@Component
export default class YourComponent extends Vue {
  @Prop({ type: Object, required: true }) readonly user!: User

  message: string = 'This is a message'

  get fullName (): string {
    return `${this.user.firstName} ${this.user.lastName}`
  }
}

Function API 25 lines

import Vue from 'vue'
import { computed, value } from 'vue-function-api'

interface User {
  firstName: string
  lastName: number
}

interface YourProps {
  user?: User
}

export default Vue.extend({
  name: 'YourComponent',

  setup ({ user }: YourProps) {
    const fullName = computed(() => `${user.firstName} ${user.lastName}`)
    const message = value('This is a message')

    return {
      fullName,
      message
    }
  }
})
写法优点缺点
Object APIVue 官方写法,方便Vue直接处理组件1. 代码长、缩进多,组件复杂时难以理清逻辑,不好进行分割
2. 混入较多Vue的概念,新手学习成本高
Class API相关概念可以用class的思路理解,可以更好地描述Vue的混入、data、computed,生命周期钩子等概念。Vue 3.0 将原生支持class写法用到了修饰器语法特性,目前还在实验阶段(typescript可以使用helper函数解决兼容问题,问题不大)
Function API无状态(部分场景),更好的单元测试、并行化函数式写法很容易写出回调地狱,导致代码可读性、可维护性差,目前纯粹function api 写法较少见

React 两种组件写法对比(Js 部分)

class 组件 34 lines

import React, { Component } from 'react';

interface P {}

interface S {}

class Index extends Component<P, S> {

  constructor(props: Readonly<P>) {
    super(props);
    this.state = {};
  }

  static defaultProps = {};

  componentDidMount() {}

  componentDidUpdate(prevProps: Readonly<P>) {}

  componentWillUnmount() {}

  render() {
    return (
      <div>
      </div>
    );
  }
}

export default Index;

函数组件 15 lines

import React, { FC } from "react";

interface Props {}

const Index: FC<Props> = (props) => {
  // js 代码

  return (
    <div></div>
  );
};

Index.defaultProps = {};

export default Index;

在 js 逻辑部分两者写法没毛病,都需要用到框架特定的生命周期钩子,Vue 的 class 写法最为简洁(3种对比),React 的 function 写法最为清晰(全部写法对比)。这部分不是选择关键,怎么写是个人喜好。

组件内状态管理

Vue 使用的是数据对象(data),React 使用的是状态对象(不可变 state),这点两个框架的设计不同,如下的问题解决思路也不同。

  1. 我如何修改数据?

    • Vue 直接 this 引用数据对象,直接修改。
    • React 使用 setState 方法修改
  2. 框架如何发现数据被修改?

    • Vue 使用 es5 新方法 Object.defineProperty,劫持 setter、getter 实现数据监听。
    • React,你用了 setState,它通过这个函数就知道哪些数据变化了。
  3. 我如何发现数据被修改?

    • Vue:使用 watcher,或者 computed 属性发现
    • React:componentWillUpdate、componentDidUpdate 中可以监听变化,或者函数组件的依赖部分插入
  4. 框架何时渲染修改的数据,我如何知道已经渲染好了?

    • Vue:在适当的时候渲染,你通过使用 watcher,或者 computed 属性发现
    • React:setState 调用后在适当的时候重新渲染,并调用相关生命周期钩子

在组件状态管理功能上两者都没有太多槽点,如果要说的话就是 Vue watcher 写多了代码一堆缩进,比较难看,React 也没好多少。

Vue 的数据对象相比 React 的状态对象在代码膨胀的时候差距就来了。代码少的时候 Vue 的写法更为简洁,但组件状态很多,需要明确数据更新逻辑时,React 简单的 setState({}, callback),就搞定了,Vue 有点让人摸不到头脑。

Vue 项目解决 bug 和疑难杂症三大定理

  1. 没有什么是 deep watch 解决不了的,有就加 immediate
  2. 事件相关,dom 相关记得 nextTick
  3. 实在不行,就用 setTimeout

(来自某个师兄)

React 的不可变(immutable)状态在应用复杂时表现出的透明、可测试性更佳。

以上内容对比下来,感觉两者都OKOK的,功能也健全,Vue 生态差一点,但是可以自己动手丰衣足食。下面几点是我们真正弃用 Vue 的原因。

沃苏艾德布耀布耀德说过:同样的问题,在语言层面上的解决方案才是最佳解决方案。语言生命周期长于框架生命周期

模板语法 VS JSX

上下文丢失

Vue 的单文件组件,使用 <template><script> 对代码进行分割,直接导致的问题就是上下文丢失。 举个例子,你封装了一些常用的函数,在 Vue 文件中 import 进来。你这个函数能在 template 中直接使用吗?

// filter.js 文件
export function isNickname(value) {
  return /^[\s\S]{1,50}$/.test(value);
}
<template>
  <div>
    {{ a }}
    <button @click="a = isNickname('abc')">Test</button>
    {{ b }}
  </div>
</template>

<script>
// eslint-disable-next-line no-unused-vars
import {isNickname} from '../fn/filter';


export default {
  name: 'HelloWorld',
  props: { msg: String },
  data: () => {
    return {
      a: false,
      b: 1,
      c: 1,
    }
  },
  methods: {
    isNickname1() {
      return isNickname('abc');
    }
  }
}
</script>

<style scoped></style>

上述代码会报错:

[Vue warn]: Property or method "isNickname" is not defined on the instance but referenced during render

所以你只能将方法定义在methods中,再引用进来。模板语法并不知道你有 isNickname 这个函数,简单的操作多了 3 行代码。

模板语法不是图灵完备的,必须转换为js代码(render 函数),放在component语境下才行。
类似的例子还有很多,你会发现,你写的代码与 Vue 强绑定了,哪天Vue核心库崩了,你代码也崩了。Vue 核心库升级了,周边依赖库也得跟着升级。

模板分割

好的代码组织能将常变与不变的部分进行分割解耦

Vue 的模板严重限制了这一点。 举个例子,前端有个下拉菜单,功能不断增加,而且对于不同的人要显示不同菜单(权限管理)。在 Vue 中,为了实现 html 代码(绑定在 template 中)的分割,你只能再搞一个组件。在 React 中,可以直接这样写:

const menu = <div>abc<div>;

可单独做一个组件(低开销函数组件),也可当做变量,放在当前代码中。相对灵活很多。

JSX 手写 render 渲染函数自带下面的优势

  • 完整的 js 功能来构建视图页面,可以使用临时变量、js 自带的控制流、以及直接引用当前 js 作用域中的值
  • 开发工具对 jsx 的支持比现有 vue 模板先进(linting、typescript、编译器自动补全)

JSX 可以用于 Vue 可以用于 React,就像 Redux 一样。这种语言是与框架解耦的。

“虽然模板语法有那么多问题,但是 Vue 也支持 JSX 呀。”

我猜到你会这么说,但就像上面所说的,既然我一定要用JSX/TSX、Redux了,那我为什么不用 React?

"基于 HTML 的模板使得将已有的应用逐步迁移到 Vue 更为容易"

不会更容易,只会更麻烦。 首先,下面会说到的 template 中无法很好 linting、type 推断,代码迁移过去很多 bug 无法及时发现。其次代码迁移很大部分都是 js 逻辑的迁移(这个更重要),迁移到 vue 中,你需要填鸭式拆分原先代码,放到 computed、menthods 中,工作量不小且代码和 Vue 强绑定。最后,原代码 class、@click 这些东西,有现代化的编辑器,批量 replace 成 className、onClick 不是很简单的事情吗?

Typescript、linting 支持

这点更是致命,Typescript 已成为我们前端开发必需。类型检测、推断对于代码重构非常重要,哪天后端字段改了,前端可以很方便设配改动,明确知道代码改动点在哪,构建前就能发现大量错误。

而 Vue 的模板不支持 typescript(官方还在增强),在模板上支持要很多“hack”操作,原始框架更为复杂。 Vue.extend 对象中编写代码很难有比较好的 ts 推断,为了更好的支持 Typescript,我们以前都是使用 Vue 的 Class 写法(参考上文)。前端配合后台改动接口,然而很多未提前检查出的错误都出现在模板代码中。

可测试性、重构

Vue 需要新建一个.vue 文件

<template>
  <div>
    {{hello}}
  </div>
</template>

<script>
export default {
  name: 'Test',
  props: {
    hello: String
  },
  created() {
    console.log(this.hello);
  }
}
</script>

<style scoped></style>

React 操作都在 jsx 环境下执行,放的位置随意,写法比模板更容易测试,迭代:

function Test(props: { hello: string }) {
  console.log(props);
  return <div>{props.hello}</div>
}

Vue 与 React 测试成本的差距明显。React 手起刀落,一个函数就搞定了,要测试什么内容清晰可见。如果要重构 hello 字段,Vue 要记得 template 中的代码也要手动改,React 直接 typescript 类型、属性重构就行了,编辑器自动化。

复杂状态、Action 管理

全局状态管理方案选型是很重要的,毕竟 95% 以上的 API 对接代码都在这里,这部分代码占全局代码很大一部分比例,能否复用、重构、测试成为选择的关键。

Vue 推荐的方案只有强耦合的 Vuex(Redux 迁移到 Vue 等不算在内) React 周边方案有 Redux、Mobx 等。这些库不会与 React 有太强的耦合(可以独立存在)。 两个框架的状态管理思想差不多,都是单向数据流、单例模式(Vuex & Redux)。

Vuex

Vuex

Vuex 的源码不多,可以看这里。可以看到代码中有很多和 Vue 强绑定的东东,脱离了 Vue,这东西就没法用了。你可能会说我就用 Vue,什么 React 不去用不就完了?考虑以下场景:

  • 项目经理要把 Vue 的代码迁移支持小程序,突然!有的框架不支持Vuex,脑袋嗡嗡叫
  • 项目经理说要设配 APP 端,突然!一堆Bug!脑袋嗡嗡叫
  • 项目经理脑抽,要把 React 项目迁移到 Vue,突然!redux!用的还是 saga!脑袋嗡嗡叫
  • 状态管理出现竟态问题!卧槽要写一堆烂码去解决。新人看了脑袋嗡嗡叫

怎么办?!!!这部分的代码比 Vuex 源码都多? 这些问题都是状态管理库和框架强绑定导致的,框架上的问题也会影响到周边库。

if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

可以看到,Vue 核心升级,这些伴随的库也得升级、测试。在非浏览器环境下运行时,由于 Vue (或类Vue 框架)的初始化等机制需要改动,会导致相关库,如 Vuex 不可用,多了一个代码分支,相关代码无法复用、测试、重构负担重。

Redux

redux

Redux 是 React 上比较常用的状态管理方案,其设计思想非常简单(见上图),可以独立使用,相关代码容易迁移到不同平台。衍生出周边异步方案也很多:

选型可以参考这里 & 这里,我们用 saga 比较多,处理竟态问题等比较简单,起步多看看文档就可以,也不难。下面这张图可以帮助你理解几个方案之间的关系,利弊权衡。

相关插件也很丰富,参考:Redux Middleware。你会发现很多你想要的东西 Vuex 都木有!

“既然这样,我在 Vue 上用 Redux 就行了”

也行,毕竟这样以后迁移到 React 会简单点。

万恶之源 this 指针

写过 React 函数组件的同学都知道,相比 class 组件,函数组件少了 this 指针,代码简化、清晰不少。而这个问题在 Vue 上更为严重。全局 this !

有人觉得这是优点,方便使用。等你代码量上去了再来说话。

当项目多人协作的时候,或者承接某某祖传代码,你不全局搜索,你都不知道 this 上面挂了羊头还是狗肉。

  • this.ajax
  • this.http
  • this.message
  • this.wtf......

正如一位网友评论:

那东西就是全局作用域。拿“允许在全局作用域上随便放东西很方便”作为优点的话,和“允许随地大小便会很方便”有什么区别……

写 C 语言的新手都知道全局变量不要随意用,这满天飞的 this,张三读不懂,李四看不懂,IDE 也不懂。而且这是官方推荐写法,╮(╯▽╰)╭(说全局不太准确,应该说是组件作用域)

想起我以前写的 Vue UI 库,叫 SUE-UI,sue~很快的样子。为了避免以后和其他插件冲突,插件使用都是:

  • this.$su_message
  • this.$su_modal
  • this.$su_toast

往事不堪回首啊!

最后

框架功能上,暂时没有发现 Vue 做的来 React 做不来的事情,反过来也一样,两个框架都能满足功能需求。 工程实践上,由于耦合性、代码组织灵活性、平滑升级、测试、重构让我们最终放弃了 Vue。

在 Vue 中你操作的是定义好的对象,React 中你操作的是一个函数。所谓前端开发,本质就是在编写下面几个函数。

S = async(A1)
S = sync(A2)
UI = f(S)

显然,React 对此的抽象更为彻底。 (完)