客户端渲染效率比vue2提升了1.3~2倍
SSR渲染效率比vue2提升了2~3倍
面试题:vue3的效率提升主要表现在哪些方面?
效率的提升
静态提升
下面的静态节点会被提升
1 2 3 4 5 6 7 8 9 10 11
| render(){ createVNode("h1", null, "Hello World") }
const hoisted = createVNode("h1", null, "Hello World") function render(){ }
|
静态属性会被提升
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
| render(ctx){ return createVNode("button", { onClick: function($event){ ctx.count++; } }) }
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
| <div id="app1"></div> <div id="app2"></div> <script> Vue.use(...); Vue.mixin(...); Vue.component(...); new Vue({ }).$mount("#app1") new Vue({ }).$mount("#app2") </script>
|
在vue3中,去掉了Vue构造函数,转而使用createApp创建vue应用
1 2 3 4 5 6 7
| <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-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值时, 会根据情况决定是否要运行函数 |
应用:
- 如果想要让一个对象变为响应式数据,可以使用
reactive或ref
- 如果想要让一个对象的所有属性只读,使用
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(){
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 } }
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)
const stop = watchEffect(() => {
console.log('count', count.value); console.log('stats', stats.a); })
state.a++; state.a++; state.a++; state.a++; state.a++; state.a++; count.value++; count.value++; count.value++; count.value++; count.value++;
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
|
const state = reactive({ count: 0 }) const options = { immediate: true }
watch(() => state.count, (newValue, oldValue) => { }, options)
const countRef = ref(0); watch(countRef, (newValue, oldValue) => { }, options)
watch([() => state.count, countRef], ([new1, new2], [old1, old2]) => { });
|
注意:无论是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");
|
判断
转换
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++ console.log(state.foo)
state.foo++ console.log(fooRef.value)
|
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)
|
应用:
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, ...state2 } }
setup(){ const state1 = reactive({a:1, b:2}); const state2 = reactive({c:3, d:4}); return { ...toRefs(state1), ...toRefs(state2) } }
function usePos(){ const pos = reactive({x:0, y:0}); return pos; }
setup(){ const {x, y} = usePos(); const {x, y} = toRefs(usePos()); }
|
降低心智负担
所有的composition function均以ref的结果返回,以保证setup函数的返回结果中不包含reactive或readonly直接产生的数据
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); } function useBooks(){ const books = ref([]); return { books } } function useLoginUser(){ const user = readonly({ isLogin: false, loginId: null }); return toRefs(user); }
setup(){ return { ...usePos(), ...useBooks(), ...useLoginUser() } }
|
CompositionApi
面试题:composition api相比于option api有哪些优势?
不同于reactivity api,composition api提供的函数很多是与组件深度绑定的,不能脱离组件而存在。
setup
1 2 3 4 5 6 7 8
| export default { setup(props, context){ } }
|
context对象的成员
| 成员 |
类型 |
说明 |
| attrs |
对象 |
同vue2的this.$attrs |
| slots |
对象 |
同vue2的this.$slots |
| emit |
方法 |
同vue2的this.$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
| 有了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),