0%

Vue3 && React 对比学习

Vue3/React 对比学习

针对以下几个点,参照官方文档,进行对比学习。

  • 添加样式
  • 组件传值
  • 操作 DOM
  • 条件渲染
  • 列表渲染
  • 计算属性
  • 侦听器

说明: vue3.0 的示例中,使用了 unplugin-auto-import/vite。
自动导入了 Vue 相关函数,如:ref, reactive, defineProps, defineEmits 等

添加样式

vue3.0

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
34
35
36
<!-- https://cn.vuejs.org/guide/essentials/class-and-style.html -->

<!-- 给 :class (v-bind:class 的缩写) 传递一个对象来动态切换 class -->
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>

<!-- 直接绑定一个对象 -->
<div :class="classObject"></div>
<script setup>
const classObject = reactive({
active: true,
'text-danger': false
});
</script>

<!-- 绑定内联样式 -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<div :style="{ 'font-size': fontSize + 'px' }"></div>
<!-- 绑定数组 -->
<div :style="[baseStyles, overridingStyles]"></div>
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

<!-- 在组件上使用 -->
<!-- 如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class。你可以通过组件的 $attrs 属性来实现指定: -->

<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>

<MyComponent class="baz" />

<!-- 这将被渲染为: -->
<p class="baz">Hi!</p>
<span>This is a child component</span>

react

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
// https://github.com/JedWatson/classnames#readme
// A simple JavaScript utility for conditionally joining classNames together.
import classNames from 'classnames';
export default function TodoList() {
return (
<div>
<Child className={['item-ddd', 'item-eee']} />
<ul
className="avatar" // 使用 className 来指定一个 CSS class
style={{
backgroundColor: 'black',
color: 'pink'
}}
>
<li>Improve the videophone</li>
<li>Prepare aeronautics lectures</li>
<li>Work on the alcohol-fuelled engine</li>
</ul>
</div>
);
}

const Child = props => {
return <div className={classNames(props.className, 'aaa', 'bbb')}>Child</div>;
};

组件传值

vue3.0

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
34
35
36
37
38
39
40
<!-- https://cn.vuejs.org/guide/components/props.html -->

<!-- parent.vue -->
<template>
<Child :data="data" :version="version" @submit="emitFun" />
</template>
<script setup>
import Child from './child.vue';
const data = ref({
name: 'xiao ming',
age: 18
});
const version = '3.0';
const emitFun = e => {
console.log('emitFun', e);
};
</script>

<!-- child.vue -->
<template>
<div>{{ props.data.name }}<br />{{ data.age }}</div>
<div>{{ version }}</div>
<button @click="handleSubmit">submit</button>
</template>
<script setup>
const props = defineProps({
data: Object,
version: String
});
const emit = defineEmits(['submit']);
const handleSubmit = () => {
// props 单向数据流:所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
// 每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。
props.version = '2.0'; // ❌ 警告!prop 是只读的!
// 当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。
props.data.age = 33; //
// 这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变
emit('submit', 'ok!');
};
</script>

react

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
34
35
36
37
38
39
// https://zh-hans.react.dev/learn/passing-props-to-a-component

// 父组件
const Parent = () => {
const data = {
name: 'xiao ming',
age: 18
};
const version = '3.0';
const emitFun = e => {
console.log('emitFun', e);
};
return (
<section>
<Child data={data} version={version} submit={emitFun} />
</section>
);
};

// 子组件
function Child(props) {
console.log('props', props);
// 子传父
const handleSubmit = () => {
props.submit('123');
};
return (
<div>
<div>
{props.data.name}
<br />
{props.data.age}
</div>
<div>{props.version}</div>
<button onClick={handleSubmit}>submit</button>
</div>
);
}
export default Parent;

操作 DOM

vue3.0

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
34
35
36
37
38
39
40
41
<!-- https://cn.vuejs.org/guide/essentials/template-refs.html -->

<!-- parent.vue -->
<template>
<Child ref="child" />
<div ref="input"><input /></div>
</template>
<script setup>
const input = ref(null);
const child = ref(null);
// null 注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input,在初次渲染时会是 null。这是因为在初次渲染前这个元素还不存在呢!
console.log(input.value);
console.log(child.value);
onMounted(() => {
console.log(input.value); // <div><input></div>
console.log(child.value); // Proxy 对象 child.value.a = 1; child.value.b = 2;
});
// 如果你需要侦听一个模板引用 ref 的变化,确保考虑到其值为 null 的情况:
watchEffect(() => {
if (input.value) {
input.value.focus();
} else {
// 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
}
});
</script>

<!-- child.vue -->
<template>
<div>Child</div>
</template>
<script setup>
// 使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:
const a = 1;
const b = ref(2);

defineExpose({
a,
b
});
</script>

react

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
34
35
36
37
38
39
40
41
42
43
44
45
// https://zh-hans.react.dev/reference/react/useRef
// https://zh-hans.react.dev/learn/manipulating-the-dom-with-refs
// https://zh-hans.react.dev/reference/react/useImperativeHandle

import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
const Parent = () => {
const input = useRef(null);
const child = useRef(null); // react-dom.development.js:86 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
console.log(input.current); // null 🚩 不要在渲染期间读取 ref
console.log(child.current); // null 🚩 不要在渲染期间读取 ref
useEffect(() => {
// ✅ 你可以在 effects 中读取和写入 ref
console.log(input.current); // <div><input></div>
console.log(child.current); // {a: 1, b: 2}
});
function handleClick() {
// ✅ 你可以在事件处理程序中读取和写入 ref
console.log(input.current);
console.log(child.current);
}
return (
<div>
<div ref={input}>
<input />
</div>
<Child ref={child} />
</div>
);
};

const Child = forwardRef(({}, ref) => {
useImperativeHandle(
ref,
() => {
return {
a: 1,
b: 2
};
},
[]
);
return <div ref={ref}>Child</div>;
});

export default Parent;

条件渲染

vue3.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- https://cn.vuejs.org/guide/essentials/conditional.html -->

<!-- v-if,v-else,v-else-if -->
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>

<!-- v-show -->
<h1 v-show="ok">Hello!</h1>

<!-- v-if vs. v-show​
v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。
总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。 -->

react

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
34
35
36
37
38
// https://zh-hans.react.dev/learn/conditional-rendering

function Item({ name, isPacked }) {
// 一、选择性地包含 JSX
// if (isPacked) {
// return <li className="item">{name} ✔</li>;
// }
// return <li className="item">{name}</li>;

// 二、与运算符(&&)
// <li className="item">
// {name} {isPacked && '✔'}
// </li>

// 三、三目运算符(? :) PS:选择性地将 JSX 赋值给变量
const itemContent = isPacked ? <del>{name + ' ✔'}</del> : name;
return <li className="item">{itemContent}</li>;
}

export default function PackingList() {
return (
<section>
<h1>Sally Ride 的行李清单</h1>
<ul>
<Item isPacked={true} name="宇航服" />
<Item isPacked={true} name="带金箔的头盔" />
<Item isPacked={false} name="Tam 的照片" />
</ul>
</section>
);
}
// 摘要
// 在 React,你可以使用 JavaScript 来控制分支逻辑。
// 你可以使用 if 语句来选择性地返回 JSX 表达式。
// 你可以选择性地将一些 JSX 赋值给变量,然后用大括号将其嵌入到其他 JSX 中。
// 在 JSX 中,{cond ? <A /> : <B />} 表示 “当 cond 为真值时, 渲染 <A />,否则 <B />”。
// 在 JSX 中,{cond && <A />} 表示 “当 cond 为真值时, 渲染 <A />,否则不进行渲染”。
// 快捷的表达式很常见,但如果你更倾向于使用 if,你也可以不使用它们,。

列表渲染

vue3.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- https://cn.vuejs.org/guide/essentials/list.html -->

<!-- 它们同时存在于一个节点上时,v-if 比 v-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名 -->
<!-- key 这个特殊的 attribute 主要作为 Vue 的虚拟 DOM 算法提示,在比较新旧节点列表时用于识别 vnode。 在没有 key 的情况下,Vue 将使用一种最小化元素移动的算法,并尽可能地就地更新/复用相同类型的元素。如果传了 key,则将根据 key 的变化顺序来重新排列元素,并且将始终移除/销毁 key 已经不存在的元素。 -->
<!-- 不建议使用 index 作为key, 数组项的顺序在插入、删除或者重新排序等操作中会发生改变,此时把索引顺序用作 key 值会产生一些微妙且令人困惑的 bug -->
<!-- Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
-->
<template>
<template v-for="todo in todos" :key="todo.name">
<li v-if="!todo.isComplete">{{ todo.name }}</li>
</template>
</template>

react

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// https://zh-hans.react.dev/learn/rendering-lists
// 用 key 保持列表项的顺序
// 这些 key 会告诉 React,每个组件对应着数组里的哪一项,所以 React 可以把它们匹配起来。这在数组项进行移动(例如排序)、插入或删除等操作时非常重要。一个合适的 key 可以帮助 React 推断发生了什么,从而得以正确地更新 DOM 树。
// React 中为什么需要 key?
// 设想一下,假如你桌面上的文件都没有文件名,取而代之的是,你需要通过文件的位置顺序来区分它们———第一个文件,第二个文件,以此类推。也许你也不是不能接受这种方式,可是一旦你删除了其中的一个文件,这种组织方式就会变得混乱无比。原来的第二个文件可能会变成第一个文件,第三个文件会成为第二个文件……
// React 里需要 key 和文件夹里的文件需要有文件名的道理是类似的。它们(key 和文件名)都让我们可以从众多的兄弟元素中唯一标识出某一项(JSX 节点或文件)。而一个精心选择的 key 值所能提供的信息远远不止于这个元素在数组中的位置。即使元素的位置在渲染的过程中发生了改变,它提供的 key 值也能让 React 在整个生命周期中一直认得它。
// 陷阱:你可能会想直接把数组项的索引当作 key 值来用,实际上,如果你没有显式地指定 key 值,React 确实默认会这么做。但是数组项的顺序在插入、删除或者重新排序等操作中会发生改变,此时把索引顺序用作 key 值会产生一些微妙且令人困惑的 bug。
// 与之类似,请不要在运行过程中动态地产生 key,像是 key={Math.random()} 这种方式。这会导致每次重新渲染后的 key 值都不一样,从而使得所有的组件和 DOM 元素每次都要重新创建。这不仅会造成运行变慢的问题,更有可能导致用户输入的丢失。所以,使用能从给定数据中稳定取得的值才是明智的选择。
// 有一点需要注意,组件不会把 key 当作 props 的一部分。Key 的存在只对 React 本身起到提示作用。如果你的组件需要一个 ID,那么请把它作为一个单独的 prop 传给组件: <Profile key={id} userId={id} />。

const people = [
{
id: 0, // 在 JSX 中作为 key 使用
name: '凯瑟琳·约翰逊',
profession: '数学家',
accomplishment: '太空飞行相关数值的核算',
imageId: 'MK3eW3A'
},
{
id: 1, // 在 JSX 中作为 key 使用
name: '马里奥·莫利纳',
profession: '化学家',
accomplishment: '北极臭氧空洞的发现',
imageId: 'mynHUSa'
},
{
id: 2, // 在 JSX 中作为 key 使用
name: '穆罕默德·阿卜杜勒·萨拉姆',
profession: '物理学家',
accomplishment: '关于基本粒子间弱相互作用和电磁相互作用的统一理论',
imageId: 'bE7W1ji'
},
{
id: 3, // 在 JSX 中作为 key 使用
name: '珀西·莱温·朱利亚',
profession: '化学家',
accomplishment: '开创性的可的松药物、类固醇和避孕药',
imageId: 'IOjWm71'
},
{
id: 4, // 在 JSX 中作为 key 使用
name: '苏布拉马尼扬·钱德拉塞卡',
profession: '天体物理学家',
accomplishment: '白矮星质量计算',
imageId: 'lrWQx8l'
}
];
export default function List() {
const listItems = people.map(person => (
<li key={person.id}>
<p>
<b>{person.name}</b>
{' ' + person.profession + ' '}因{person.accomplishment}而闻名世界
</p>
</li>
));
return <ul>{listItems}</ul>;
}

计算属性

vue3.0

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
<!-- https://cn.vuejs.org/guide/essentials/computed.html -->

<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
<script setup>
import { reactive, computed } from 'vue';

const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
});

// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No';
});
</script>

<!-- 计算属性缓存 vs 方法
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的
不同之处在于计算属性值会基于其响应式依赖被缓存
一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。
相比之下,方法调用总是会在重渲染发生时再次执行函数。
为什么需要缓存呢?想象一下我们有一个非常耗性能的计算属性 list,需要循环一个巨大的数组并做许多计算逻辑,并且可能也有其他计算属性依赖于 list。没有缓存的话,我们会重复执行非常多次 list 的 getter,然而这实际上没有必要!如果你确定不需要缓存,那么也可以使用方法调用。 -->

react

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// https://zh-hans.react.dev/reference/react/useMemo

// useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果
// 通过将 visibleTodos 的计算函数包裹在 useMemo 中,你可以确保它在重新渲染之间具有相同值,直到依赖项发生变化。你 不必 将计算函数包裹在 useMemo 中,除非你出于某些特定原因这样做。在此示例中,这样做的原因是你将它传递给包裹在 memo 中的组件,这使得它可以跳过重新渲染。
import { useMemo, memo } from 'react';
export default function TodoList({ todos, tab, theme }) {
// 告诉 React 在重新渲染之间缓存你的计算结果...
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab] // ...所以只要这些依赖项不变...
);
return (
<div className={theme}>
{/* ... List 也就会接受到相同的 props 并且会跳过重新渲染 */}
<List items={visibleTodos} />
</div>
);
}
const List = memo(function List({ items }) {
// ...
});

侦听器

vue3.0

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
34
35
36
37
<!-- https://cn.vuejs.org/guide/essentials/watchers.html -->

<script setup>
import { ref, watch } from 'vue';

const question = ref('');
const answer = ref('Questions usually contain a question mark. ;-)');

// 可以直接侦听一个 ref
watch(
question,
async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?') > -1) {
answer.value = 'Thinking...';
try {
const res = await fetch('https://yesno.wtf/api');
answer.value = (await res.json()).answer;
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error;
}
}
},
// 直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发
// 相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调
// 显式地加上 deep 选项,强制转成深层侦听器
// 即时回调的侦听器: 通过传入 immediate: true 选项来强制侦听器的回调立即执行
{ deep: true, immediate: true }
);
</script>

<template>
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>
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
34
35
36
37
38
// 侦听数据源类型
const x = ref(0);
const y = ref(0);

// 单个 ref
watch(x, newX => {
console.log(`x is ${newX}`);
});

// getter 函数
watch(
() => x.value + y.value,
sum => {
console.log(`sum of x + y is: ${sum}`);
}
);

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`);
});

// 注意,你不能直接侦听响应式对象的属性值,例如:
const obj = reactive({ count: 0 });

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, count => {
console.log(`count is: ${count}`);
});

// 这里需要用一个返回该属性的 getter 函数
// 提供一个 getter 函数
watch(
() => obj.count,
count => {
console.log(`count is: ${count}`);
}
);
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
// watchEffect()
const todoId = ref(1);
const data = ref(null);

watch(
todoId,
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
);
data.value = await response.json();
},
{ immediate: true }
);

// 使用 watchEffect()
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
);
data.value = await response.json();
});
// 这个例子中,回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 todoId 作为源值。

// watch vs. watchEffect​
// watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
// - watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
// - watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 回调的触发时机
// 如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项
watch(source, callback, {
flush: 'post'
});

watchEffect(callback, {
flush: 'post'
});

// 后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect()

watchPostEffect(() => {
/* 在 Vue 更新后执行 */
});

react

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
34
35
36
37
38
39
40
41
// https://zh-hans.react.dev/reference/react/useEffect

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}

// 另一个例子: 在自定义 Hook 中封装 Effect
// 这个 useChatRoom 自定义 Hook 把 Effect 的逻辑“隐藏”在一个更具声明性的 API 之后
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
// 然后你可以像这样从任何组件使用它
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
}