WBlog

wangzhiwei blog

0%

vue3 和 vue2 的对比

客户端渲染效率比vue2提升了1.3~2倍

SSR渲染效率比vue2提升了2~3倍

面试题:vue3的效率提升主要表现在哪些方面?

效率的提升

静态提升

下面的静态节点会被提升

  • 元素节点
  • 没有绑定动态内容
1
2
3
4
5
6
7
8
9
10
11
// vue2 的静态节点
render(){
createVNode("h1", null, "Hello World")
// ...
}

// vue3 的静态节点
const hoisted = createVNode("h1", null, "Hello World")
function render(){
// 直接使用 hoisted 即可
}

静态属性会被提升

1
2
3
<div class="user">
{{user.name}}
</div>
1
2
3
4
5
6
const hoisted = { class: "user" }

function render(){
createVNode("div", hoisted, user.name)
// ...
}

预字符串化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="menu-bar-container">
<div class="logo">
<h1>logo</h1>
</div>
<ul class="nav">
<li><a href="">menu</a></li>
<li><a href="">menu</a></li>
<li><a href="">menu</a></li>
<li><a href="">menu</a></li>
<li><a href="">menu</a></li>
</ul>
<div class="user">
<span>{{ user.name }}</span>
</div>
</div>

当编译器遇到大量连续的静态内容,会直接将其编译为一个普通字符串节点

1
const _hoisted_2 = _createStaticVNode("<div class=\"logo\"><h1>logo</h1></div><ul class=\"nav\"><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li></ul>")

缓存事件处理函数

1
<button @click="count++">plus</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue2
render(ctx){
return createVNode("button", {
onClick: function($event){
ctx.count++;
}
})
}

// vue3
render(ctx, _cache){
return createVNode("button", {
onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))
})
}

Block Tree

vue2在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上

1
2
3
4
5
6
7
8
9
10
<form>
<div>
<label>账号:</label>
<input v-model="user.loginId" />
</div>
<div>
<label>密码:</label>
<input v-model="user.loginPwd" />
</div>
</form>

PatchFlag

vue2在对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此只能将所有信息依次比对

1
2
3
<div class="user" data-id="1" title="user name">
{{user.name}}
</div>

面试题1:为什么vue3中去掉了vue构造函数?

面试题2:谈谈你对vue3数据响应式的理解

数据响应式和API的变化

去掉了Vue构造函数

在过去,如果遇到一个页面有多个vue应用时,往往会遇到一些问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- vue2 -->
<div id="app1"></div>
<div id="app2"></div>
<script>
Vue.use(...); // 此代码会影响所有的vue应用
Vue.mixin(...); // 此代码会影响所有的vue应用
Vue.component(...); // 此代码会影响所有的vue应用

new Vue({
// 配置
}).$mount("#app1")

new Vue({
// 配置
}).$mount("#app2")
</script>

vue3中,去掉了Vue构造函数,转而使用createApp创建vue应用

1
2
3
4
5
6
7
<!-- vue3 -->
<div id="app1"></div>
<div id="app2"></div>
<script>
createApp(根组件).use(...).mixin(...).component(...).mount("#app1")
createApp(根组件).mount("#app2")
</script>

更多vue应用的api:https://v3.vuejs.org/api/application-api.html

组件实例中的API

vue3中,组件实例是一个Proxy,它仅提供了下列成员,功能和vue2一样

属性:https://v3.vuejs.org/api/instance-properties.html

方法:https://v3.vuejs.org/api/instance-methods.html

对比数据响应式

vue2和vue3均在相同的生命周期完成数据响应式,但做法不一样

面试题参考答案

面试题1:为什么vue3中去掉了vue构造函数?

1
2
3
4
5
vue2的全局构造函数带来了诸多问题:
1. 调用构造函数的静态方法会对所有vue应用生效,不利于隔离不同应用
2. vue2的构造函数集成了太多功能,不利于tree shaking,vue3把这些功能使用普通函数导出,能够充分利用tree shaking优化打包体积
3. vue2没有把组件实例和vue应用两个概念区分开,在vue2中,通过new Vue创建的对象,既是一个vue应用,同时又是一个特殊的vue组件。vue3中,把两个概念区别开来,通过createApp创建的对象,是一个vue应用,它内部提供的方法是针对整个应用的,而不再是一个特殊的组件。

面试题2:谈谈你对vue3数据响应式的理解

1
2
3
4
5

vue3不再使用Object.defineProperty的方式定义完成数据响应式,而是使用Proxy。
除了Proxy本身效率比Object.defineProperty更高之外,由于不必递归遍历所有属性,而是直接得到一个Proxy。所以在vue3中,对数据的访问是动态的,当访问某个属性的时候,再动态的获取和设置,这就极大的提升了在组件初始阶段的效率。
同时,由于Proxy可以监控到成员的新增和删除,因此,在vue3中新增成员、删除成员、索引访问等均可以触发重新渲染,而这些在vue2中是难以做到的。

模板中的变化

v-model

vue2比较让人诟病的一点就是提供了两种双向绑定:v-model.sync,在vue3中,去掉了.sync修饰符,只需要使用v-model进行双向绑定即可。

为了让v-model更好的针对多个属性进行双向绑定,vue3作出了以下修改

  • 当对自定义组件使用v-model指令时,绑定的属性名由原来的value变为modelValue,事件名由原来的input变为update:modelValue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- vue2 -->
    <ChildComponent :value="pageTitle" @input="pageTitle = $event" />
    <!-- 简写为 -->
    <ChildComponent v-model="pageTitle" />

    <!-- vue3 -->
    <ChildComponent
    :modelValue="pageTitle"
    @update:modelValue="pageTitle = $event"
    />
    <!-- 简写为 -->
    <ChildComponent v-model="pageTitle" />
  • 去掉了.sync修饰符,它原本的功能由v-model的参数替代

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- vue2 -->
    <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
    <!-- 简写为 -->
    <ChildComponent :title.sync="pageTitle" />

    <!-- vue3 -->
    <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
    <!-- 简写为 -->
    <ChildComponent v-model:title="pageTitle" />
  • model配置被移除

  • 允许自定义v-model修饰符

    vue2 无此功能

v-if v-for

v-if 的优先级 现在高于 v-for

key

  • 当使用<template>进行v-for循环时,需要把key值放到<template>中,而不是它的子元素中

  • 当使用v-if v-else-if v-else分支的时候,不再需要指定key值,因为vue3会自动给予每个分支一个唯一的key

    即便要手工给予key值,也必须给予每个分支唯一的key不能因为要重用分支而给予相同的 key

Fragment

vue3现在允许组件出现多个根节点

组件的变化

Teleport

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

import Modal from "../components/Modal.vue";


<Teleport to="body">
<Modal v-if="modalVisible">
<button @click="modalVisible = false">关闭朦层</button>
</Modal>
</Teleport>


<template>
<div class="modal">
<slot></slot>
</div>
</template>

<style scoped>
.modal {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
left: 0;
top: 0;
}
</style>

asyncComponent

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

utils.js
定义 getAsyncComponent 方法加载异步组件 定义 delay 通过 await delay(); 延迟加载


import { defineAsyncComponent, h } from "vue";
import Loading from "../components/Loading.vue";
import Error from "../components/Error.vue";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
NProgress.configure({
trickleSpeed: 50,
showSpinner: false,
});
export function delay(duration) {
if (!duration) {
duration = random(1000, 5000);
}
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, duration);
});
}

export function random(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}

export function getAsyncPage(path) {
return defineAsyncComponent({
loader: async () => {
NProgress.start();
await delay();
const comp = await import(path);
NProgress.done();
return comp;
},
loadingComponent: Loading,
});
}

export function getAsyncComponent(path) {
return defineAsyncComponent({
loader: async () => {
await delay();
if (Math.random() < 0.5) {
return import(path);
}
throw new Error();
},
loadingComponent: Loading,
errorComponent: {
render() {
return h(Error, "组件加载出错");
},
},
});
}



import { getAsyncPage } from "../util";

export default [
{
path: "/",
name: "Home",
component: getAsyncPage("../views/Home.vue"),
},
{
path: "/about",
name: "About",
component: getAsyncPage("../views/About.vue"),
},
];


ReactivityApi

reactivity api: https://v3.vuejs.org/api/reactivity-api

获取响应式数据

API 传入 返回 备注
reactive plain-object 对象代理 深度代理对象中的所有成员
readonly plain-object or proxy 对象代理 只能读取代理对象中的成员,不可修改
ref any { value: ... } 对value的访问是响应式的
如果给value的值是一个对象,
则会通过reactive函数进行代理
如果已经是代理,则直接使用代理
computed function { value: ... } 当读取value值时,
根据情况决定是否要运行函数

应用:

  • 如果想要让一个对象变为响应式数据,可以使用reactiveref
  • 如果想要让一个对象的所有属性只读,使用readonly
  • 如果想要让一个非对象数据变为响应式数据,使用ref
  • 如果想要根据已知的响应式数据得到一个新的响应式数据,使用computed

笔试题1:下面的代码输出结果是什么?

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
import { reactive, readonly, ref, computed } from "vue";

const state = reactive({
firstName: "Xu Ming",
lastName: "Deng",
});
const fullName = computed(() => {
console.log("changed");
return `${state.lastName}, ${state.firstName}`;
});
console.log("state ready");
console.log("fullname is", fullName.value);
console.log("fullname is", fullName.value);
const imState = readonly(state);
console.log(imState === state);

const stateRef = ref(state);
console.log(stateRef.value === state);

state.firstName = "Cheng";
state.lastName = "Ji";

console.log(imState.firstName, imState.lastName);
console.log("fullname is", fullName.value);
console.log("fullname is", fullName.value);

const imState2 = readonly(stateRef);
console.log(imState2.value === stateRef.value);

笔试题2:按照下面的要求完成函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

import { readonly, reactive } from "vue";
function useUser(){
// 在这里补全函数
// const userOrigin = reactive({})
// const user = readonly(userOrigin)
// const setUserName = (name) => {
// userOrigin.name = name
// }

// const setUserAge = (age) => {
// userOrigin.age = age
// }

return {
user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
setUserAge, // 这是一个函数,传入用户年龄,用户修改用户的年龄
}
}

笔试题3:按照下面的要求完成函数

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

import { readonly, reactive } from "vue";

function useDebounce(obj, duration){
// 在这里补全函数
const valueOrigin = reactive(obj);
const value = readonly(valueOrigin);

let timer = null;
const setValue = (newValue) => {

clearTimeout(timer);
timer = setTimeout(() => {
Object.entries(newValue).forEach(([key, value]) => {
valueOrigin[key] = value;
})
},duration)

}

return {
value, // 这里是一个只读对象,响应式数据,默认值为参数值
setValue // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,混合操作需要在duration的时间中防抖
}
}

const {value, setValue} = useDebounceValue({a:1,b:2},5000)

window.value = value;
window.setValue = setValue;

监听数据变化

watchEffect

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

const stats = reactive({a:1, b:2})
const count =ref(0)

// watchEffect 会自动收集用到的依赖,依赖的数据改变后,会重新执行回调函数
const stop = watchEffect(() => {

console.log('count', count.value);
console.log('stats', stats.a);
// 该函数会立即执行,然后追中函数中用到的响应式数据,响应式数据变化后会再次执行
})

// 修改数据,后会自动执行watchEffect函数
state.a++;
state.a++;
state.a++;
state.a++;
state.a++;
state.a++;
count.value++;
count.value++;
count.value++;
count.value++;
count.value++;

//因为会进入微队列,所以会执行一次 watchEffect


// 通过调用stop函数,会停止监听
stop(); // 停止监听

watch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 等效于vue2的$watch

// 监听单个数据的变化
const state = reactive({ count: 0 })
const options = { immediate: true } //会立即执行一次回调函数

// watch 默认不执行回调函数,只有值发生变化才会执行回调函数
// 不可以直接传state.count 需要 () => state.count
watch(() => state.count, (newValue, oldValue) => {
// ...
}, options)

const countRef = ref(0);
watch(countRef, (newValue, oldValue) => {
// ...
}, options)

// 监听多个数据的变化
watch([() => state.count, countRef], ([new1, new2], [old1, old2]) => {
// ...
});

// watch 也是延迟执行的

注意:无论是watchEffect还是watch,当依赖项变化时,回调函数的运行都是异步的(微队列)

应用:除非遇到下面的场景,否则均建议选择watchEffect

  • 不希望回调函数一开始就执行
  • 数据改变时,需要参考旧值
  • 需要监控一些回调函数中不会用到的数据

笔试题: 下面的代码输出结果是什么?

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
import { reactive, watchEffect, watch } from "vue";
const state = reactive({
count: 0,
});
watchEffect(() => {
console.log("watchEffect", state.count);
});
watch(
() => state.count,
(count, oldCount) => {
console.log("watch", count, oldCount);
}
);
console.log("start");

//宏队列
setTimeout(() => {
console.log("time out");
state.count++; //微队列
state.count++;
});
state.count++;//微队列
state.count++;

console.log("end");

// 输出结果
// watchEffect 0
// start
// end
// watchEffect 2
// watch 2 0
// time out
// watchEffect 4
// watch 4 2

判断

API 含义
isProxy 判断某个数据是否是由reactivereadonly
isReactive 判断某个数据是否是通过reactive创建的
详细:https://v3.vuejs.org/api/basic-reactivity.html#isreactive
isReadonly 判断某个数据是否是通过readonly创建的
isRef 判断某个数据是否是一个ref对象

转换

unref

等同于:isRef(val) ? val.value : val

应用:

1
2
3
4
function useNewTodo(todos){
todos = unref(todos);
// ...
}

toRef

得到一个响应式对象某个属性的ref格式

1
2
3
4
5
6
7
8
9
10
11
12
const state = reactive({
foo: 1,
bar: 2
})

const fooRef = toRef(state, 'foo'); // fooRef: {value: ...}

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

toRefs

把一个响应式对象的所有属性转换为ref格式,然后包装到一个plain-object中返回

1
2
3
4
5
6
7
8
9
10
11
12
13
const state = reactive({
foo: 1,
bar: 2
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs: not a proxy
{
foo: { value: ... },
bar: { value: ... }
}
*/

应用:

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
setup(){
const state1 = reactive({a:1, b:2});
const state2 = reactive({c:3, d:4});
return {
...state1, // lost reactivity
...state2 // lost reactivity
}
}

setup(){
const state1 = reactive({a:1, b:2});
const state2 = reactive({c:3, d:4});
return {
...toRefs(state1), // reactivity
...toRefs(state2) // reactivity
}
}
// composition function
function usePos(){
const pos = reactive({x:0, y:0});
return pos;
}

setup(){
const {x, y} = usePos(); // lost reactivity
const {x, y} = toRefs(usePos()); // reactivity
}

降低心智负担

所有的composition function均以ref的结果返回,以保证setup函数的返回结果中不包含reactivereadonly直接产生的数据

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
function usePos(){
const pos = reactive({ x:0, y:0 });
return toRefs(pos); // {x: refObj, y: refObj}
}
function useBooks(){
const books = ref([]);
return {
books // books is refObj
}
}
function useLoginUser(){
const user = readonly({
isLogin: false,
loginId: null
});
return toRefs(user); // { isLogin: refObj, loginId: refObj } all ref is readonly
}

setup(){
// 在setup函数中,尽量保证解构、展开出来的所有响应式数据均是ref
return {
...usePos(),
...useBooks(),
...useLoginUser()
}
}

CompositionApi

面试题:composition api相比于option api有哪些优势?

不同于reactivity api,composition api提供的函数很多是与组件深度绑定的,不能脱离组件而存在。

setup

1
2
3
4
5
6
7
8
// component
export default {
setup(props, context){
// 该函数在组件属性被赋值后立即执行,早于所有生命周期钩子函数
// props 是一个对象,包含了所有的组件属性值
// context 是一个对象,提供了组件所需的上下文信息
}
}

context对象的成员

成员 类型 说明
attrs 对象 vue2this.$attrs
slots 对象 vue2this.$slots
emit 方法 vue2this.$emit

生命周期函数

vue2 option api vue3 option api vue 3 composition api
beforeCreate beforeCreate 不再需要,代码可直接置于setup中
created created 不再需要,代码可直接置于setup中
beforeMount beforeMount onBeforeMount
mounted mounted onMounted
beforeUpdate beforeUpdate onBeforeUpdate
updated updated onUpdated
beforeDestroy ==改== beforeUnmount onBeforeUnmount
destroyed ==改==unmounted onUnmounted
errorCaptured errorCaptured onErrorCaptured
- ==新==renderTracked onRenderTracked
- ==新==renderTriggered onRenderTriggered

新增钩子函数说明:

钩子函数 参数 执行时机
renderTracked DebuggerEvent 渲染vdom收集到的每一次依赖时
renderTriggered DebuggerEvent 某个依赖变化导致组件重新渲染时

DebuggerEvent:

  • target: 跟踪或触发渲染的对象
  • key: 跟踪或触发渲染的属性
  • type: 跟踪或触发渲染的方式

面试题参考答案

面试题:composition api相比于option api有哪些优势?

从两个方面回答:

  1. 为了更好的逻辑复用和代码组织
  2. 更好的类型推导
1
2
有了composition api,配合reactivity api,可以在组件内部进行更加细粒度的控制,使得组件中不同的功能高度聚合,提升了代码的可维护性。对于不同组件的相同功能,也能够更好的复用。
相比于option api,composition api中没有了指向奇怪的this,所有的api变得更加函数式,这有利于和类型推断系统比如TS深度配合。
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
useBar.js

import { computed, ref, watch } from "vue";
import gsap from "gsap";
const colors = ["#334552", "#B34335", "#6E9FA5", "#A2C3AC", "#C8846C"];
export default function useGdpBar(maxSize, gdp) {
const max = computed(() => {
if (gdp.value.length) {
return Math.max(...gdp.value.map((it) => it.value));
}
return 0;
});
const bars = ref([]);
const targetBars = computed(() =>
gdp.value.map((it, i) => {
let size = (it.value / max.value) * maxSize;
return {
size,
color: colors[i % colors.length],
...it,
};
})
);
watch(
targetBars,
(newValue) => {
for (let i = 0; i < newValue.length; i++) {
if (!bars.value[i]) {
bars.value[i] = {
...newValue[i],
size: 0,
value: 0,
};
}
gsap.to(bars.value[i], {
...newValue[i],
duration: 1,
});
}
},
{
deep: true,
}
);
return {
bars,
};
}


Bar1.vue

<template>
<div class="bar1">
<div class="item" v-for="item in bars">
<label>{{ item.country }}</label>
<div
class="bar"
:style="{ background: item.color, width: item.size + 'px' }"
></div>
<div class="value">{{ item.value }}万亿</div>
</div>
</div>
</template>

<script>
import useBar from "../composition/useBar";
import { computed } from "vue";
export default {
props: ["gdp"],
setup(props) {
const gdp = computed(() => props.gdp);
return {
...useBar(400, gdp),
};
},
};
</script>

<style scoped>
.bar1 {
width: 500px;
box-sizing: border-box;
margin: 3em;
border-left: 1px solid #333;
}
.item {
display: flex;
height: 35px;
line-height: 35px;
margin: 1em 0;
position: relative;
}
.bar {
width: 100px;
height: 100%;
margin-right: 1em;
flex: 0 0 auto;
}
.item label {
position: absolute;
left: -50px;
}
.value {
flex: 0 0 auto;
}
</style>


其他

图片引用

创建 URL 对象:调用 new URL 构造函数。
解析相对路径:使用 import.meta.url 解析 ./assets/Wallpaper1.jpg 的相对路径。打包后,图片会存放在 public 目录下。
new URL(./assets/Wallpaper1.jpg, import.meta.url),