React.js和Vue.js都是很好的框架。而且Next.js和Nuxt.js甚至将它们带入了一个新的高度,这有助于我们以更少的配置和更好的可维护性来创建应用程序。但是,如果你必须经常在框架之间切换,在深入探讨另一个框架之后,你可能会轻易忘记另一个框架中的语法。在本文中,我总结了这些框架的基本语法和方案,然后并排列出。我希望这可以帮助我们尽快掌握语法,不过限于篇幅,这篇文章只比较React.js和Vue.js,下一篇再谈Next.js个Nuxt.js。

Github:https://github.com/oahehc/react-vue-comparison

Render

React.js

1
ReactDOM.render(<App />, document.getElementById("root"));

Vue.js

1
2
3
new Vue({
render: (h) => h(App),
}).$mount("#root");

基本组件

React.js

Class component

1
2
3
4
5
class MyReactComponent extends React.Component {
render() {
return <h1>Hello world</h1>;
}
}

Function component

1
2
3
function MyReactComponent() {
return <h1>Hello world</h1>;
}

Vue.js

1
2
3
4
5
6
7
8
<template>
<h1>Hello World</h1>
</template>
<script>
export default {
name: "MyVueComponent",
};
</script>

Prop

React.js

1
2
3
4
5
6
7
8
9
10
11
12
13
function MyReactComponent(props) {
const { name, mark } = props;
return <h1>Hello {name}{mark}</h1>;
}
MyReactComponent.propTypes = {
name: PropTypes.string.isRequired,
mark: PropTypes.string,
}
MyReactComponent.defaultProps = {
mark: '!',
}
...
<MyReactComponent name="world">

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<h1>Hello {{ name }}</h1>
</template>
<script>
export default {
name: "MyVueComponent",
props: {
name: {
type: String,
required: true,
},
mark: {
type: String,
default: "!",
},
},
};
</script>
...
<MyVueComponent name="World" />

事件绑定

React.js

Class component

1
2
3
4
5
6
7
8
class MyReactComponent extends React.Component {
save = () => {
console.log("save");
};
render() {
return <button onClick={this.save}>Save</button>;
}
}

Function component

1
2
3
4
5
6
function MyReactComponent() {
const save = () => {
console.log("save");
};
return <button onClick={save}>Save</button>;
}

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<button @click="save()">Save</button>
</template>
<script>
export default {
methods: {
save() {
console.log("save");
},
},
};
</script>

自定义事件

React.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function MyItem({ item, handleDelete }) {
return <button onClick={() => handleDelete(item)}>{item.name}</button>;
/*
* 应用useCallback钩子来防止在每次渲染时生成新的函数。
*
* const handleClick = useCallback(() => handleDelete(item), [item, handleDelete]);
*
* return <button onClick={handleClick}>{item.name}</button>;
*/
}
...
function App() {
const handleDelete = () => { ... }
return <MyItem item={...} handleDelete={handleDelete} />
}

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<button @click="deleteItem()">{{item.name}}</button>
</template>
<script>
export default {
name: "my-item",
props: {
item: Object,
},
methods: {
deleteItem() {
this.$emit("delete", this.item);
},
},
};
</script>
...
<template>
<MyItem :item="item" @delete="handleDelete" />
</template>
<script>
export default {
components: {
MyItem,
},
methods: {
handleDelete(item) { ... }
},
};
</script>

State

React.js

Class component

1
2
3
4
5
6
7
8
class MyReactComponent extends React.Component {
state = {
name: 'world,
}
render() {
return <h1>Hello { this.state.name }</h1>;
}
}

Function component

1
2
3
4
function MyReactComponent() {
const [name, setName] = useState("world");
return <h1>Hello {name}</h1>;
}

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<h1>Hello {{ name }}</h1>
<!-- 使用组件状态作为prop -->
<my-vue-component :name="name">
</template>
<script>
export default {
data() {
return { name: "world" };
},
};
</script>

Change-State

React.js

Class component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyReactComponent extends React.Component {
state = {
count: 0,
};
increaseCount = () => {
this.setState({ count: this.state.count + 1 });
// 在更新之前获取当前状态,以确保我们没有使用陈旧的值
// this.setState(currentState => ({ count: currentState.count + 1 }));
};
render() {
return (
<div>
<span>{this.state.count}</span>
<button onClick={this.increaseCount}>Add</button>
</div>
);
}
}

Function component

1
2
3
4
5
6
7
8
9
10
11
12
13
function MyReactComponent() {
const [count, setCount] = useState(0);
const increaseCount = () => {
setCount(count + 1);
// setCount(currentCount => currentCount + 1);
};
return (
<div>
<span>{count}</span>
<button onClick={increaseCount}>Add</button>
</div>
);
}

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<span>{{count}}</span>
<button @click="increaseCount()">Add</button>
</div>
</template>
<script>
export default {
data() {
return { count: 0 };
},
methods: {
increaseCount() {
this.count = this.count + 1;
},
},
};
</script>

双向绑定 (仅Vue.js)

React.js

React没有双向绑定,因此我们需要自己处理数据流

1
2
3
4
5
6
7
8
9
10
function MyReactComponent() {
const [content, setContent] = useState("");
return (
<input
type="text"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
);
}

Vue.js

1
2
3
4
5
6
7
8
9
10
<template>
<input type="text" v-model="content" />
</template>
<script>
export default {
data() {
return { content: "" };
},
};
</script>

计算属性

React.js

React.js没有计算属性,但我们可以通过react hook轻松实现

1
2
3
4
5
6
7
8
function DisplayName({ firstName, lastName }) {
const displayName = useMemo(() => {
return `${firstName} ${lastName}`;
}, [firstName, lastName]);
return <div>{displayName}</div>;
}
...
<DisplayName firstName="Hello" lastName="World" />

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>{{displayName}}</div>
</template>
<script>
export default {
name: "display-name",
props: {
firstName: String,
lastName: String,
},
computed: {
displayName: function () {
return `${this.firstName} ${this.lastName}`;
},
},
};
</script>
...
<DisplayName firstName="Hello" lastName="World" />

Watch

React.js

React.js没有 watch 属性,但是我们可以通过react hook轻松实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function MyReactComponent() {
const [count, setCount] = useState(0);
const increaseCount = () => {
setCount((currentCount) => currentCount + 1);
};
useEffect(() => {
localStorage.setItem("my_count", newCount);
}, [count]);
return (
<div>
<span>{count}</span>
<button onClick={increaseCount}>Add</button>
</div>
);
}

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<span>{{count}}</span>
<button @click="increaseCount()">Add</button>
</div>
</template>
<script>
export default {
data() {
return { count: 0 };
},
methods: {
increaseCount() {
this.count = this.count + 1;
},
},
watch: {
count: function (newCount, oldCount) {
localStorage.setItem("my_count", newCount);
},
},
};
</script>

Children-and-Slot

React.js

1
2
3
4
5
function MyReactComponent({ children }) {
return <div>{children}</div>;
}
...
<MyReactComponent>Hello World</MyReactComponent>

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<slot />
</div>
</template>
<script>
export default {
name: "my-vue-component",
};
</script>
...
<MyVueComponent>Hello World</MyVueComponent>

渲染HTML

React.js

1
2
3
function MyReactComponent() {
return <div dangerouslySetInnerHTML={{ __html: "<pre>...</pre>" }} />;
}

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div v-html="html"></div>
</template>
<script>
export default {
data() {
return {
html: "<pre>...</pre>",
};
},
};
</script>

条件渲染

React.js

1
2
3
4
5
6
7
8
9
function MyReactComponent() {
const [isLoading, setLoading] = useState(true);
return (
<div>
{isLoading && <span>Loading...</span>}
{isLoading ? <div>is loading</div> : <div>is loaded</div>}
</div>
);
}

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<!--v-show: 总是渲染,但根据条件更改CSS-->
<span v-show="loading">Loading...</span>
<div>
<div v-if="loading">is loading</div>
<div v-else>is loaded</div>
</div>
</div>
</template>
<script>
export default {
data() {
return { loading: true };
},
};
</script>

列表渲染

React.js

1
2
3
4
5
6
7
8
9
10
11
function MyReactComponent({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name}: {item.desc}
</li>
))}
</ul>
);
}

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<ul>
<li v-for="item in items" :key="item.id">
{{item.name}}: {{item.desc}}
</li>
</ul>
</template>
<script>
export default {
props: {
items: Array,
},
};
</script>

Render-Props

React.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Modal({children, isOpen}) {
const [isModalOpen, toggleModalOpen] = useState(isOpen);
return (
<div className={isModalOpen ? 'open' : 'close'}>
{type children === 'function' ? children(toggleModalOpen) : children}
</div>)
;
}
Modal.propTypes = {
isOpen: PropTypes.bool,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
}
Modal.defaultProps = {
isOpen: false,
}
...
<Modal isOpen>
{(toggleModalOpen) => {
<div>
<div>...</div>
<button onClick={() => toggleModalOpen(false)}>Cancel</button>
</div>
}}
</Modal>

Vue.js(slot)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<div v-show="isModalOpen">
<slot v-bind:toggleModal="toggleModalOpen" />
</div>
</template>
<script>
export default {
name: "modal",
props: {
isOpen: {
type: Boolean,
default: false,
},
},
data() {
return {
isModalOpen: this.isOpen,
};
},
methods: {
toggleModalOpen(state) {
this.isModalOpen = state;
},
},
};
</script>
...
<Modal isOpen>
<template v-slot="slotProps">
<div>...</div>
<button @click="slotProps.toggleModal(false)">Close</button>
</template>
</Modal>

生命周期

React.js

Class component

1
2
3
4
5
6
7
8
9
10
11
class MyReactComponent extends React.Component {
static getDerivedStateFromProps(props, state) {}
componentDidMount() {}
shouldComponentUpdate(nextProps, nextState) {}
getSnapshotBeforeUpdate(prevProps, prevState) {}
componentDidUpdate(prevProps, prevState) {}
componentWillUnmount() {}
render() {
return <div>Hello World</div>;
}
}

Function component

1
2
3
4
5
6
7
8
9
10
11
12
13
function MyReactComponent() {
// componentDidMount
useEffect(() => {}, []);
// componentDidUpdate + componentDidMount
useEffect(() => {});
// componentWillUnmount
useEffect(() => {
return () => {...}
}, []);
// 在渲染之后但在屏幕更新之前同步运行
useLayoutEffect(() => {}, []);
return <div>Hello World</div>;
}

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>Hello World</div>
</template>
<script>
export default {
beforeCreate() {},
created() {},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeDestroy() {},
destroyed() {},
};
</script>

错误处理

React.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
// 更新状态,这样下一个渲染将显示回退UI。
return { hasError: true };
}
componentDidCatch(error, errorInfo) {}
render() {
if (this.state.hasError) return <h1>Something went wrong.</h1>;
return this.props.children;
}
}
...
<ErrorBoundary>
<App />
</ErrorBoundary>

Vue.js

1
2
3
4
5
6
7
8
const vm = new Vue({
data: {
error: "",
},
errorCaptured: function(err, component, details) {
error = err.toString();
}
}

Ref

React.js

Class component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class AutofocusInput extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
}
state = {
content: "",
};
componentDidMount() {
this.ref.current.focus();
}
setContent = (e) => {
this.setState({ content: e.target.value });
};
render() {
return (
<input
ref={this.ref}
type="text"
value={this.state.content}
onChange={this.setContent}
/>
);
}
}

Function component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function AutofocusInput() {
const [content, setContent] = useState("");
const ref = useRef(null);
useEffect(() => {
if (ref && ref.current) {
ref.current.focus();
}
}, []);
return (
<input
ref={ref}
type="text"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
);
}

Vue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<input ref="input" type="text" v-model="content" />
</template>
<script>
export default {
name: "autofocus-input",
data() {
return { content: "" };
},
mounted() {
this.$refs.input.focus();
},
};
</script>

性能优化

React.js

PureComponent

1
2
3
class MyReactComponent extends React.PureComponent {
...
}

shouldComponentUpdate

1
2
3
4
class MyReactComponent extends React.Component {
shouldComponentUpdate(nextProps) {...}
...
}

React.memo

1
2
3
4
5
6
export default React.memo(
MyReactComponent,
(prevProps, nextProps) => {
...
}
);

useMemo

1
2
3
4
5
export default function MyReactComponent() {
return React.useMemo(() => {
return <div>...</div>;
}, []);
}

useCallback

1
2
3
4
5
6
7
function MyItem({ item, handleDelete }) {
const handleClick = useCallback(() => handleDelete(item), [
item,
handleDelete,
]);
return <button onClick={handleClick}>{item.name}</button>;
}

Vue.js

v:once

1
<span v-once>This will never change: {{msg}}</span>

函数式组件:我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。

1
2
3
4
5
6
7
8
9
10
11
<template functional>
<h1>Hello {{ name }}</h1>
</template>
<script>
export default {
name: "MyVueComponent",
props: {
name: String,
},
};
</script>

keep-alive 组件

1
2
3
<keep-alive>
<component :is="view"></component>
</keep-alive>

完。