轻松搞懂前端面试题系列(vue篇四)
一、Vue 3.0中Treeshaking特性是什么,并举例进行说明?
Tree shaking
是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination
。
简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码。
如果把代码打包比作制作蛋糕,传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去,而treeshaking
则是一开始就把有用的蛋白蛋黄(import
)放入搅拌,最后直接作出蛋糕,也就是说 ,tree shaking
其实是找出使用的代码。
与vue2的区别
在Vue2
中,无论我们使用什么功能,它们最终都会出现在生产代码中,主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到。
vue3
一个比较大的显著的区别就是,当你用一个bundler
的时候,比如webpack
或者rollup
,webpack
和rollup
都是有tree shaking
功能,但是tree shaking
的前提是所有的东西都必须用ES6 module
的import
来写。
vue3
在浏览器里的时候依然会由一个全局的Vue对象,但是当你用了一个bundler
时(比如webpack
),它就没有default export
,你就不能import xxx from vue
,然后把vue本身当一个对象去操作。
那所有的这些API全部要用import
的方式import
进来,这样的结果就是使得一些可能不会用到的一些功能就可以被tree shaking
掉。
比如说 v-model、<transition>
这些功能,如果你不用的话,就不会引用到最后的包里,Tree-shaking某种程度上来讲,也是通过编译器去实现的(记住这句话),举个例子:
可以看到,空的渲染函数没有从vue
中引入任何东西。
但是如果加入一个div
可以看见它引入一些东西,这些东西只有当你引入东西的时候,这些东西才会被打包进去,但默认的还是会保留一些最低的限制,比如Virtual DOM
的更新算法以及响应式系统,无论如何这两个都是会包含在你得包里的。
但是很多常用或者非常用的功能,只有当你用到时才会被import
进来,比如v-model
。
可以看见,从vue
中新引入了vModelText
,withDirectives
。
如果input
类型改成checkbox
,就会引入的是vModelCheckbox
。
如果改成动态的type
就会引入动态的v-model
,即vModelDynamic
。
再比如<transition>
组件,用了<transition>
,对应的Transition就会引入进来。
如何实现
Tree shaking
是基于ES6
模板语法(import
与exports
),主要是借助ES6
模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。
Tree shaking
无非就是做了两件事:
- 编译阶段利用ES6 Module判断哪些模块已经加载。
- 判断那些模块和变量未被使用或者引用,进而删除对应代码。
作用
通过Tree shaking
,Vue3
给我们带来的好处是:
- 减少程序体积(更小)
- 减少程序执行时间(更快)
- 便于将来对程序架构进行优化(更友好)
二、Vue3.0 所采用的 Composition Api 与 Vue2.x 使用的 Options Api 有什么不同?
Composition API
可以说是Vue3
的最大特点,那么为什么要推出Composition Api
,解决了什么问题?
通常使用Vue2
开发的项目,普遍会存在以下问题:
- 代码的可读性随着组件变大而变差。
- 每一种代码复用的方式,都存在缺点。
TypeScript
支持有限。
以上通过使用Composition Api
都能迎刃而解。
Options Api
Options API
,即大家常说的选项API,即以vue为后缀的文件,通过定义methods,computed,watch,data
等属性与方法,共同处理页面逻辑
如下图:
可以看到Options
代码编写方式,如果是组件状态,则写在data
属性上,如果是方法,则写在methods
属性上...用组件的选项 (data、computed、methods、watch
) 组织逻辑在大多数情况下都有效,然而,当组件变得复杂,导致对应属性的列表也会增长,这可能会导致组件难以阅读和理解。假设一个组件是一个大型组件,其内部有很多处理逻辑关注点(对应下图不用颜色)。
可以看到,这种碎片化使得理解和维护复杂组件变得困难,选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
Compostion API
在 Vue3 Composition API
中,组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起(更加的高内聚,低耦合),即使项目很大,功能很多,我们都能快速的定位到这个功能所用到的所有 API。
此外,Composition API
最核心的,就是可以把代码提取出来,把整个功能封装成一个单独的函数(模块),随处可用,也不用担心变量和方法的命名冲突。
只需在组件中导入模块,并调用它即可(模块返回的是函数),函数将返回我们定义的变量,随后我们可以从 setup
函数中使用它们。
// useCount.js
import { ref, computed } from 'vue'
function useCount() {
let count = ref(0)
let double = computed(() => count.value * 2)
function increment() {
count.value++
}
return {
count,
double,
increment
}
}
export default useCount
// app.vue
import useCount from './useCount.js'
export default {
setup() {
let { count, double, increment } = useCount()
return {
count,
double,
increment
}
}
}
小结
- 在逻辑组织和逻辑复用方面,
Composition API
是优于Options API
。 - 因为
Composition API
几乎是函数,会有更好的类型推断。 Composition API
对tree-shaking
友好,代码也更容易压缩。Composition API
中见不到this的使用,减少了this指向不明的情况。- 如果是小型组件,可以继续使用
Options API
,也是十分友好的。
三、Vue3.0 性能提升主要是通过哪几方面体现的?
编译阶段
回顾Vue2
,我们知道每个组件实例都对应一个 watcher
实例,它会在组件渲染的过程中把用到的数据property
记录为依赖,当依赖发生改变,触发setter
,则会通知watcher
,从而使关联的组件重新渲染。
试想一下,一个组件结构如下图:
<template>
<div id="content">
<p class="text">静态文本</p>
<p class="text">静态文本</p>
<p class="text">{{ message }}</p>
<p class="text">静态文本</p>
...
<p class="text">静态文本</p>
</div>
</template>
可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff
和遍历其实都是不需要的,造成性能浪费,因此,Vue3在编译阶段,做了进一步优化,主要有如下:
- diff算法优化
- 静态提升
- 事件监听缓存
- SSR优化
diff算法优化
vue3
在diff
算法中相比vue2
增加了静态标记,关于这个静态标记,其作用是为了会发生变化的地方添加一个flag
标记,下次发生变化的时候直接找该地方进行比较,下图这里,已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提高。
关于静态类型枚举如下:
export const enum PatchFlags {
TEXT = 1,// 动态的文本节点
CLASS = 1 << 1, // 2 动态的 class
STYLE = 1 << 2, // 4 动态的 style
PROPS = 1 << 3, // 8 动态属性,不包括类名和样式
FULL_PROPS = 1 << 4, // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 Fragment
KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
NEED_PATCH = 1 << 9, // 512
DYNAMIC_SLOTS = 1 << 10, // 动态 solt
HOISTED = -1, // 特殊标志是负整数表示永远不会用作 diff
BAIL = -2 // 一个特殊的标志,指代差异算法
}
静态提升
Vue3
中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用,这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用。
<span>你好</span>
<div>{{ message }}</div>
没有做静态提升之前:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode("span", null, "你好"),
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
做了静态提升之后:
const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "你好", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_hoisted_1,
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
// Check the console for the AST
静态内容_hoisted_1
被放置在render
函数外,每次渲染的时候只要取 _hoisted_1
即可,同时 _hoisted_1
被打上了 PatchFlag
,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff
。
事件监听缓存
默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化。
<div>
<button @click = 'onClick'>点我</button>
</div>
没开启事件监听器缓存:
export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
// PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
]))
})
开启事件侦听器缓存后:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "点我")
]))
}
上述发现开启了缓存后,没有了静态标记。也就是说下次diff
算法的时候直接使用。
SSR优化
当静态内容大到一定量级时候,会用createStaticVNode
方法在客户端去生成一个static node
,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染。
div>
<div>
<span>你好</span>
</div>
... // 很多个静态属性
<div>
<span>{{ message }}</span>
</div>
</div>
编译后
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"
export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
const _cssVars = { style: { color: _ctx.color }}
_push(`<div${
_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
}><div><span>你好</span>...<div><span>你好</span><div><span>${
_ssrInterpolate(_ctx.message)
}</span></div></div>`)
}
源码体积
相比Vue2,Vue3
整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking
,任何一个函数,如ref、reavtived、computed
等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小。
import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
setup(props, context) {
const age = ref(18)
let state = reactive({
name: 'test'
})
const readOnlyAge = computed(() => age.value++) // 19
return {
age,
state,
readOnlyAge
}
}
});
响应式系统
vue2
中采用 defineProperty
来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter
和setter
,实现响应式。
vue3
采用proxy
重写了响应式系统,因为proxy
可以对整个对象进行监听,所以不需要深度遍历
- 可以监听动态属性的添加。
- 可以监听到数组的索引和数组length属性。
- 可以监听删除属性。