[译]写给Vue用户的React指南

529 阅读7分钟
标题React for Vue developers
中文标题写给Vue用户的React指南
出处sebastiandedeyne.com/react-for-v…
首发www.yuque.com/freeroadmap…
发布日期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。

基础

<!-- Gretter.vue -->
<template>
  <p>Hello, {{ name }}!</p>
</template>
<script>
export default {
  props: ['name']
};
</script>

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

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

条件渲染

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

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

<!-- Awesome.vue -->

<template>
  <article>
    <h1 v-if="awesome">Vue is awesome!</h1>
  </article>
</template>

<script>
export default {
  props: ['awesome']
};
</script>

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

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

export default function Awesome({ awesome }) {
  return (
    <article>
      {awesome && <h1>React is awesome!</h1>};
    </article>
  );
}

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

export default function Awesome({ awesome }) {
  return (
    <article>
      {awesome ? (
        <h1>React is awesome!</h1>
      ) : (
        <h1>Oh no 😢</h1>
      )};
    </article>
}

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

export default function Awesome({ awesome }) {
  if (!awesome) {
    return (
      <article>
        <h1>Oh no 😢</h1>
      </article>
    );
  }

  return (
    <article>
      <h1>React is awesome!</h1>
    </article>
  );
}

列表渲染

React的替代: Array.map

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

<!-- Recipe.vue -->

<template>
  <ul>
    <li v-for="(ingredient, index) in ingredients" :key="index">
      {{ ingredient }}
    </li>
  </ul>
</template>

<script>
export default {
  props: ['ingredients']
};
</script>

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

export default function Recipe({ ingredients }) {
  return (
    <ul>
      {ingredients.map((ingredient, index) => (
        <li key={index}>{ingredient}</li>
      ))}
    </ul>
  );
}

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

<!-- KeyValueList.vue -->

<template>
  <ul>
    <li v-for="(value, key) in object" :key="key">
      {{ key }}: {{ value }}
    </li>
  </ul>
</template>

<script>
export default {
  props: ['object'] // E.g. { a: 'Foo', b: 'Bar' }
};
</script>

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

export default function KeyValueList({ object }) {
  return (
    <ul>
      {Object.entries(object).map(([key, value]) => (
        <li key={key}>{value}</li>
      ))}
    </ul>
  );
}

CSS: class和style绑定

React的选择:手动传递props。

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

<!-- Post.vue -->

<template>
  <article>
    <h1>{{ title }}</h1>
  </article>
</template>

<script>
export default {
  props: ['title'],
};
</script>

<!--
<post
  :title="About CSS"
  class="margin-bottom"
  style="color: red"
/>
-->

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

export default function Post({ title, className, style }) {
  return (
    <article className={className} style={style}>
      {title}
    </article>
  );
}

{/* <Post
  title="About CSS"
  className="margin-bottom"
  style={{ color: 'red' }}
/> */}

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

export default function Post({ title, ...props }) {
  return (
    <article {...props}>
      {title}
    </article>
  );
}

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

Props

React 的选择: Props

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

<!-- Post.vue -->

<template>
  <h1>{{ title }}</h1>
</template>

<script>
export default {
  props: ['title'],
};
</script>

react

export default function Post({ title }) {
  return <h3>{title}</h3>;
}

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

<!-- Post.vue -->

<template>
  <post-title :title="title" />
</template>

<script>
export default {
  props: ['title'],
};
</script>

react

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

数据Data

基础

React的选择 : useState hook

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

<!-- ButtonCounter.vue -->

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

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
};
</script>

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

import { useState } from 'react';

export default function ButtonCounter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

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

使用多个 useState ,多次调用。

import { useState } from 'react';

export default function ProfileForm() {
  const [name, setName] = useState('Sebastian');
  const [email, setEmail] = useState('sebastian@spatie.be');

  // ...
}

或者使用一个对象。

import { useState } from 'react';

export default function ProfileForm() {
  const [values, setValues] = useState({
    name: 'Sebastian',
    email: 'sebastian@spatie.be'
  });

  // ...
}

v-model

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

<!-- Profile.vue -->

<template>
  <input type="text" v-model="name" />
</template>

<script>
export default {
  data() {
    return {
      name: 'Sebastian'
    }
  }
};
</script>

v-model 展开是下面这样:

<template>
  <input
    type="text"
    :value="name"
    @input="name = $event.target.value"
  />
</template>

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

import { useState } from 'react';

export default function Profile() {
  const [name, setName] = useState('Sebastian');

  return (
    <input
      type="text"
      value={name}
      onChange={event => setName(event.target.name)}
    />
  );
}

计算属性Computed properties

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

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

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

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

<!-- ReversedMessage.vue -->

<template>
  <p>{{ message.split('').reverse().join('') }}</p>
</template>

<script>
export default {
  props: ['message']
};
</script>

React

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

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

<!-- ReversedMessage.vue -->

<template>
  <p>{{ reversedMessage }}</p>
</template>

<script>
export default {
  props: ['message'],

  computed: {
    reversedMessage() {
      return this.message.split('').reverse().join('');
    }
  }
};
</script>

react

export default function ReversedMessage({ message }) {
  const reversedMessage = message.split('').reverse().join('');

  return <p>{reversedMessage}</p>;
}

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

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

import { useMemo } from 'react';

export default function ReversedMessage({ message }) {
  const reversedMessage = useMemo(() => {
    return message.split('').reverse().join('');
  }, [message]);

  return <p>{reversedMessage}</p>;
}

方法Methods

React 的替代:函数Function

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

<!-- ImportantButton.vue -->

<template>
  <button onClick="doSomething">
    Do something!
  </button>
</template>

<script>
export default {
  methods: {
    doSomething() {
      // ...
    }
  }
};
</script>

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

export default function ImportantButton() {
  function doSomething() {
    // ...
  }

  return (
    <button onClick={doSomething}>
      Do something!
    </button>
  );
}

事件Events

React的替代: 回调props

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

<!-- PostForm.vue -->

<template>
  <form>
    <button type="button" @click="$emit('save')">
      Save
    </button>
    <button type="button" @click="$emit('publish')">
      Publish
    </button>
  </form>
</template>

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

export default function PostForm({ onSave, onPublish }) {
  return (
    <form>
      <button type="button" onClick={onSave}>
        Save
      </button>
      <button type="button" onClick={onPublish}>
        Publish
      </button>
    </form>
  );
}

事件修饰

React:可以考虑HOC高阶组件

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

<!-- AjaxForm.vue -->

<template>
  <form @submit.prevent="submitWithAjax">
    <!-- ... -->
  </form>
</template>

<script>
export default {
  methods: {
    submitWithAjax() {
      // ...
    }
  }
};
</script>

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

export default function AjaxForm() {
  function submitWithAjax(event) {
    event.preventDefault();
    // ...
  }

  return (
    <form onSubmit={submitWithAjax}>
      {/* ... */}
    </form>
  );
}

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

function prevent(callback) {
  return (event) => {
      event.preventDefault();
      callback(event);
  };
}

export default function AjaxForm() {
  function submitWithAjax(event) {
    // ...
  }

  return (
    <form onSubmit={prevent(submitWithAjax)}>
      {/* ... */}
    </form>
  );
}

生命周期Lifecycle methods

React的替代: useEffect hook。

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

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

<template>
  <input type="text" ref="input" />
</template>

<script>
import DateTimePicker from 'awesome-date-time-picker';

export default {
  mounted() {
   this.dateTimePickerInstance =
     new DateTimePicker(this.$refs.input);
  },

  beforeDestroy() {
    this.dateTimePickerInstance.destroy();
  }
};
</script>

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

import { useEffect, useRef } from 'react';
import DateTimePicker from 'awesome-date-time-picker';

export default function Component() {
  const dateTimePickerRef = useRef();

  useEffect(() => {
    const dateTimePickerInstance =
      new DateTimePicker(dateTimePickerRef.current);

    return () => {
      dateTimePickerInstance.destroy();
    };
  }, []);

  return <input type="text" ref={dateTimePickerRef} />;
}

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

<script>
export default {
  mounted() {
    const dateTimePicker =
      new DateTimePicker(this.$refs.input);

    this.$once('hook:beforeDestroy', () => {
      dateTimePicker.destroy();
    });
  }
};
</script>

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

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

useEffect(() => {
    // Happens after every render 每次render之后触发

    return () => {
        // Optional; clean up before next render 可选,下次渲染之前清楚
    };
});

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

useEffect(() => {
    // Happens on mount

    return () => {
        // Optional; clean up before unmount
    };
}, []);

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

const [count, setCount] = useState(0);

useEffect(() => {
    // Happens when `count` changes

    return () => {
        // Optional; clean up when `count` changed
    };
}, [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 实现相同的效果。

<!-- AjaxToggle.vue -->

<template>
  <input type="checkbox" v-model="checked" />
</template>

<script>
export default {
  data() {
    return {
      checked: false
    }
  },

  watch: {
    checked(checked) {
      syncWithServer(checked);
    }
  },

  methods: {
    syncWithServer(checked) {
      // ...
    }
  }
};
</script>

React

import { useEffect, useState } from 'react';

export default function AjaxToggle() {
  const [checked, setChecked] = useState(false);

  function syncWithServer(checked) {
    // ...
  }

  useEffect(() => {
    syncWithServer(checked);
  }, [checked]);

  return (
    <input
      type="checkbox"
      checked={checked}
      onChange={() => setChecked(!checked)}
    />
  );
}

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

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

import { useEffect, useRef, useState } from 'react';

export default function AjaxToggle() {
  const [checked, setChecked] = useState(false);
  const firstRender = useRef(true);

  function syncWithServer(checked) {
    // ...
  }

  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false;
      return;
    }
    syncWithServer(checked);
  }, [checked]);

  return (
    <input
      type="checkbox"
      checked={checked}
      onChange={() => setChecked(!checked)}
    />
  );
}

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

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

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

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

<!-- RedParagraph.vue -->

<template>
  <p style="color: red">
    <slot />
  </p>
</template>

React

export default function RedParagraph({ children }) {
  return (
    <p style={{ color: 'red' }}>
      {children}
    </p>
  );
}

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

<!-- Layout.vue -->

<template>
  <div class="flex">
    <section class="w-1/3">
        <slot name="sidebar" />
    </section>
    <main class="flex-1">
        <slot />
    </main>
  </div>
</template>

<!-- In use: -->

<layout>
  <template #sidebar>
    <nav>...</nav>
  </template>
  <template #default>
    <post>...</post>
  </template>
</layout>

React

export default function RedParagraph({ sidebar, children }) {
  return (
    <div className="flex">
      <section className="w-1/3">
        {sidebar}
      </section>
      <main className="flex-1">
        {children}
      </main>
    </div>
  );
}

// In use:

return (
  <Layout sidebar={<nav>...</nav>}>
    <Post>...</Post>
  </Layout>
);

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

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

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

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

我们也可以使用 chidren ,或者其他prop

<!-- CurrentUser.vue -->

<template>
  <span>
    <slot :user="user" />
  </span>
</template>

<script>
export default {
  inject: ['user']
};
</script>

<!-- In use: -->

<template>
  <current-user>
    <template #default="{ user }">
      {{ user.firstName }}
    </template>
  </current-user>
</template>

React

import { useContext } from 'react';
import UserContext from './UserContext';

export default function CurrentUser({ children }) {
  const { user } = useContext(UserContext);

  return (
    <span>
      {children(user)}
    </span>
  );
}

// In use:

return (
  <CurrentUser>
    {user => user.firstName}
  </CurrentUser>
);

Provide/inject

React 的替代: createContextuseContext hook

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

<!-- MyProvider.vue -->

<template>
  <div><slot /></div>
</template>

<script>
export default {
  provide: {
    foo: 'bar'
  },
};
</script>

<!-- Must be rendered inside a MyProvider instance: -->

<template>
  <p>{{ foo }}</p>
</template>

<script>
export default {
  inject: ['foo']
};
</script>

React

import { createContext, useContext } from 'react';

const fooContext = createContext('foo');

function MyProvider({ children }) {
  return (
    <FooContext.Provider value="foo">
      {children}
    </FooContext.Provider>
  );
}

// Must be rendered inside a MyProvider instance:

function MyConsumer() {
  const foo = useContext(FooContext);

  return <p>{foo}</p>;
}

自定义指令Custom directives

React替代: Components 组件

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

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

React

return (
  <Tooltip text="Hello">
    <div>
      <p>...</p>
    </div>
  </Tooltip>
);

过渡效果Transitions

React的替代:第三方库。

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

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

完。