标题 React for Vue developers
中文标题 写给Vue用户的React指南
出处 https://sebastiandedeyne.com/react-for-vue-developers/
发布日期 2019年5月20日
翻译日期 2021年01月23日
关键词 React, Vue

在过去的三年里,我在不同项目中使用 React 和 Vue 。

上个月我写了一篇文章 《为什么我相比vue更喜欢React》。后来我参加了 Adam Wathan 的《Full Stack Radio》(一个podcast访谈)。谈起了从一个Vue开发者的观点看React。

我们在podcast中谈到了很多,但我们谈到大部分内容,可以从下面的代码片段得到启发,这些代码解释他们的相同和不同。

这篇文章是大部分vue特性的简单总结,我会使用2019年Rect Hooks 来实现它们。

如果我遗漏了什么,或者想看到其他比较,或者你想分享你vue和react的想法,可以看我的 Twitter

Template 模板

react的替代: JSX

Vue 使用HTML字符串和一些特殊的指令Directive 来书写模板。鼓励使用 .vue 后缀的文件来分离模板template、逻辑script、样式style。

基础

  1. <!-- Gretter.vue -->
  2. <template>
  3. <p>Hello, {{ name }}!</p>
  4. </template>
  5. <script>
  6. export default {
  7. props: ['name']
  8. };
  9. </script>

React 使用 JSX ,这是一个 ECMAScript 的一个拓展。

  1. export default function Greeter({ name }) {
  2. return <p>Hello, {name}!</p>;
  3. }

条件渲染

React的选择: && 操作符 Logical && operator、三元表达式Ternary statements、尽早返回early returns

Vue 使用 v-if , v-elsev-else-if 指令进行条件渲染模板的一部分。

  1. <!-- Awesome.vue -->
  2. <template>
  3. <article>
  4. <h1 v-if="awesome">Vue is awesome!</h1>
  5. </article>
  6. </template>
  7. <script>
  8. export default {
  9. props: ['awesome']
  10. };
  11. </script>

React 不支持指令,所以要使用js判断返回你想要的内容。

这个 && 操作符提供了简洁succinct 的方式来书写 if 语句。
React

  1. export default function Awesome({ awesome }) {
  2. return (
  3. <article>
  4. {awesome && <h1>React is awesome!</h1>};
  5. </article>
  6. );
  7. }

如果你需要使用 else,可以使用三元表达式。

  1. export default function Awesome({ awesome }) {
  2. return (
  3. <article>
  4. {awesome ? (
  5. <h1>React is awesome!</h1>
  6. ) : (
  7. <h1>Oh no 😢</h1>
  8. )};
  9. </article>
  10. }

也可以使用 return 提前结束判断,让两个条件分离,

  1. export default function Awesome({ awesome }) {
  2. if (!awesome) {
  3. return (
  4. <article>
  5. <h1>Oh no 😢</h1>
  6. </article>
  7. );
  8. }
  9. return (
  10. <article>
  11. <h1>React is awesome!</h1>
  12. </article>
  13. );
  14. }

列表渲染

React的替代: Array.map

Vue 使用了 v-for 指令来循环数组和对象。

  1. <!-- Recipe.vue -->
  2. <template>
  3. <ul>
  4. <li v-for="(ingredient, index) in ingredients" :key="index">
  5. {{ ingredient }}
  6. </li>
  7. </ul>
  8. </template>
  9. <script>
  10. export default {
  11. props: ['ingredients']
  12. };
  13. </script>

在React中,可以使用js内置的 Arrap.map 来实现数组遍历。

  1. export default function Recipe({ ingredients }) {
  2. return (
  3. <ul>
  4. {ingredients.map((ingredient, index) => (
  5. <li key={index}>{ingredient}</li>
  6. ))}
  7. </ul>
  8. );
  9. }

迭代对象需要一点小技巧,Vue还是使用 v-for 指令来获得对象上的属性key和值value。

  1. <!-- KeyValueList.vue -->
  2. <template>
  3. <ul>
  4. <li v-for="(value, key) in object" :key="key">
  5. {{ key }}: {{ value }}
  6. </li>
  7. </ul>
  8. </template>
  9. <script>
  10. export default {
  11. props: ['object'] // E.g. { a: 'Foo', b: 'Bar' }
  12. };
  13. </script>

我喜欢在React中使用 Object.entries 方法来获取对象里的内容。

  1. export default function KeyValueList({ object }) {
  2. return (
  3. <ul>
  4. {Object.entries(object).map(([key, value]) => (
  5. <li key={key}>{value}</li>
  6. ))}
  7. </ul>
  8. );
  9. }

CSS: class和style绑定

React的选择:手动传递props。

Vue 会自动绑定组件的元素 classstyle

  1. <!-- Post.vue -->
  2. <template>
  3. <article>
  4. <h1>{{ title }}</h1>
  5. </article>
  6. </template>
  7. <script>
  8. export default {
  9. props: ['title'],
  10. };
  11. </script>
  12. <!--
  13. <post
  14. :title="About CSS"
  15. class="margin-bottom"
  16. style="color: red"
  17. />
  18. -->

React中你需要手动传递 classNamestyle 属性。注意 style 在react中必须是一个对象,不支持字符串。

  1. export default function Post({ title, className, style }) {
  2. return (
  3. <article className={className} style={style}>
  4. {title}
  5. </article>
  6. );
  7. }
  8. {/* <Post
  9. title="About CSS"
  10. className="margin-bottom"
  11. style={{ color: 'red' }}
  12. /> */}

如果你想传递所有的属性,可以使用 ... 操作符

  1. export default function Post({ title, ...props }) {
  2. return (
  3. <article {...props}>
  4. {title}
  5. </article>
  6. );
  7. }

如果你怀念 Vue 里的 class api,可以看看 这个库 classnames

Props

React 的选择: Props

React Props 的行为和Vue很相似。一个微小的不同:React组件不会继承未知Unknown 的属性。

  1. <!-- Post.vue -->
  2. <template>
  3. <h1>{{ title }}</h1>
  4. </template>
  5. <script>
  6. export default {
  7. props: ['title'],
  8. };
  9. </script>

react

  1. export default function Post({ title }) {
  2. return <h3>{title}</h3>;
  3. }

Vue 里使用props,可以使用 : 前缀,这是 v-bind 指令的别名。React使用 {} 作为动态值

  1. <!-- Post.vue -->
  2. <template>
  3. <post-title :title="title" />
  4. </template>
  5. <script>
  6. export default {
  7. props: ['title'],
  8. };
  9. </script>

react

  1. export default function Post({ title }) {
  2. return <PostTitle title={title} />;
  3. }

数据Data

基础

React的选择 : useState hook

Vue中 data 用来存储本地组件的状态。

  1. <!-- ButtonCounter.vue -->
  2. <template>
  3. <button @click="count++">
  4. You clicked me {{ count }} times.
  5. </button>
  6. </template>
  7. <script>
  8. export default {
  9. data() {
  10. return {
  11. count: 0
  12. }
  13. }
  14. };
  15. </script>

React 提供了一个 useState 的hook,它返回包含两个元素的数组,对应当前的状态和设置方法。

  1. import { useState } from 'react';
  2. export default function ButtonCounter() {
  3. const [count, setCount] = useState(0);
  4. return (
  5. <button onClick={() => setCount(count + 1)}>
  6. {count}
  7. </button>
  8. );
  9. }

如果要分配多个状态,你有两种方式:

使用多个 useState ,多次调用。

  1. import { useState } from 'react';
  2. export default function ProfileForm() {
  3. const [name, setName] = useState('Sebastian');
  4. const [email, setEmail] = useState('sebastian@spatie.be');
  5. // ...
  6. }

或者使用一个对象。

  1. import { useState } from 'react';
  2. export default function ProfileForm() {
  3. const [values, setValues] = useState({
  4. name: 'Sebastian',
  5. email: 'sebastian@spatie.be'
  6. });
  7. // ...
  8. }

v-model

v-model 是 Vue中的一个指令,用来传递 value 和监听 input 事件。这看起来Vue实现了双向绑定 two-way bindling,但背后的原理还是 传递props,接收event—— props down,events up

  1. <!-- Profile.vue -->
  2. <template>
  3. <input type="text" v-model="name" />
  4. </template>
  5. <script>
  6. export default {
  7. data() {
  8. return {
  9. name: 'Sebastian'
  10. }
  11. }
  12. };
  13. </script>

v-model 展开是下面这样:

  1. <template>
  2. <input
  3. type="text"
  4. :value="name"
  5. @input="name = $event.target.value"
  6. />
  7. </template>

react没有 v-model ,你需要始终这样:

  1. import { useState } from 'react';
  2. export default function Profile() {
  3. const [name, setName] = useState('Sebastian');
  4. return (
  5. <input
  6. type="text"
  7. value={name}
  8. onChange={event => setName(event.target.name)}
  9. />
  10. );
  11. }

计算属性Computed properties

React 的选择: 变量Variable、可选包装 useMemo

Vue 使用计算属性有两个原因:

  • 避免在HTML中混合逻辑Mixing logic 和标签
  • 在一个组件实例中缓存复杂的计算操作

如果不使用计算属性:
(html中含有逻辑和标签混杂)

  1. <!-- ReversedMessage.vue -->
  2. <template>
  3. <p>{{ message.split('').reverse().join('') }}</p>
  4. </template>
  5. <script>
  6. export default {
  7. props: ['message']
  8. };
  9. </script>

React

  1. export default function ReversedMessage({ message }) {
  2. return <p>{message.split('').reverse().join('')}</p>;
  3. }

使用React,你可以从模板中提取一个计算的结果作为一个变量。

  1. <!-- ReversedMessage.vue -->
  2. <template>
  3. <p>{{ reversedMessage }}</p>
  4. </template>
  5. <script>
  6. export default {
  7. props: ['message'],
  8. computed: {
  9. reversedMessage() {
  10. return this.message.split('').reverse().join('');
  11. }
  12. }
  13. };
  14. </script>

react

  1. export default function ReversedMessage({ message }) {
  2. const reversedMessage = message.split('').reverse().join('');
  3. return <p>{reversedMessage}</p>;
  4. }

如果考虑性能,计算过程可以用 useMemo hook,它需要:用来返回一个计算结果的回调,一个数组依赖。

下面的例子中, reverseMessage 会在 依赖项 message 变化之后重新计算。

  1. import { useMemo } from 'react';
  2. export default function ReversedMessage({ message }) {
  3. const reversedMessage = useMemo(() => {
  4. return message.split('').reverse().join('');
  5. }, [message]);
  6. return <p>{reversedMessage}</p>;
  7. }

方法Methods

React 的替代:函数Function

Vue 有一个 methods 选项,用来声明组件用到的方法。

  1. <!-- ImportantButton.vue -->
  2. <template>
  3. <button onClick="doSomething">
  4. Do something!
  5. </button>
  6. </template>
  7. <script>
  8. export default {
  9. methods: {
  10. doSomething() {
  11. // ...
  12. }
  13. }
  14. };
  15. </script>

在React中,你可以在组件里声明一个简单的函数。

  1. export default function ImportantButton() {
  2. function doSomething() {
  3. // ...
  4. }
  5. return (
  6. <button onClick={doSomething}>
  7. Do something!
  8. </button>
  9. );
  10. }

事件Events

React的替代: 回调props

事件本质上是子组件触发了某些事情的一个回调。Vue 把事件视为一等公民,所以你可以使用 v-on 或者 @ 来监听:

  1. <!-- PostForm.vue -->
  2. <template>
  3. <form>
  4. <button type="button" @click="$emit('save')">
  5. Save
  6. </button>
  7. <button type="button" @click="$emit('publish')">
  8. Publish
  9. </button>
  10. </form>
  11. </template>

React里的事件没有特殊的含义,只是子组件的props回调。

  1. export default function PostForm({ onSave, onPublish }) {
  2. return (
  3. <form>
  4. <button type="button" onClick={onSave}>
  5. Save
  6. </button>
  7. <button type="button" onClick={onPublish}>
  8. Publish
  9. </button>
  10. </form>
  11. );
  12. }

事件修饰

React:可以考虑HOC高阶组件

Vue有一些修饰符 prevent stop 来改变事件的行为,而不需要修改处理逻辑。

  1. <!-- AjaxForm.vue -->
  2. <template>
  3. <form @submit.prevent="submitWithAjax">
  4. <!-- ... -->
  5. </form>
  6. </template>
  7. <script>
  8. export default {
  9. methods: {
  10. submitWithAjax() {
  11. // ...
  12. }
  13. }
  14. };
  15. </script>

React里没有修饰符语法。阻止默认事件和停止冒泡通常需要在回调里处理。

  1. export default function AjaxForm() {
  2. function submitWithAjax(event) {
  3. event.preventDefault();
  4. // ...
  5. }
  6. return (
  7. <form onSubmit={submitWithAjax}>
  8. {/* ... */}
  9. </form>
  10. );
  11. }

如果你真的想有一些类似修饰符的东西,你可以考虑高阶组件HOC

  1. function prevent(callback) {
  2. return (event) => {
  3. event.preventDefault();
  4. callback(event);
  5. };
  6. }
  7. export default function AjaxForm() {
  8. function submitWithAjax(event) {
  9. // ...
  10. }
  11. return (
  12. <form onSubmit={prevent(submitWithAjax)}>
  13. {/* ... */}
  14. </form>
  15. );
  16. }

生命周期Lifecycle methods

React的替代: useEffect hook。

免责声明 在类组件Class Component 中,React和Vue都有很相似的生命周期api。使用 useEffect hooks,可以解决大多数生命周期相关的问题。两者是完全不同的思路,因此很难进行比较。这部分内容只是使用几个例子,用来说明效果。

使用生命周期方法常用来建立和销毁第三方库。

  1. <template>
  2. <input type="text" ref="input" />
  3. </template>
  4. <script>
  5. import DateTimePicker from 'awesome-date-time-picker';
  6. export default {
  7. mounted() {
  8. this.dateTimePickerInstance =
  9. new DateTimePicker(this.$refs.input);
  10. },
  11. beforeDestroy() {
  12. this.dateTimePickerInstance.destroy();
  13. }
  14. };
  15. </script>

React 使用 useEffect ,你可以声明一个 副作用 side effect 需要渲染之后运行。使用 useEffect 的回调会在完成之后触发。在这个例子中,副作用发生在组件销毁之后。

  1. import { useEffect, useRef } from 'react';
  2. import DateTimePicker from 'awesome-date-time-picker';
  3. export default function Component() {
  4. const dateTimePickerRef = useRef();
  5. useEffect(() => {
  6. const dateTimePickerInstance =
  7. new DateTimePicker(dateTimePickerRef.current);
  8. return () => {
  9. dateTimePickerInstance.destroy();
  10. };
  11. }, []);
  12. return <input type="text" ref={dateTimePickerRef} />;
  13. }

这和Vue在 mounted 中注册一个 beforeDestroy 监听很相似。

  1. <script>
  2. export default {
  3. mounted() {
  4. const dateTimePicker =
  5. new DateTimePicker(this.$refs.input);
  6. this.$once('hook:beforeDestroy', () => {
  7. dateTimePicker.destroy();
  8. });
  9. }
  10. };
  11. </script>

useEffectuseMemo 一样,接收一个依赖数组作为第二个参数。

如果不设置依赖,会在每次渲染之后触发,会在下次渲染之前清除。这个功能和Vue的 mounted updated beforeUpdate beforeDestroy 很像。

  1. useEffect(() => {
  2. // Happens after every render 每次render之后触发
  3. return () => {
  4. // Optional; clean up before next render 可选,下次渲染之前清楚
  5. };
  6. });

如果你指定了没有依赖项,触发条件只有组件第一次渲染的时候,因为它没有条件触发。这个功能和Vue的 moutnedbeforeDestroyed 很像。

  1. useEffect(() => {
  2. // Happens on mount
  3. return () => {
  4. // Optional; clean up before unmount
  5. };
  6. }, []);

如果执行了有一个依赖,会在依赖项发生变化之后触发。我们会在后续的 watchers 章节再回顾。

  1. const [count, setCount] = useState(0);
  2. useEffect(() => {
  3. // Happens when `count` changes
  4. return () => {
  5. // Optional; clean up when `count` changed
  6. };
  7. }, [count]);

不要考虑把生命周期的方法全都转换成 useEffect 。最好重新声明一组副作用。

正如 Ryan Florence 提到:

问题不是“副作用何时被触发”,而是:“这个效果和哪个状态同步” useEffect(fn) // all state useEffect(fn, []) // no state useEffect(fn, [these, states]) by @ryanflorence on Twitter

侦听器Watchers

React的替代: useEfeect hook

watcher 概念上和生命周期的方法很像:当 X 发生了,就做 Y事情。React里没有Watcher,但可以使用 useEffect 实现相同的效果。

  1. <!-- AjaxToggle.vue -->
  2. <template>
  3. <input type="checkbox" v-model="checked" />
  4. </template>
  5. <script>
  6. export default {
  7. data() {
  8. return {
  9. checked: false
  10. }
  11. },
  12. watch: {
  13. checked(checked) {
  14. syncWithServer(checked);
  15. }
  16. },
  17. methods: {
  18. syncWithServer(checked) {
  19. // ...
  20. }
  21. }
  22. };
  23. </script>

React

  1. import { useEffect, useState } from 'react';
  2. export default function AjaxToggle() {
  3. const [checked, setChecked] = useState(false);
  4. function syncWithServer(checked) {
  5. // ...
  6. }
  7. useEffect(() => {
  8. syncWithServer(checked);
  9. }, [checked]);
  10. return (
  11. <input
  12. type="checkbox"
  13. checked={checked}
  14. onChange={() => setChecked(!checked)}
  15. />
  16. );
  17. }

注意 useEffect 在第一次渲染时候会被触发,这和Vue在watcher中设置 immediate 选项一样。

如果你不想在第一次渲染触发,你可以创建一个 ref 变量来控制第一次渲染是否触发。(就是设置一个flag)

  1. import { useEffect, useRef, useState } from 'react';
  2. export default function AjaxToggle() {
  3. const [checked, setChecked] = useState(false);
  4. const firstRender = useRef(true);
  5. function syncWithServer(checked) {
  6. // ...
  7. }
  8. useEffect(() => {
  9. if (firstRender.current) {
  10. firstRender.current = false;
  11. return;
  12. }
  13. syncWithServer(checked);
  14. }, [checked]);
  15. return (
  16. <input
  17. type="checkbox"
  18. checked={checked}
  19. onChange={() => setChecked(!checked)}
  20. />
  21. );
  22. }

插槽和作用域插槽Slots & scoped slots

React的替代:JSX 的props 或者 render props

如果你在一个组件内部渲染一个其他模板,React使用 children 属性。

Vue可以声明一个 <slot/> 表示内部的内容。React你可以渲染 children

  1. <!-- RedParagraph.vue -->
  2. <template>
  3. <p style="color: red">
  4. <slot />
  5. </p>
  6. </template>

React

  1. export default function RedParagraph({ children }) {
  2. return (
  3. <p style={{ color: 'red' }}>
  4. {children}
  5. </p>
  6. );
  7. }

React里的 插槽是指props,我们不需要在模板中声明,只需要接收 jsx的props,然后在render时候进行渲染。

  1. <!-- Layout.vue -->
  2. <template>
  3. <div class="flex">
  4. <section class="w-1/3">
  5. <slot name="sidebar" />
  6. </section>
  7. <main class="flex-1">
  8. <slot />
  9. </main>
  10. </div>
  11. </template>
  12. <!-- In use: -->
  13. <layout>
  14. <template #sidebar>
  15. <nav>...</nav>
  16. </template>
  17. <template #default>
  18. <post>...</post>
  19. </template>
  20. </layout>

React

  1. export default function RedParagraph({ sidebar, children }) {
  2. return (
  3. <div className="flex">
  4. <section className="w-1/3">
  5. {sidebar}
  6. </section>
  7. <main className="flex-1">
  8. {children}
  9. </main>
  10. </div>
  11. );
  12. }
  13. // In use:
  14. return (
  15. <Layout sidebar={<nav>...</nav>}>
  16. <Post>...</Post>
  17. </Layout>
  18. );

Vue 有作用域插槽scoped slots ,给将要渲染的slot传递数据。关键点:将要渲染

常规的slot是在父组件渲染之前就渲染完了。父组件再决定如何处理渲染的片段。

作用域插槽不能在父组件之前渲染,因为他们依赖父组件传递的数据,因此作用域插槽延迟执行。

在js中延迟执行某些事情很简单:用function包裹然后需要的时候再调用。如果你需要在React中使用作用域插槽,传递一个负责渲染的函数就可以。

我们也可以使用 chidren ,或者其他prop, 传递一个函数而不是声明一个模板 ??

  1. <!-- CurrentUser.vue -->
  2. <template>
  3. <span>
  4. <slot :user="user" />
  5. </span>
  6. </template>
  7. <script>
  8. export default {
  9. inject: ['user']
  10. };
  11. </script>
  12. <!-- In use: -->
  13. <template>
  14. <current-user>
  15. <template #default="{ user }">
  16. {{ user.firstName }}
  17. </template>
  18. </current-user>
  19. </template>

React

  1. import { useContext } from 'react';
  2. import UserContext from './UserContext';
  3. export default function CurrentUser({ children }) {
  4. const { user } = useContext(UserContext);
  5. return (
  6. <span>
  7. {children(user)}
  8. </span>
  9. );
  10. }
  11. // In use:
  12. return (
  13. <CurrentUser>
  14. {user => user.firstName}
  15. </CurrentUser>
  16. );

Provide/inject

React 的替代: createContextuseContext hook

provide/inject 允许一个组件和所有的子组件共享状态。React有一个相同的特性:context

  1. <!-- MyProvider.vue -->
  2. <template>
  3. <div><slot /></div>
  4. </template>
  5. <script>
  6. export default {
  7. provide: {
  8. foo: 'bar'
  9. },
  10. };
  11. </script>
  12. <!-- Must be rendered inside a MyProvider instance: -->
  13. <template>
  14. <p>{{ foo }}</p>
  15. </template>
  16. <script>
  17. export default {
  18. inject: ['foo']
  19. };
  20. </script>

React

  1. import { createContext, useContext } from 'react';
  2. const fooContext = createContext('foo');
  3. function MyProvider({ children }) {
  4. return (
  5. <FooContext.Provider value="foo">
  6. {children}
  7. </FooContext.Provider>
  8. );
  9. }
  10. // Must be rendered inside a MyProvider instance:
  11. function MyConsumer() {
  12. const foo = useContext(FooContext);
  13. return <p>{foo}</p>;
  14. }

自定义指令Custom directives

React替代: Components 组件

React里没有指令。但是很多靠指令解决的问题可以使用 组件来解决。

  1. <div v-tooltip="Hello!">
  2. <p>...</p>
  3. </div>

React

  1. return (
  2. <Tooltip text="Hello">
  3. <div>
  4. <p>...</p>
  5. </div>
  6. </Tooltip>
  7. );

过渡效果Transitions

React的替代:第三方库。

React没有内置的过渡工具。可以考虑使用 react-transition-group,没有提供动画,但是可以通过class类名来编排。

如果你想要一个提供更多帮助的库,考虑 pose ,(注:React Pose for web has been deprecated by Framer Motion.)