阅读 611

Vue 3 和 React 16.8 到底能多像

导读

注:为行文方便,本文将对 Vue 3 的 composition api 也称为 hooks,也是目前业内比较通俗的说法。

Vue 团队于 2020 年 9 月 18 日晚 11 点半发布了 Vue 3.0 版本。大家最关注的点就是 Vue 也引入了类似 React Hooks 的概念和用法。很多人说二者越来越像了,那么它们到底有多像?哪里像?能像到什么程度? 我想没有比列出代码对比更直观的方式了,毕竟 “Talk is cheap, show me the code.” 才是程序员的风格。

本文将以 表单提交列表展示 两个典型场景,来对比 Vue 3 和 React16.8+ 的代码实现。React 的代码将只使用 hooks + functional component 的形式展现;Vue 由于支持的形式更丰富,所以会有 hooks + template、hooks + JSX 两个版本,后者将是体现本文主题的关键点。

场景说明

场景一 - Form

image.png

如上图,一个最简单的表单提交场景,主要展示数据响应式的实现,具体说明如下:

  1. 表单有初始值,进入页面后从服务端异步获取,API 为 fetchUserInfo
  2. input 和 radio 实时响应用户输入
  3. 点击 Submit 按钮,会 alert 出当前表单展示的用户信息

场景二 - Table

image.png

如上图,一个最简单的表格展示场景,主要展示对数组的处理以及表达式的实现,具体说明如下:

  1. 数据从后端异步获取,API 为 fetchUserList
  2. sex 字段返回值为 0 或 1,列表要分别展示为 Female 或 Male

代码实现

全文的代码将全部采用 TS 实现

API & Types

此部分代码为必要的 TS 声明和模拟的后端 API,是所有例子的前置依赖。

// services.ts
export enum Sex {
  female,
  male,
}

export interface UserInfo {
  name: string;
  sex: Sex;
}

export const fetchUserInfo = (userId: string): Promise<UserInfo> =>
  new Promise((rev) => {
    setTimeout(() => rev({ name: "Zhang san", sex: Sex.male }), 500);
  });

export const updateUserInfo = (params: UserInfo): Promise<boolean> =>
  Promise.resolve(true);

export const fetchUserList = (): Promise<UserInfo[]> =>
  new Promise((rev) => {
    setTimeout(() => {
      rev([
        { name: "Zhang san", sex: Sex.male },
        { name: "Li si", sex: Sex.female },
        { name: "Wang wu", sex: Sex.male },
      ]);
    }, 500);
  });
复制代码

Vue 3 Hooks + template

我们先来看下 .vue 单文件组件的经典写法,也就是 template + script + style 的形式。本例只是做了 Vue 3 hooks 的改造,显然这根 React 还不够像,我们只是做个铺垫。

Form.vue

<template>
  <div>
    <div>
      <div>Name</div>
      <input type="text" v-model="name" />
    </div>
    <div>
      <div>Sex</div>
      <input type="radio" name="sex" :checked="maleChecked" @click="setSex(Sex.male)" />Male
      <input type="radio" name="sex" :checked="femaleChecked" @click="setSex(Sex.female)" />Female
    </div>
    <p>
      <button @click="handleSubmit">Submit</button>
    </p>
  </div>
</template>

<script lang="ts">
import { onMounted, ref, computed } from "vue";
import { Sex, fetchUserInfo, updateUserInfo } from "../services";
export default {
  setup() {
    const nameRef = ref("");
    const sexRef = ref(Sex.male);

    onMounted(() => {
      fetchUserInfo("id-xxx").then((res) => {
        nameRef.value = res.name;
        sexRef.value = res.sex;
      });
    });

    const handleSubmit = () => {
      const params = { name: nameRef.value, sex: sexRef.value };
      updateUserInfo(params).then((res) => {
        if (res) alert(JSON.stringify(params));
      });
    };

    const setSex = (sex: Sex) => {
      sexRef.value = sex;
    };

    const maleChecked = computed(() => sexRef.value === Sex.male);
    const femaleChecked = computed(() => sexRef.value === Sex.female);

    return {
      name: nameRef,
      sex: sexRef,
      handleSubmit,
      setSex,
      maleChecked,
      femaleChecked,
    };
  },
};
</script>
复制代码

从以上代码可以看出,理论上在 Vue 3 里,data、methods、computed、生命周期等这些代码,都可以不再出现在实例对象的第一层,而是全部聚合到了 setup 里。正因如此,setup 内的每一行代码都可以自由的调整位置,以达到“根据功能聚合代码”的目的,甚至可以再进一步的抽离出去,形成独立的 hooks。这也是 Vue 团队引入此功能的目的所在。详见官方文档 Why Composition API?

另外,请注意 setSexmaleCheckedfemaleChecked 的实现,下文会有所优化。回过头来看的话,这部分实际上有些冗余了。

Table.vue

<template>
  <table :cellPadding="5" :cellSpacing="5">
    <tr>
      <th>Name</th>
      <th>Sex</th>
    </tr>
    <tr v-for="user in userList" :key="user.name">
      <td>{{ user.name }}</td>
      <td>{{ user.sex === 0 ? "Male" : "Female" }}</td>
    </tr>
  </table>
</template>

<script lang="ts">
import { onMounted, ref } from "vue";
import { fetchUserList, UserInfo } from "../services";
export default {
  setup() {
    const userListRef = ref<UserInfo[]>([]);

    onMounted(() => {
      fetchUserList().then((res) => {
        userListRef.value = res;
      });
    });

    return {
      userList: userListRef,
    };
  },
};
</script>
复制代码

表格场景的代码实现非常简单,但是请注意 sex 转换成 Male/Female 部分的逻辑(template 部分)。我们看到,代码里出现了 Magic Number。这是因为 template 中无法结合 TS 导致的,否则完全可以用 Sex.male 这样的代码代替(见下文代码)。

当然,我们也可以把这个表达式的逻辑放到 setup 中,用一个 function 实现(类似 Form 例中的 maleChecked 方式),这样就可以充分利用 TS 的特性让代码变得更可读,但是会比较重,也不太优雅。这个是目前笔者认为 Vue 结合 TS 最尴尬的地方,template 与 TS 几乎无法结合。

Vue 3 + JSX vs. React 16.8+

重头戏来了,经过上面的铺垫,我们来看下 Vue3 和 JSX 在一起会起什么样的化学反应。为展示方便,下面的代码进行了格式处理并用图片展示(文末会有代码的文本版),尽量将相同功能对齐,左侧 Vue 右侧 React。下面开始“大家来找茬”:

Form

image.png

Table

image.png

像,太像了,真的太像了。起码表面上看起来,它们的相似度能达到 80%。

本文的例子非常简单,当加入 props、slot、其他生命周期等特性的时候,二者的代码实现还是会有较大差异的。笔者会另起一篇文章写,本文还是聚焦在二者像的部分。

思考

细心的朋友会注意到,文章的标题用的是“能多像”而不是“有多像”。这是因为要想让二者代码很像,Vue 必须使用 JSX 的方式,所以是需要一些主观控制才能达到“像”的目的,不过这已足够说明二者在深层次的设计理念上有绝对的相似之处。笔者无意纠结于谁抄袭谁,谁更优秀的问题,只是想学习和理解其设计理念和思想。不过人毕竟是感情动物,还是会有所偏向,下面就简单说说笔者这几年的思想变化,供大家参考。

不得不说,从 VDOM 到 Hooks,React 团队对整个行业起到了颠覆性的影响。前者催生了前端跨平台的解决方案,后者让前端项目的可维护性提高了一大截,真真让人拍案叫绝。这些理念也很快被整个行业所接受并借鉴。笔者在 React 16.8 之前,更欣赏 Vue 多一些,因为其文档与渐进式的设计理念实在做的太棒了,Vue 的学习曲线要明显比 React 平滑的多,而且全家桶使用也更简单,管理较集中(都是 Vue 团队维护),.vue 单文件写法也对于熟悉模板语法的前端同学比较友好。当时都流行说,React 适合做大型复杂项目,Vue 更适合做一些中小型项目,个人认为这种断言的说服力很有限,根本不疼不痒。

但是 React Hooks 的出现,让 React 的世界只剩下了 functional component 和 hooks 的概念,二者与 TS 的结合毫无阻碍,完美配合。

与之相对的,老版本的 class component 的方式,实际上会让 TS 的使用在 Class 这里产生断层。比如 constructor 的参数 props,不能够自动继承 class 泛型中的声明,必须手动再次声明(见下图代码注释)。虽然问题不大,但是总归不算完美。

image.png

这就造成了 hooks + jsx 的收益,明显大于放弃使用 template 所造成的不适,毕竟 template 与 TS 基本算得上是格格不入了。尤大也说过,“对 TS 的支持更友好”,是 Vue 3 的一个重大提升,相信大牛对于这点的认识肯定比笔者要深(补充:实际已经有了解决方案,详见 Vue3 + TS 最佳实践)。就像上文展示的,现在 Vue 可以有多种写法,而且它们是那么的不同,选择多同样意味着容易造成混乱,也许过段时间关于 Vue 3 的最佳实践之类的讨论会成为热点。就笔者而言,还是更倾向于拥抱 JSX + TS,因为这个组合真的能够大大提升项目的可维护性。

总结

TS 的出现,让前端代码的可读性、稳定性与可维护性有了显著的提高,可以说是提升前端生产效率的重要工具。以此为前提,前端项目如果能够充分利用 TS,将能获得可观的收益。

React Hooks 的出现,让 React 与 TS 形成了完美的配合。目前来看,Vue3 可以做到类似于 React Class Component + Hooks + TS 的效果,但是还未达到完美契合的程度,单从这点来说,Vue 算是落后于 React 一点。不过 template 作为渐进式理念的重要组成部分,有自己的优势,也很难被舍弃,个人臆测,未来 Vue 会面临 template 与 TS 的取舍抉择。当然,大牛或许有两全其美的解决方案,目前的趋势还不够明朗,我们且拭目以待。

最后,作为程序员,务实、好学是必要的品质。希望大家不要受限于“门派之见”,仰望星空,集百家之所长,努力提升自己,才是最明智的选择。

“得其大者可以兼其小,未有学其小而能至其大者也” —— 欧阳修

引用

附录

代码文本

React Hooks - Form.tsx

// Form.tsx
import React, { useState, useEffect } from "react";
import { Sex, fetchUserInfo, updateUserInfo } from "../services";

const Form = () => {
  const [name, setName] = useState("");
  const [sex, setSex] = useState(Sex.male);

  useEffect(() => {
    fetchUserInfo("id-xxx").then((res) => {
      setName(res.name);
      setSex(res.sex);
    });
  }, []);

  const handleSubmit = () => {
    const params = { name, sex };
    updateUserInfo(params).then((res) => {
      if (res) alert(JSON.stringify(params));
    });
  };

  return (
    <div>
      <p>
        <div>Name</div>
        <input type="text" value={name} onChange={(e) => setName(e.target.value)} />
      </p>
      <p>
        <div>Sex</div>
        <input type="radio" name="sex" checked={sex === Sex.male} onClick={() => setSex(Sex.male)} />Male
        <input type="radio" name="sex" checked={sex === Sex.female} onClick={() => setSex(Sex.female)} />Female
      </p>
      <p>
        <button onClick={handleSubmit}>Submit</button>
      </p>
    </div>
  );
};

export default Form;

复制代码

React Hooks - Table.tsx

// Table.tsx
import React, { useState, useEffect } from "react";
import { Sex, UserInfo, fetchUserList } from "../services";

const Table = () => {
  const [userList, setUserList] = useState<UserInfo[]>([]);

  useEffect(() => {
    fetchUserList().then((res) => setUserList(res));
  }, []);

  return (
    <table cellPadding={5} cellSpacing={5}>
      <tr>
        <th>Name</th>
        <th>Sex</th>
      </tr>
      {userList.map(({ name, sex }) => (
        <tr key={name}>
          <td>{name}</td>
          <td>{sex === Sex.male ? "Male" : "Female"}</td>
        </tr>
      ))}
    </table>
  );
};

export default Table;
复制代码

Vue 3 + JSX - Form.tsx

// Form.tsx
import { ref, onMounted, defineComponent } from "vue";
import { Sex, fetchUserInfo, updateUserInfo } from "../services";

const Form = defineComponent({
  setup() {
    const nameRef = ref("");
    const sexRef = ref(Sex.male);

    onMounted(() => {
      fetchUserInfo("id-xxx").then((res) => {
        nameRef.value = res.name;
        sexRef.value = res.sex;
      });
    });

    const handleSubmit = () => {
      const params = { name: nameRef.value, sex: sexRef.value };
      updateUserInfo(params).then((res) => {
        if (res) alert(JSON.stringify(params));
      });
    };

    return () => (
      <div>
        <p>
          <div>Name</div>
          <input type="text" value={nameRef.value} onChange={(e) => { nameRef.value = e.target.value; }} />
        </p>
        <p>
          <div>Sex</div>
          <input type="radio" name="sex" checked={sexRef.value === Sex.male} onClick={() => { sexRef.value = Sex.male; }} /> Male
          <input type="radio" name="sex" checked={sexRef.value === Sex.female} onClick={() => { sexRef.value = Sex.female; }} /> Female
        </p>
        <p>
          <button onClick={handleSubmit}>Submit</button>
        </p>
      </div>
    );
  },
});

export default Form;
复制代码

Vue 3 + JSX - Table.tsx

// Table.tsx
import { ref, onMounted, defineComponent } from "vue";
import { Sex, UserInfo, fetchUserList } from "../services";

const Table = defineComponent({
  setup() {
    const userListRef = ref<UserInfo[]>([]);

    onMounted(() => {
      fetchUserList().then((res) => {
        userListRef.value = res;
      });
    });

    return () => (
      <table cellpadding={5} cellspacing={5}>
        <tr>
          <th>Name</th>
          <th>Sex</th>
        </tr>
        {userListRef.value.map(({ name, sex }) => (
          <tr key={name}>
            <td>{name}</td>
            <td>{sex === Sex.male ? "Male" : "Female"}</td>
          </tr>
        ))}
      </table>
    );
  },
});

export default Table;
复制代码
文章分类
前端
文章标签