Vue核心概念回顾
提到Vue一般会想到的是响应式。如果经历过面试,或者想稍微深入一些Vue原理,都会清楚这个词,然后立马脱口而出Object.defineProperty
和Proxy API
可用于实现这一特性。那么问题再深入一些,响应式到底是要响应些什么呢?这个问题如果看过官方文档就很容易答出来:状态变更驱动视图更新。与此同时,与响应式这一概念配套的还有诸如自动收集依赖、细粒度更新等概念。
除了"响应式", Vue中另一个高频概念是生命周期 在生命周期中做什么事情,在你的业务中都安排的明明白白。
虽然本节标题是“Vue核心概念回顾”,但本节的内容并不是对这些名词一一详解。恰恰相反,本节想强调的是在学习React之前,如果能把上面的概念通通忘掉最好,如果没法忘掉,那么也需要记住的是,Vue和React的心智模型不同,换成白话说,就是两个库是两回事,没有太多共通之处,尤其对于只会写Vue 2的同学来说很重要。
不过,如果你有使用Vue 3的经验,那就太好了。Vue 3和React Hooks虽然在写法上有一些神似,但还是那句话,Vue和React的心智模型不同,不是一个东西。
总之,忘了Vue吧。
From Vue 3 to React
本节仅适用于有Vue 3 (包括Vue 2.7)使用经验的读者。如果完全不会Vue 3, 或即使写Vue 3也只写data / methods / computed / watch
这种写法的,直接划过去,读下一节。
虽然上一小节本人最终还是强调"忘了Vue吧",不过如果从使用的角度来说,Vue 3转React还是相对容易些,这是因为Vue 3主推的composition API
(下文简称Vue 3,或称Vue 3 hooks以示区分)借鉴了React hooks
的思想,现在也是全hooks的写法。
下面是本人总结的Vue 3 hooks与React hooks的约等于对照表。但还是那句话,两个库是两回事。
vue 3 | react |
---|---|
ref<T> , shallowRef<T> (不常用) |
useState<T> , 在ref DOM时为useRef<T extends Element>
|
reactive<T extends object> |
useState<T extends object> , 配合immer 库则可使用useImmer<T extends object>
|
watch / watchEffect / 各种生命周期hook |
useEffect |
computed |
不需要, 勉强可认为useMemo 对应 |
nextTick |
无 |
defineProps |
props直接作为函数式组件的参数传入 |
defineEmits |
单向数据流, 需要自定义事件放props时传 |
defineExpose 与 ref 协同 |
使用forwardRef 与useRef 协同实现 |
defineSlots |
不需要 |
这样看来, 是不是React更简洁一些?
From Vue all to React
这一章的核心内容是教你科学地忘掉Vue。对于读过上一节的读者来说,请第三遍告诉自己Vue 和 React两个库是两回事。如果直接划到这一节,那么恭喜你,你将深刻认识到为什么说Vue和React不是一回事。
如果我们用Vue写一个倒计时组件,你可能会写出下面的代码 (不计空格共43行):
<template>
<div class="hello-world">{{ innerTime }}</div>
</template>
<script>
export default {
props: {
startTime: {
type: Number,
default() {
return 60;
}
}
},
data() {
return {
innerTime: this.startTime,
timer: null,
};
},
watch: {
startTime: {
immediate: true,
handler(newValue) {
this.innerTime = newValue;
}
}
},
mounted() {
this.timer = setInterval(() => {
if (!this.innerTime) {
clearInterval(this.timer);
} else {
this.innerTime--;
}
}, 1000);
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer);
}
}
};
</script>
如果用React hooks
实现相同的功能则只需要22行(除去空格):
import { useState, useEffect, useRef } from 'react';
export function MyCountdown(props: { readonly startTime: number }) {
const { startTime } = props;
const [innerTime, setInnerTime] = useState(startTime);
useEffect(() => {
setInnerTime(startTime);
}, [startTime]);
const timer = useRef<ReturnType<typeof setInterval>>();
useEffect(() => {
timer.current = setInterval(() => {
if (!innerTime) {
clearInterval(timer.current);
} else {
setInnerTime(pre => pre - 1);
}
}, 1000);
return () => {
clearInterval(timer.current);
};
}, [innerTime]);
return <p className="my-countdown">{innerTime}</p>;
}
可以看出, 代码量直接减少了50%(这还是在React开了TS的情况下)。并且仔细看代码,发现我们在React中并没有写任何的data
以及生命周期函数,全靠useEffect
实现。如果写惯了Vue,去读下面的useEffect
会感到疑惑,可能的疑点在于--组件挂载后setInterval
, 组件卸载前clearInterval
,可是为什么在React的代码中没有感受到这一变化呢? 后续会详谈为什么,这一小节只需要忘掉生命周期的概念。React hooks没有生命周期一说,总想着生命周期会干扰你对React的理解。
再比如,如果我们写一个“输入框+搜索按钮”的组件(不考虑CSS)。使用Vue实现如下 (不考虑空行22行):
<template>
<div class="input-search">
<input class="input-search-input" v-model="inputValue" />
<button @click="handleSearch">
Search
</button>
</div>
</template>
<script>
export default {
data() {
return {
inputValue: '',
};
},
methods: {
handleSearch() {
const { $emit: emit, inputValue } = this;
emit('search', inputValue);
}
}
}
</script>
如使用React, 需要20行:
import { useRef } from 'react';
export function InputSearch(props: { onSearch(inputValue: string): void }) {
const { onSearch } = props;
const inputRef = useRef<HTMLInputElement>(null);
function handleSearch() {
onSearch(inputRef.current?.value || '');
}
return (
<div className="input-search">
<input ref={inputRef} />
<button
onClick={handleSearch}
type="button"
>
Search
</button>
</div>
);
}
我们可以发现,在React中并不存在Vue的v-model语法糖。所以,忘掉各种好用的语法糖。
另外,Vue可以透传class, style等属性,也可以使用css scoped
这一特性。而这一点在React中却不可以。
ChildComponent.vue
:
<template>
<div>child component</div>
</template>
ParentComponent.vue
:
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
</script>
<template>
<div>
<!--- 这样做在Vue中是允许的 --->
<child-component class="child-comp" />
</div>
</template>
ChildComponent.tsx
// ChildComponent.tsx
import React from 'react';
export function ChildComponent() {
return <div>child component</div>;
}
ParentComponent.tsx
// ChildComponent.tsx
import React from 'react';
import ChildComponent from './ChildComponent.tsx';
export function ParentComponent() {
// 这样不可以!
return <div> <ChildComponent className="child-comp" /> </div>
}