标题 | 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。
基础
<!-- 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-else
和 v-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 会自动绑定组件的元素 class
和 style
<!-- 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中你需要手动传递 className
和 style
属性。注意 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>
useEffect
和 useMemo
一样,接收一个依赖数组作为第二个参数。
如果不设置依赖,会在每次渲染之后触发,会在下次渲染之前清除。这个功能和Vue的 mounted
updated
beforeUpdate
beforeDestroy
很像。
useEffect(() => {
// Happens after every render 每次render之后触发
return () => {
// Optional; clean up before next render 可选,下次渲染之前清楚
};
});
如果你指定了没有依赖项,触发条件只有组件第一次渲染的时候,因为它没有条件触发。这个功能和Vue的 moutned
和 beforeDestroyed
很像。
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 的替代: createContext
和 useContext
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.)