1.谈谈对vue的理解
官方:Vue是一套基于构建用户页面的渐进式框架,Vue的核心库只关注视图层
声明式框架
-
早在jquery的时期,编写代码都是命令式的,命令式框架的特点就是关注过程
-
声明式框架更加注重结果,命令式的代码封装到了vue.js中,过程靠vue.js来实现
声明式代码更加简单,不需要关注实现,按照要求填代码就可以(给上原材料就出结果)
//命令式编程 let number=[1,2,3,4,5] let total= 0 for(let 1=0;i<number.length;i++){ total+=numbers[i] //关注了过程 } console.log(total) //声明式编程 let total2 = number.reduce((prev,cur)=>prev+=cur,0)
MVVM模式
https://blog.csdn.net/qq_40588441/article/details/140337378
目的:职责划分、分层管理
对于前端而言就是如何将数据同步到页面上,也是借鉴后端思想
-
MVC模式:backbone+underscore+jquery
- Node中的egg.js、Nest.js都是MVC模式
对于前端而言,数据变化无法同步到视图中,需要将逻辑聚拢在controller层
-
MVVM模式:映射关系的简化(隐藏controller)
- Vue并没有完全遵循MVVM模型(Vue只是其中的响应式原理遵循的是MVVM)
- Vue可以不直接通过viewModel来操作视图,可以手动把数据挂载到视图上(例如:ref)
- Vue并没有完全遵循MVVM模型(Vue只是其中的响应式原理遵循的是MVVM)
采用虚拟DOM
- 传统更新页面,拼接一个完整的字符串 innerHTML 全部重新渲染添加虚拟 DOM 后,可以比较新旧虚拟节点,找到变化在进行更新。虚拟 DOM 就是一个对象,用来描达真实 DOM 的
- 虚拟DOM的属性比真实DOM属性少很多
- Vue2虚拟DOM属性 https://github.com/vuejs/vue/blob/main/src/core/vdom/vnode.ts
- 虚拟DOM可以跨平台,可以在小程序,可以在APP使用
- 虚拟DOM可以减少操作DOM的频率,diff算法等
区分编译时(打包)和运行(浏览器)时
- Vue 的渲染核心就是调用渲染(render)方法将虚拟 DOM 渲染成真实 DOM(缺点就是虚拟 DOM 编写)麻烦)
- 专门写个编译时可以将模板编译成虚拟 DOM(在构建的时候进行编译性能更高,不需要再运行的时候进行编译)
组件化
-
实现高内聚、低耦合、单向数据流
-
组件化开发能大幅提高应用开发效率、测试性、复用性等
-
降低更新范围,只重新渲染变化的组件
2.谈谈对SPA的理解
-
SPA (single-page application)单页应用,默认情况下我们编写\ue、React 都只有一个 htm1 页面,并且提供一个挂载点,最终打
包后会再此页面中引入对应的资源。(页面的渲染全部是由 JS 动态进行渲染的)。切换页面时通过监听路由变化,渲染对应的页面
Client Side Rendering,客户端渲染 CSR
-
MPA (Multi-page application)多页应用,多个 html 页面。每个页面必须重复加载,js, css 等相关资源。(服务端返回完整的
html,同时数据也可以再后端进行获取一并返回“模板引擎“)。多页应用跳转要整页资源刷新。Server Side Rendering,服务器端渲
染 SSR
HTML 是在前端动态生成的“客户端渲染“,在服务端处理好并返回的是“服务端渲染“
优缺点
单页面应用(SPA) | 多页面应用(MPA) | |
---|---|---|
组成 | 一个主页面和页面组件 | 多个完整的页面 |
刷新方式 | 局部刷新 | 整页刷新 |
SEO搜索引擎优化 | 无法实现 | 容易实现 |
页面切换 | 速度快,用户体验好 | 切换加载资源,速度慢,用户体验差 |
维护成本 | 相对容易 | 相对复杂 |
- SPA用户体验好、快,内容的改变不需要重新加载整个页面,服务端压力小
- SPA 应用不利于搜索引擎的抓取
- SPA首次渲染速度相对较慢(第一次返回空的 html,需要再次请求首屏数据)白屏时间长
解决方案
-
静态页面预渲染(Static Site Generation) SSG,在构建时生成完整的 html 页面。(就是在打包的时候,先将页面放到浏览器中运行一下,将 HTML 保存起来),仅适合静态页面网站。变化率不高的网站
-
SSR+CSR 的方式,首屏采用服务端渲染的方式,后续交互采用客户端渲染方式。NuxtJS
3.Vue为什么需要使用虚拟DOM
基本上所有框架都引入了虚拟 DOM 来对真实 DOM 进行抽象,也就是现在大家所熟知的 VNode 和VDOM
- Virtual DOM 就是用 js 对象来描述真实 DOM,是对真实 DOM 的拍象,由于直接操作 DOM 性能低但是 js 层的操作效率高,可以将 DOM 操作转化成对象操作,最终通过 diff 算法比对差异进行更新 DOM(减)少了对真实 DOM 的操作)
- 虚拟DOM不依赖真实平台环境从而也可以实现跨平台
VDOM是如何生成的?
- 在 vue 中我们常常会为组件编写模板-template
- 这个模板会被编译器编译为渲染函数-render
- 在接下来的挂载过程中会调用 render 函数,返回的对象就是虚拟 dom
- 会在后续的 patch 过程中进一步转化为真实 dom
VDom如何diff的?
-
挂载过程结束后,会记录第一次生成的 VDOM-oldVnode
-
当响应式数据发生变化时,将会引起组件重新 render,此时就会会生成新的 VDOM-newVnode
-
使用 oldVnode 与 newVnode 做 diff 操作,将更改的部分应到真实 DOM 上,从而转换为最小量的 dom 操作,高效更新视图
4.对Vue组件化的理解
WebComponent 组件化的核心组成:模板、属性、事件、插槽、生命周期
组件化好处:高内聚、可重用、可组合
- 组件化开发能大幅提高应用开发效率、测试性、复用性等;
- 降低更新范围,只重新渲染变化的组件;
Vue 中的每个组件都有一个渲染函数 watcher(Vue2中称之为渲染watcher)、effect(Vue3中称之为渲染effect)
数据是响应式的,数据变化后会执行 watcher 或者 effect
组件要合理的划分,如果不拆分组件,那更新的时候整个页面都要重新更新
如果过分的拆分组件会导致 watcher、effect 产生过多也会造成性能浪费
5.既然 Vue 通过数据劫持可以精准探测数据变化,为什么需要虚拟 DOM 进行 diff 检测差异?
Vue 内部设计原因导致,vue 设计的是每个组件一个 watcher(渲染 watcher),没有采用一个属性对应一个 watcher。这样会导致大量 watcher 的产生而且浪费内存
如果粒度过低也无法精准检测变化。所以采用 diff 算法+组件级 watcher,减少watcher的产生
6.请说一下你对响应式数据的理解?
如何实现响应式数据
数组和对象类型当值变化时如何劫持到。对象内部通过defineReactive
方法,使用Object.defineProperty
将属性进行劫持(只会劫
持已经存在的属性),数组则是通过重写数组方法来实现。 多层对象是通过递归来实现劫持。Vue3
则采用 proxy
vue2
处理缺陷
- 在
Vue2
的时候使用defineProperty
来进行数据的劫持, 需要对属性进行重写添加getter
及setter
性能差。 - 当新增属性和删除属性时无法监控变化,只能监控已经存在的属性进行get set。需要通过
$set
、$delete
实现 - 数组不采用
defineProperty
来进行劫持 (浪费性能,对所有索引进行劫持会造成性能浪费)需要对数组单独进行处理。 - 对于
ES6
中新产生的 Map、Set 这些数据结构不支持。
Vue2
与 Vue3
实现对比
vue3的proxy并不用去重写属性,只是做了一层代理,而且拦截的是对象,而不是对象里的属性,也不涉及到无法监控到新增属性和删除属性,也支持数组的劫持
// vue2中
function defineReactive(target,key,value){
observer(value);
Object.defineProperty(target,key,{ ¸v
get(){
return value;
},
set(newValue){
if (value !== newValue) {
value = newValue;
observer(newValue)
}
}
})
}
function observer(data) {
if(typeof data !== 'object'){
return data
}
for(let key in data){
defineReactive(data,key,data[key]);
}
}
// vue3中
let handler = {
get(target, key) {
// 是对象,再次进行代理,懒代理
if (typeof target[key] === "object") {
return new Proxy(target[key], handler);
}
return Reflect.get(target, key);
},
set(target, key, value) {
let oldValue = target[key];
if (oldValue !== value) {
return Reflect.set(target, key, value);
}
return true;
},
};
let proxy = new Proxy(obj, handler);
Vue2对应源码:https://github1s.com/vuejs/vue/blob/HEAD/src/core/observer/index.ts
Vue3对应源码:https://github1s.com/vuejs/core/blob/main/packages/reactivity/src/reactive.ts#L90-L91
7.Vue 中如何检测数组变化?
Vue2 中采用重写数组方法的方式
- 数组考虑性能原因没有用
defineProperty
对数组的每一项进行拦截,而是选择重写数组(push,shift,pop,splice,unshift,sort,reverse
)方法。 - 数组中如果是对象数据类型也会进行递归劫持。
数组的缺点
- 数组的索引和长度变化是无法监控到的
Vue3 直接采用的是 Proxy
- 在 Vue 3.x 中,直接使用 Proxy 实现了更高效和精确的数组变化检测,通过 Proxy,Vue 可以捕获到数组索引和长度的变化,不再需要重写数组的方法。这是 Vue 3.x 在性能方面的一个重要改进(但是由于代理问题,还需要对部分检测方法进行重写)。
Vue2数组重写对应源码:https://github.com/vuejs/vue/blob/main/src/core/observer/array.ts#L12
Vue3数组重写对应源码:https://github.com/vuejs/core/blob/main/packages/reactivity/src/baseHandlers.ts#L53
8.Vue 中如何进行依赖收集?
依赖收集的流程
- 每个属性都拥有自己的
dep
属性,存放他所依赖的 watcher,当属性变化后会通知自己对应的 watcher 去更新 - 默认在初始化时会调用 render 函数,此时会触发属性依赖收集
dep.depend
- 当属性发生修改时会触发
watcher
更新dep.notify()
Vue3
依赖收集
Vue3
中会通过Map
结构将属性和effect
映射起来。- 默认在初始化时会调用 render 函数,此时会触发属性依赖收集
track
, - 当属性发生修改时会找到对应的
effect
列表依次执行trigger
Vue3依赖收集源码:https://github1s.com/vuejs/core/blob/main/packages/reactivity/src/effect.ts#L112
9.Vue.set 方法是如何实现的?
Vue2
不允许在已经创建的实例上动态添加新的响应式属性。所以采用 set API 来进行实现。
Vue2.set对应源码位置:https://github1s.com/vuejs/vue/blob/main/src/core/observer/index.ts#L221
export function set(targetx, key, val) {
// 1.是开发环境 target 没定义或者是基础类型则报错
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${target}`
);
}
// 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
// 3.如果是对象本身的属性,则直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
// 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)
const ob = target.__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid adding reactive properties to a Vue instance or its root $data " +
"at runtime - declare it upfront in the data option."
);
return val;
}
// 5.如果不是响应式的也不需要将其定义成响应式属性
if (!ob) {
target[key] = val;
return val;
}
// 6.将属性定义成响应式的
defineReactive(ob.value, key, val);
// 通知视图更新
ob.dep.notify();
return val;
}
当我们选择新增属性时,可以考虑使用对象合并的方式实现
this.info = {...this.info,...{newProperty1:1,newProperty2:2 ...}}
Vue3
则采用 proxy 来进行数据劫持,可以直接劫持到属性新增的逻辑,无需采用补丁的方式来进行实现。
10.Vue 中的 v-show 和 v-if 怎么理解?
- v-if 如果条件不成立不会渲染当前指令所在节点的 dom 元素
- 最后会变成三元表达式
- v-show 只是切换当前 dom 的显示或者隐藏 display、opacity(控制透明度,会占位,事件正常)、visiviblity(不满足则被隐藏,占位,没有事件)
- 最后会变成指令
Vue2 v-show源码位置:https://github1s.com/vuejs/vue/blob/main/src/platforms/web/runtime/directives/show.ts#L46
效果展示
如何选择
v-if
可以阻断内部代码是否执行,如果条件不成立不会执行内部逻辑- 如果页面逻辑在第一次加载的时候已经被确认后续不会频繁更改则采用
v-if
- v-if优先级高于v-show
11.computed 和 watch 区别
好文推荐:https://blog.csdn.net/qq_36384657/article/details/137138375
Vue2 中有三种 watcher (渲染 watcher 、计算属性 watcher、 用户 watcher)
Vue3 中有三种 effect (渲染 effect 、计算属性 effect、 用户 effect)
computed
-
计算属性仅当用户取值时才会执行对应的方法。
-
computed 属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行。
-
每一个计算属性内部维护一个 dirty 属性 dirty: true
当取值的时候 dirty 为 true 就执行用户的方法,拿到值缓存起来 this. Value 并且将 dirty=false 再次取值的时候 dirty 为
false,直接返回缓存的 this. Value
-
-
计算属性可以简化模板中复杂表达式。
-
计算属性中不支持异步逻辑。
-
computed 属性是可以再模板中使用的。
watch
watch 则是监控值的变化,当值发生变化时调用对应的回调函数。经常用于监控某个值的变化,进行一些操作。(异步要注意竞态问题。)
Vue2中watch存在数据清理问题,Vue3中,提供了第三个参数onCleanup
,数据清理问题解决参考:https://www.jb51.net/article/271466.htm
<template>
<div>
<input v-model="value" />
{{ result }}
</div>
</template>
<script>
export default {
name: "App",
components: {},
data() {
return {
result: "",
timer: 3000,
};
},
methods: {
async getData(newVal) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(newVal);
}, (this.timer -= 1000));
});
},
},
mounted() {
let arr = [];
this.$watch("value", async (newVal, oldValue) => {
let clear = false;
while (arr.length > 0) {
arr.shift()();
}
arr.push(() => {
clear = true; // 利用闭包实现清理操作
});
let result = await this.getData(newVal);
if (!clear) this.result = result;
});
},
};
</script>
vue3 提供了 onCleanup 函数,让用户更加方便使用也解决了清理问题。
watch(
() => this.value,
async (newVal, oldValue, onCleanup) => {
let clear = false;
onCleanup(() => {
clear = true;
});
let result = await this.getData(newVal);
if (!clear) this.result = result as string;
}
);
源码剖析
Vue2中computed
- 计算属性会创建一个计算属性 watcher,这个 watcher (lazy: true),不会立刻执行 ,普通的watcher默认是立刻执行
- 通过 Object. defineProperty 将计算属性定义到实例上
- 当用户取值时会触发 getter,拿到计算属性对应的 watcher,看 dirty 是否为 true,如果为 true 则求值
- 并且让计算属性 watcher 中依赖的属性收集最外层的渲染 watcher(也就是外层的模板渲染watcher,实现视图更新),可以做到依赖的属性变化了,触发计算属性更新 dirty 并且可以触发页面更新
- 如果依赖的值没有发生变化,则采用缓存
Vue3中computed
- 依赖的值变化后,会通知计算属性effect 更新dirty,并且计算属性会触发自己收集的渲染effect执行
- 计算属性effect会收集外层的effect,和Vue2中不一样的就是,计算属性本身拥有了收集能力(trackRefValue(self)),收集当前组件渲染产生的effect,而不是让里面的属性去收集外层的effect
Vue2中watch
Vue2源码:https://github1s.com/vuejs/vue/blob/main/src/core/instance/state.ts#L338
Vue3中watch
Vue3源码:https://github1s.com/vuejs/core/blob/main/packages/runtime-core/src/apiWatch.ts#L171
12.如何理解 reactive、ref 、toRef 和 toRefs?
- reactive::将一个普通对象转换为响应式对象。(采用 new Proxy 进行实现) 通过代理对象访问属性时会进行依赖收集,属性更新时会触发依赖更新,reactive会有拆包操作,reactive包裹的ref,取值不用.value。
- ref: 创建一个包装对象(Wrapper Object)将一个简单的值包装成一个响应式对象,当访问
value
属性时会进行依赖收集,更新value
属性时会触发依赖更新(Object.defineProperty)。(采用类访问器实现) 内部是对象的情况会采用 reactive 来进行处理 - toRef::创建
ref
对象,引用reactive
中的属性。 - toRefs::批量创建
ref
对象,引用reactive
中的属性。
13.watch 和 watchEffect 的区别
- watchEffect 立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。
- watch 侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。
effect 原理
<script type="module">
import {
ReactiveEffect,
reactive,
} from "./node_modules/vue/dist/vue.esm-browser.js";
const state = reactive({ name: "jw" });
const effect = new ReactiveEffect(
// getter
() => {
return state.name;
},
// scheduler
() => {
console.log("数据变化");
}
);
effect.run();
state.name = "shui";
</script>
基于 effect 进行包装
// getter函数
watchEffect(() => {
app.innerHTML = state.name; // 数据变化后,会调用scheduler内部会再次触发effect.run()重新运行getter
});
// 1.getter 函数 2.cb函数
watch(
() => state.name, // 数据变化后,会调用scheduler,内部会调用cb
(newVal, oldVal) => {}
);
14.如何将 template 转换成 render 函数 ?
Vue 中含有模版编译的功能,它的主要作用是将用户编写的 template 编译为 js 中可执行的 render 函数。
Vue 中的模版转化流程
- 1.将 template 模板转换成
ast
语法树 -parserHTML
- 2.Vue2 对静态语法做静态标记 -
optimize
/ Vue3 对 ast 语法进行转化 -transform
- 3.重新生成代码 -
codeGen
Vue3 中的模版转化,做了更多的优化操作。Vue2 仅仅是标记了静态节点而已~
15.在Vue2中 new Vue()这个过程究竟做了什么?
对应源码:https://github1s.com/vuejs/vue/blob/main/src/core/instance/init.ts#L17
-
在 newVue 的时候内部会进行初始化操作。
-
内部会初始化组件绑定的事件,初始化组件的父子关系 p a r e n t paren t parentchilldren $root
-
初始化响应式数据 data、computed、props、watch、method。同时也初始化了 provide 和 inject 方法。内部会对数据进行劫持对象采用 defineProperty 数组采用方法重写。
-
在看一下用户是否传入了 el 属性和 template 或者 render。rende 的优先级更高,如果用户写的是 template,会做模板编译(三部曲)。最终就拿到了 render 函数
-
内部挂载的时候会产生一个 watcher,会调用 render 函数会触发依赖收集。内部还会给所有的响应式数数据增加 dep 属性,让属性记录当前的 watcher(用户后续修改的时候可以触发 watcher 重新渲染)
-
vue 更新的时候采用虚拟 DOM 的方式进行 diff 算法更新。
16.Vue.observable有了解过吗?
- 是Vue2.6中新增的一个方法,可以把普通对象变成一个响应式对象
- 需要引入Vue才能使用
- 可以实现组件跨级、平级共享数据
17.v-if 和 v-for 哪个优先级更高?
v-for 和 v-if 避免在同一个标签中使用。如果遇到需要同时使用时可以考虑写成计算属性的方式。
<!--应当避免这种写法-->
<li v-for="l in arr" v-if="exists"></li>
- 在 Vue2 中解析时,先解析 v-for 在解析 v-if。会导致先循环后在对每一项进行判断,浪费性能。
- 在 Vue3 中 v-if 的优先级高于 v-for。
18.Vue 的生命周期方法有哪些?一般在哪一步发送请求及原因
Vue2 中的生命周期
主要的生命周期有:创建前后, 挂载前后, 更新前后, 销毁前后
- beforeCreate 初始化父子关系及事件,数据观测(data observer) 之前被调用。用此方法一般编写插件的时候会用到。
- created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法等, 但是这里没有$el,一般也不咋用。
- beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
- mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。可以用于获取 DOM 元素
- beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。此时修改数据不会再次出发更新方法
- updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
- beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
- destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
- keep-alive (activated 和 deactivated)
V2 和 V3 中的生命周期对比
生命周期 v2 | 生命周期 v3 | 描述 |
---|---|---|
beforeCreate | beforeCreate | 组件实例被创建之初 |
created | created | 组件实例已经完全创建 |
beforeMount | beforeMount | 组件挂载之前 |
mounted | mounted | 组件挂载到实例上去之后 |
beforeUpdate | beforeUpdate | 组件数据发生变化,更新之前 |
updated | updated | 数据数据更新之后 |
beforeDestroy | beforeUnmount | 组件实例销毁之前 |
destroyed | unmounted | 组件实例销毁之后 |
activated | activated | keep-alive 缓存的组件激活时 |
deactivated | deactivated | keep-alive 缓存的组件停用时调用 |
errorCaptured | errorCaptured | 捕获一个来自子孙组件的错误时被调用 |
- | renderTracked Dev | 调试钩子,响应式依赖被收集时调用 |
- | renderTriggered Dev | 调试钩子,响应式依赖被触发时调用 |
- | serverPrefetch | ssr only,组件实例在服务器上被渲染前调用 |
Vue3 中新增了,组合式 API:生命周期钩子,但是不存在 onBeforeCreate 和 onCreated 钩子
19.Vue中diff算法原理
Diff 概念
vue 基于虚拟 DOM 做更新 。diff 的核心就是比较两个虚拟节点的差异 。Vue 的 diff 算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。
Vue2 Diff 比较流程
Vue2中对应源码:https://github1s.com/vuejs/vue/blob/main/src/core/vdom/patch.ts#L801
- 1.先比较是否是相同节点 key tag标签
- 2.相同节点比较属性,并复用老节点(将老的虚拟 dom 复用给新的虚拟节点 DOM)
- 3.比较儿子节点,考虑老节点和新节点儿子的情况
- 老的没儿子,现在有儿子。 直接插入新的儿子
- 老的有儿子,新的没儿子。直接删除页面节点
- 老的儿子是文本,新的儿子是文本,直接更新文本节点即可
- 老的儿子是一个列表,新的儿子也是一个列表 updateChildren
- 4.优化比较:头头、尾尾、头尾、尾头
- 5.比对查找进行复用
Vue3 中采用最长递增子序列来实现 diff 优化。
Vue3 Diff 算法比较流程
全量 Diff:
- 刚开始默认从头比对,相同节点则复用节点。
- 如果头部节点不一致,我们就从后向前对比,相同节点则复用。
- 默认优化了子节点追加和子节点删除的情况。
- 乱序比对,通过最长递增子序列实现在复用过程中减少节点的移动操作
sync from start
sync from end
common sequence + mount
common sequence + unmount
unknown sequence
build key:index map for newChildren
loop through old children left to be patched and try to patch
move and mount`
Vue3 采用最长递增子序列,求解不需要移动的元素有哪些
20.请说明 Vue 中 key 的作用和原理,谈谈你对它的理解
key 的概念
key
的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。- 当 Vue 正在更新使用
v-for
渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染
key 的作用
- Vue 在 patch 过程中通过 key 可以判断两个虚拟节点是否是相同节点。 (可以复用老节点)
- 无 key 会导致更新的时候出问题
- 尽量不要采用索引作为 key
21.Vue.use 是干什么的?
use 概念
安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入,这样插件中就不在需要依赖 Vue 了。(扩展应用的功能)
插件的功能
- 添加全局指令、全局过滤器(Vue3 不再支持过滤器)、全局组件。
- 通过全局混入来添加一些组件选项。
- 添加实例方法,通过把它们添加到
Vue.prototype
/app.config.globalProperties
上实现。
实现原理
Vue.use = function (plugin: Function | Object) {
// 插件缓存
const installedPlugins =
this._installedPlugins || (this._installedPlugins = []);
if (installedPlugins.indexOf(plugin) > -1) {
// 如果已经有插件 直接返回
return this;
}
// additional parameters
const args = toArray(arguments, 1); // 除了第一项其他的参数整合成数组
args.unshift(this); // 将Vue 放入到数组中
if (typeof plugin.install === "function") {
// 调用install方法
plugin.install.apply(plugin, args);
} else if (typeof plugin === "function") {
// 直接调用方法
plugin.apply(null, args);
}
installedPlugins.push(plugin); // 缓存插件
return this;
};
Vue3 中使用app.use进行插件的注册,原理同 Vue2~
22.Vue.extend 方法的作用?
Vue.extend 概念
Vue2中extend源码:https://github1s.com/vuejs/vue/blob/main/src/core/global-api/extend.ts#L20
使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
data
选项是特例,需要注意 - 在 Vue.extend()
中它必须是函数
var Profile = Vue.extend({
template: "<p>{{firstName}} {{lastName}} aka {{alias}}</p>",
data: function () {
return {
firstName: "Walter",
lastName: "White",
alias: "Heisenberg",
};
},
});
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount("#mount-point");
new Vue().$mount();
分析
- 所有的组件创建时都会调用
Vue.extend
方法进行创建 - 有了此方法我们可以用于手动挂载组件。
- 后端存储的字符串模板我们可以通过 Vue.extend 方法将其进行渲染,但是需要引入编译时。
Vue3 中手动挂载
Vue3 中不在使用
Vue.extend
方法,而是采用render
方法进行手动渲染。
html
<div id="app"></div>
<script type="module">
import { render, h, ref } from "./vue.esm-browser.js";
const App = {
template: `<div id="counter">{{ count }}</div>`,
setup() {
const count = ref(0);
return { count };
},
};
const app = render(h(App), document.getElementById("app"));
</script>
23.Vue 组件 data 为什么必须是个函数?
- 根实例对象
data
可以是对象也可以是函数“单例”,不会产生数据污染情况 - 组件实例对象
data
必须为函数,目的是为了防止多个组件实例对象之间共用一个data
,产生数据污染。所以需要通过工厂函数返回全新的 data 作为组件的数据源
function Vue() {}
Vue.extend = function (options) {
function Sub() {
// 会将data存起来
this.data = this.constructor.options.data;
}
Sub.options = options;
return Sub;
};
let Child = Vue.extend({
data: { name: "xxx" },
});
// 两个组件就是两个实例, 希望数据互不干扰
let child1 = new Child();
let child2 = new Child();
console.log(child1.data.name);
child1.data.name = "jw";
console.log(child2.data.name);
Vue3 一切从组件开始,所以 data 都为函数形式。
24.函数组件的优势?
函数式组件是一种定义自身没有任何状态的组件的方式。它们很像纯函数:接收 props,返回 vnodes。函数式组件在渲染过程中不会创建组件实例 (也就是说,没有 this),也不会触发常规的组件生命周期钩子。
在 Vue2 正常组件是通过Vue.extend
方法进行创建, 函数式组件就是普通的函数,没有 new 的过程。最终就是将返回的虚拟 DOM 变成真实 DOM 替换对应的组件,同时函数式组件不会被记录在组件的父子关系中。
因此在 Vue2 中函数式组件有以下优势:
- 性能优化: 函数式组件相对于常规组件在渲染性能上具有优势。由于函数式组件是无状态的,不包含生命周期钩子和实例状态,渲染时的开销更小。
- 没有 this : 函数式组件不依赖于 this,不再有 this 绑定问题。
- 可读性和维护性: 函数式组件更加简洁和直观。只是一个函数,没有复杂的选项对象和实例属性。这使得代码更易于阅读和维护。
- 易测试: 由于函数式组件是纯函数,因此更容易编写单元测试。
但在 Vue3 中因为所有的组件都不用
new
了,所以在性能上没有了优势,所以不在建议使用函数组件~
25.Vue 中的过滤器了解吗?过滤器的应用场景有哪些?
Vue2中对应源码:https://github1s.com/vuejs/vue/blob/main/src/core/instance/render-helpers/resolve-filter.ts#L6
过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解成纯函数。
{{ message | filterA("arg1", "arg2") | filterB("arg1", "arg2") }}
Vue.filter("filterA", function (value) {
// 返回处理后的值
});
Vue.filter("filterB", function (value) {
// 返回处理后的值
});
常见场景:单位转换、千分符、文本格式化、时间格式化等操作。 这个写个方法、写个方法不香么?Vue3 果断废弃了过滤器…
<p>{{format(number)}}</p>
const format = (n) => {
return parseFloat(n).toFixed(2);
};
26.v-once 的使用场景有哪些
v-once 概念
v-once
是 Vue 中内置指令,只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
v-once 使用场景
<!-- 单个元素 -->
<span v-once>This will never change: {{msg}}</span>
<!-- 有子元素 -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<!-- 组件 -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` 指令-->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
vue3.2 之后,增加了
v-memo
指令,通过依赖列表的方式控制页面渲染。
<div>
<div v-memo="[valueA,valueB]">
<div class="box" v-for="item in arr" :key="item">{{ item }}</div>
</div>
</div>
27.Vue.mixin 的使用场景和原理
Vue.mixin 概念
mixin 可以用来扩展组件,将公共逻辑进行抽离。在需要该逻辑时进行“混入”,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准。
mixin 中有很多缺陷 “命名冲突问题”、“数据来源问题”,Vue3 采用 CompositionAPI 提取公共逻辑非常方便。
mixins 在 Vue 3 支持主要是为了向后兼容,因为生态中有许多库使用到。在新的应用中应尽量避免使用 mixin,特别是全局 mixin。
混入方式
在Vue
中我们可以局部混入跟全局混入。一般情况下全局混入用于编写插件。局部混入用于复用逻辑。
mixin 合并策略
核心就是:对象的合并处理。
- props、methods、inject、computed 同名时会被替换
- data 会被合并
- 生命周期和 watch 方法 会被合并成队列
- components、directives、filters 会在原型链上叠加
组件的扩展除了 mixin 之外还有一个属性叫 extends,但是不怎么常用~~~。
28.Vue 中 slot 是如何实现的?什么时候使用它?
什么是插槽?
插槽设计来源于 Web Components 规范草案,利用slot
进行占位,在使用组件时,组件标签内部内容会分发到对应的 slot 中。
什么时候使用它
通过插槽可以让用户更好的对组件进行扩展和定制化。可以通过具名插槽指定渲染的位置。常用的组件例如:弹框组件、布局组件、表格组件、树组件…
插槽的分类和原理
具名插槽,作用域插槽
Vue2 插槽实现
-
普通插槽的实现
<template> <my> <h1 slot="title">标题</h1> <div slot="content">内容</div> </my> </template> <!-- 编译后的结果 with(this){ return _c('my',[ _c('h1',{attrs:{"slot":"title"},slot:"title"},[_v("标题")]) _c('div',{attrs:{"slot":"content"},slot:"content"},[_v("内容")]) ]) } -->
my.vue
<template> <div> <slot name="title"></slot> <slot name="content"></slot> </div> </template> <!-- 编译后的结果 with(this){ return _c('div',[ _t("title"),_t("content") ]) } -->
-
作用域插槽
<template> <my> <template v-slot="{ article }"> <h1>{{ article.title }}</h1> <div>{{ article.content }}</div> </template> </my> </template> <!-- with(this) { return _c('my', { scopedSlots: _u([ { key: "default", fn: function ({ article }) { return [_c('h1', [_v(_s(article.title))]), _c('div',[ _v(_s(article.content)) ])] } } ]) }) } -->
<template> <div> <slot :article="{ title: '标题', content: '内容' }"></slot> </div> </template> <!-- 编译后的结果 with(this){ return _c('div',[_t("default",null,{"article":{title:'标题',content:'内容'}})]) } -->
普通插槽,渲染在父级, 作用域插槽在组件内部渲染!
Vue3 插槽实现
<template>
<my>
<template #default="{ article }">
<h1>{{ article.title }}</h1>
<div>{{ article.content }}</div>
</template>
</my>
</template>
<!--
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_my = _resolveComponent("my")
return (_openBlock(), _createBlock(_component_my, null, {
default: _withCtx(({ article }) => [
_createElementVNode("h1", null, _toDisplayString(article.title), 1 /* TEXT */),
_createElementVNode("div", null, _toDisplayString(article.content), 1 /* TEXT */)
]),
}))
}
-->
<template>
<div>
<slot :article="{ title: '标题', content: '内容' }"></slot>
</div>
</template>
<!--
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_renderSlot(_ctx.$slots, "default", { article: { title: '标题', content: '内容' } })
]))
}
-->
普通插槽,渲染在父级,作用域插槽在组件内部渲染
29.说说你对双向绑定的理解,以及它的实现原理吗?
双向绑定的概念
vue 中双向绑定靠的是指令 v-model,可以绑定一个动态值到视图上,同时修改视图能改变数据对应的值(能修改的视图就是表单组件) 经常会听到一句话:v-model 是 value + input 的语法糖。
v-model在输入框输入的过程中,中文输入的过程中,并没有做响应式处理
表单元素中的 v-model
内部会根据标签的不同解析出不同的语法。并且这里有“额外”的处理逻辑
- 例如 文本框会被解析成 value + input 事件 (同时处理中文输入问题)
- 例如 复选框会被解析成 checked + change 事件
- …
组件中的 v-model
组件上的 v-model
默认会利用名为 value
的 prop 和名为 input
的事件。对于组件而言 v-model 就是 value + input 的语法糖。可用于组件中数据的双向绑定。
Vue2 中 v-model
名字可以修改:
Vue.component("base-checkbox", {
model: {
prop: "checked",
event: "change",
},
props: {
checked: Boolean,
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`,
});
那组件中如果有多个数据想做双向数据绑定怎么办? 很遗憾在 vue2 中不支持使用多个 v-model 的 (使用过时的
.sync
语法)。vue3 中可以通过以下方法进行绑定。
<my v-model:a="a" v-model:b="b" v-model:c="c"></my>
30.Vue 中.sync 修饰符的作用?
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”,这时可以使用.sync
来实现。v-model
默认只能双向绑定一个属性,这里就可以通过.sync
修饰符绑定多个属性。
<my :value.sync="xxxx"></my>
<!--编译的结果是
with(this){
return _c('my',{
attrs:{"value":xxxx},
on:{"update:value":function($event){xxxx=$event}}
})
}-->
vue3 中
.sync
语法被移除。
31.Vue中递归组件理解
jsx编写el-menu菜模板
32.Vue 组件中写 name 选项有哪些好处及作用?
- 标识组件: 通过设置组件的 name 选项,可以为每个组件指定一个独特的名称,这有助于在开发和调试过程中准确标识和定位组件。
- 增加name之后,keep-alive可以根据name进行缓存操作
- Vue 开发者工具中的组件树显示时:Vue 开发者工具将以树形结构显示组件层次结构,每个组件都以其 name 显示在树中。
- 递归组件: 当你在 Vue.js 中创建递归组件,通常需要使用 name 选项来标识组件。这样 Vue.js 能够正确地识别和渲染递归组件。
- 组件抛出的警告追踪栈信息: 当 Vue.js 组件在内部出现错误时,Vue.js 通常会抛出警告,并提供详细的追踪栈信息以帮助定位和解决问题。
- 更好的根据name查找组件
33.Vue 常用的修饰符有哪些有什么应用场景?
https://cn.vuejs.org/guide/essentials/event-handling.html#event-modifiers
- 表单修饰符 lazy、trim、number
- 事件修饰符 stop、prevent、self、once、capture、passive(滚动优化)、
.native
(标识原生事件,会绑定到根元素上) - 鼠标按键修饰符 left、right、middle
- 键值修饰符 对 keyCode 处理
.sync 修饰符
Vue3 中移除
.sync
、.native
修饰符
34.自定义指令的应用场景
指令的概念
Vue 除了内置指令之外,同时 Vue 也允许用户注册自定义指令来对 Vue 进行扩展。指令的目的在于可以将操作 DOM 的逻辑进行复用。
指令的生命周期
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update
:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。componentUpdated
:指令所在组件的 VNode 及其子 VNode 全部更新后调用。unbind
:只调用一次,指令与元素解绑时调用。
常见的指令编写
- 图片懒加载 v-lazy
- 防抖 v-debounce
- 按钮权限 v-has
- 拖拽指令 v-draggable (mousemove、 mouseup、 monsedown, dragenter、dragover、drop) 可视化拖拽编辑器
- 点击事件处理 v-click-outside
<div v-click-outside="hide">
<input type="text" @focus="show" />
<div v-if="isShow">显示面板</div>
</div>
Vue.directive(clickOutside, {
bind(el, bindings, vnode) {
el.handler = function (e) {
if (!el.contains(e.target)) {
let method = bindings.expression;
vnode.context[method]();
}
};
document.addEventListener("click", el.handler);
},
unbind(el) {
document.removeEventListener("click", el.handler);
},
});
35.说说你对 nextTick 的理解?
好文推荐:https://www.jianshu.com/p/a7550c0e164f
https://segmentfault.com/a/1190000012861862
- Vue 中视图更新是异步的,使用 nextTick 方法可以保证用户定义的逻辑在更新之后执行。
- 可用于获取更新后的 DOM,多次调用 nextTick 会被合并。
当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。
Vue2 中的 nextTick
<div id="app">{{count}}</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
const vm = new Vue({
el: "#app",
data() {
return { count: 0 };
},
mounted() {
// [渲染watcher,nextTick逻辑,数据更新]
// 按照执行顺序存放到队列中,最后异步执行。
this.$nextTick(() => {
console.log(document.getElementById("app").innerHTML);
});
this.count = 100;
},
});
</script>
Vue2 nextTick 实现原理 优雅降级(Promise、MutationObserver、setImmediate、setTimeout) 这里一般会配合浏览器事件环作为面试题。
Vue3 中的 nextTick
<div id="app">{{count}}</div>
<script src="./node_modules/vue/dist/vue.global.js"></script>
<script>
const App = {
el: "#app",
data() {
return { count: 0 };
},
mounted() {
// 当执行mounted之前会创建一个promise,nextTick会被延迟到这个promise之后执行 (值被改为100后再进行渲染)
Vue.nextTick(() => {
console.log(document.getElementById("app").innerHTML);
});
this.count = 100;
},
};
const app = Vue.createApp(App);
app.mount("#app");
</script>
Vue3 中不在考虑 promise 的兼容性,所以 nextTick 的实现原理就是 promise.then 方法。
36.keep-alive 平时在哪里使用?
概念
keep-alive 是 vue 中的内置组件,能在组件切换过程会缓存组件的实例,而不是销毁它们。在组件再次重新激活时可以通过缓存的实例拿到之前渲染的 DOM 进行渲染,无需重新生成节点。
使用场景
动态组件可以采用keep-alive
进行缓存,max控制最大缓存数量
<template>
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
<component :is="component"></component>
</keep-alive>
</template>
在路由中使用 keep-alive
<template>
<!-- vue2写法 -->
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
<router-view></router-view>
</keep-alive>
<!-- vue3写法 -->
<router-view v-slot="{ Component }">
<keep-alive :include="whiteList" :exclude="blackList" :max="count">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
也可以通过 meta 属性指定哪些页面需要缓存,哪些不需要
<template>
<!-- vue2写法 -->
<keep-alive>
<!-- 需要缓存的视图组件 -->
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不需要缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
<!-- vue3写法 -->
<router-view v-slot="{ Component }">
<transition>
<keep-alive>
<!-- 需要缓存的视图组件 -->
<component :is="Component" v-if="route.meta.keepAlive" />
</keep-alive>
<!-- 不需要缓存的视图组件 -->
<component :is="Component" v-if="!route.meta.keepalive" />
</transition>
</router-view>
</template>
原理
vue2 原理
export default {
name: 'keep-alive',
abstract: true, // 不会放到对应的lifecycle
props: {
include: patternTypes, // 白名单
exclude: patternTypes, // 黑名单
max: [String, Number] // 缓存的最大个数
},
created () {
this.cache = Object.create(null) // 缓存列表
this.keys = [] // 缓存的key列表
},
destroyed () {
for (const key in this.cache) { // keep-alive销毁时 删除所有缓存
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () { // 监控缓存列表
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot) // 获得第一个组件
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if ( // 获取组件名 看是否需要缓存,不需要缓存则直接返回
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
// 拿到缓存映射表
const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key // 生成缓存的key
if (cache[key]) { // 如果有key 将组件实例直接复用
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key) // lru算法
} else {
cache[key] = vnode // 缓存组件
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode) // 超过最大限制删除第一个
}
}
vnode.data.keepAlive = true // 在firstComponent的vnode中增加keep-alive属性
}
return vnode || (slot && slot[0])
}
}
vue3 原理
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,
__isKeepAlive: true,
props: {
include: [String, RegExp, Array], // 包含需要缓存的组件名称规则
exclude: [String, RegExp, Array], // 排除不需要缓存的组件名称规则
max: [String, Number] // 最大缓存的组件实例数量
},
setup(props: KeepAliveProps, { slots }: SetupContext) {
const instance = getCurrentInstance()!
const sharedContext = instance.ctx as KeepAliveContext
// 缓存组件实例的 Map
const cache: Cache = new Map()
// 缓存组件实例的键集合
const keys: Keys = new Set()
let current: VNode | null = null
const parentSuspense = instance.suspense
const {
renderer: {
p: patch,
m: move,
um: _unmount,
o: { createElement }
}
} = sharedContext
const storageContainer = createElement('div') // 用于存储组件对应DOM的容器
// 激活组件时调用的方法
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
const instance = vnode.component!
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// ...
}
// 失活组件时调用的方法
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
// ...
}
// 卸载组件
function unmount(vnode: VNode) {
// reset the shapeFlag so it can be properly unmounted
resetShapeFlag(vnode)
_unmount(vnode, instance, parentSuspense, true)
}
// 处理缓存
function pruneCache(filter?: (name: string) => boolean) {
cache.forEach((vnode, key) => {
const name = getComponentName(vnode.type as ConcreteComponent)
if (name && (!filter || !filter(name))) {
pruneCacheEntry(key)
}
})
}
// 移除头部缓存
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode
if (!current || !isSameVNodeType(cached, current)) {
unmount(cached)
} else if (current) {
resetShapeFlag(current)
}
cache.delete(key)
keys.delete(key)
}
// 监听 include 和 exclude 属性的变化,然后清理缓存
watch(
() => [props.include, props.exclude],
([include, exclude]) => {
include && pruneCache(name => matches(include, name))
exclude && pruneCache(name => !matches(exclude, name))
},
// prune post-render after `current` has been updated
{ flush: 'post', deep: true }
)
// 在渲染后缓存子树
let pendingCacheKey: CacheKey | null = null
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
// ...
return () => {
// ...
if (cachedVNode) {
vnode.el = cachedVNode.el // 复用缓存dom
vnode.component = cachedVNode.component
if (vnode.transition) {
// recursively update transition hooks on subTree
setTransitionHooks(vnode, vnode.transition!)
}
// 避免 vnode 作为新的组件实例被挂载
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// LRU算法
keys.delete(key)
keys.add(key)
} else {
keys.add(key)
// prune oldest entry
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value) // 删除缓存中的第一个
}
}
// 避免组件卸载
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
return isSuspense(rawVNode.type) ? rawVNode : vnode
}
}
}
核心原理就是缓存 + LRU 算法
keep-alive 中数据更新问题
keep-alive中如果数据更新了,组件不会重新渲染
beforeRouteEnter:在有 vue-router 的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
next(vm=>{
vm.getData() // 获取数据
})
},
actived:在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子
activated(){
this.getData() // 获取数据
},
37.Vue 中使用了哪些设计模式?
单例模式
单例模式就是整个程序中有且仅有一个实例 (pinia 中的某一个 store)
import { useCounterStore } from "./counterStore.js";
// 组件 1
const counterStore1 = useCounterStore();
// 组件 2
const counterStore2 = useCounterStore();
// 在组件1和组件2中使用的是同一个存储实例
store 实现原理
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
// ...
if (!pinia._s.has(id)) {
if (isSetupStore) { // 缓存实例
createSetupStore(id, setup, options, pinia)
} else {
createOptionsStore(id, options as any, pinia)
}
}
const store: StoreGeneric = pinia._s.get(id)!
return store as any
}
工厂模式
传入参数即可创建实例 (
createComponentInstance
)
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null
) {
const instance: ComponentInternalInstance = {
vnode,
parent,
suspense,
// ...
};
return instance;
}
发布订阅模式
订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。(
on、emit
)
<!-- 事件绑定 -->
<MyComponent @myFn="a" @myFn="b"></MyComponent>
<!-- 触发事件 emit("myFn") -->
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_MyComponent = _resolveComponent("MyComponent");
return (
_openBlock(),
_createBlock(
_component_MyComponent,
{
onMyFn: [_ctx.a, _ctx.b], // 订阅
},
null,
8
)
);
}
代理模式
代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。(
toRef
)
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true
constructor(
private readonly _object: T,
private readonly _key: K,
private readonly _defaultValue?: T[K]
) {}
// ref.value.key -> 访问原始 object.key
get value() {
const val = this._object[this._key]
return val === undefined ? this._defaultValue! : val
}
set value(newVal) {
this._object[this._key] = newVal
}
get dep(): Dep | undefined {
return getDepFromReactive(toRaw(this._object), this._key)
}
}
中介者模式
中介者是一个行为设计模式,通过提供一个统一的接口让系统的不同部分进行通信。 (
pinia
)
外观模式
提供了统一的接口,用来访问子系统中的一群接口,以便隐藏底层复杂性。(
baseCompile
)
// 可以粗略理解成外观模式 (主要目标是编译而不是隐藏复杂性,但抽象了底层的编译细节)
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
// ...
// 1.编译
const ast = isString(template) ? baseParse(template, options) : template;
const [nodeTransforms, directiveTransforms] =
getBaseTransformPreset(prefixIdentifiers);
// 2.转化
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []), // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
),
})
);
// 3.生成代码
return generate(
ast,
extend({}, options, {
prefixIdentifiers,
})
);
}
装饰模式
Vue2
装饰器的用法 (对功能进行增强 @) 古老写法
import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
@Prop(Number) readonly propA: number | undefined
@Prop({ default: 'default value' }) readonly propB!: string
@Prop([String, Boolean]) readonly propC: string | boolean | undefined
}
策略模式
策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案。 (
Vue2中mergeOptions
)
function mergeField(key: any) {
const strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options;
// ...
strats.data = fn1;
strats[hook] = fn2;
strats[type + "s"] = fn3;
strats.watch = fn4;
strats.props = strats.methods = strats.inject = strats.computed = fn5;
strats.provide = fn6;
观察者模式
观察者模式是自动的,发布订阅是手动触发的
Vue2 中
watcher
&dep
的关系,无需用户主动触发更新,状态变化时会自动更新。
export default class Dep {
addSub(sub: DepTarget) { // 访问属性时, 订阅对应的watcher
this.subs.push(sub)
}
notify(info?: DebuggerEventExtraInfo) { // 属性变化时,发布对应的watcher
const subs = this.subs.filter(s => s) as DepTarget[]
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
sub.update()
}
}
}
export default class Watcher implements DepTarget {
//...更新逻辑
update() {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
}
38.Vue 中的性能优化有哪些?
- Vue2 中数据层级不易过深,合理设置响应式数据;
- Vue2 非响应式数据可以通过 Object.freeze()方法冻结属性;
- Vue2 中采用函数式组件 -> 函数式组件开销低;
- 使用数据时缓存值的结果,不频繁取值;
- 合理设置 Key 属性;
v-show
和v-if
的选取;- 控制组件粒度 -> Vue 采用组件级更新;
- 采用异步组件 -> 借助构建工具的分包的能力;
- 合理使用
keep-alive
、v-once
、v-memo
进行逻辑优化; - 分页、虚拟滚动、时间分片等策略…
39.单页应用首屏加载速度慢的怎么解决?
-
使用路由懒加载、异步组件,实现组件拆分,减少入口文件体积大小,优化体验骨架屏
-
抽离公共代码,采用 splitChunks 进行代码分割。
-
组件加载采用按需加载的方式。
-
静态资源缓存,采用 HTTP 缓存 (强制缓存、对比缓存)、使用 localStorage 实现缓存资源。
-
图片资源的压缩,雪碧图、对小图片进行 base64 减少 http 请求。
-
打包时开启 gzip 压缩处理
compression-webpack-plugin
插件-
Brotli 压缩
-
Brotli是开源的一种新型压缩算法(2015 年 Google 推出,Github 地址:https://github.com/google/brotli ),Brotli压缩比Gzip压缩性能更好。开启Brotli压缩功能后,CDN节点会对资源进行智能压缩后返回,缩小传输文件大小,提升文件传输效率,减少带宽消耗。
启用Brotli压缩可以将CDN流量额外减少20%,相较于Gzip。在各种情况下,Brotli的性能比Gzip高出17-25%,特别是当设置为级别1时,超过了Gzip级别9的压缩效果。(数据来自 Google 数据报告:https://quixdb.github.io/squash-benchmark/unstable/ )
-
-
静态资源采用 CDN 提速。终极的手段
-
使用 SSR 对首屏做服务端渲染。
40.Vue 项目中你是如何解决跨域的呢?
跨域是浏览器同源策略导致的,这个是浏览器的行为 (协议、主机名、端口的不同都会导致跨域问题)。服务端和服务端之间进行通信是没有跨域问题的。跨域的实现方案有很多种。不过一般常用的就那么几种。
- CORS (Cross-Origin Resource Sharing,跨域资源共享) 由服务端设置,允许指定的客户端访问服务器。
- 构建工具中设置反向代理、使用 Nginx 做反向代理。
- 使用 Websocket 进行通信。
- 搭建 BFF(Backend For Frontend) 层解决跨域问题。
41.Vue 项目中有封装过 axios 吗?主要是封装哪方面的?
- 设置请求超时时间。
- 根据项目环境设置请求路径。
- 设置请求拦截,自动添加 Token。
- 设置响应拦截,对响应的状态码或者数据进行格式化。
- 增添请求队列,实现 loading 效果。
- 维护取消请求 token,在页面切换时通过导航守卫可以取消上个页面中正在发送的请求。
class AjaxRequest {
constructor() {
// development production
this.baseURL =
process.env.NODE_ENV !== "production" ? "http://localhost:3000/api" : "/"; // 基础路径
this.timeout = 3000; // 超时时间
this.queue = {};
}
setInterceptor(instance, url) {
instance.interceptors.request.use(
(config) => {
// 每次请求前 将token 放到请求中
config.headers.token = localStorage.getItem("token") || "";
// 每次请求的时候 都拿到一个取消请求的方法
let Cancel = axios.CancelToken; // 产生一个请求令牌
config.cancelToken = new Cancel(function (c) {
store.commit(types.PUSH_TOKEN, c);
});
// 只要页面变化 就要去依次调用cancel方法 路由的钩子 beforeEach
// 显示loading
if (Object.keys(this.queue).length === 0) {
this.toast = Toast.$create({
txt: "正在加载", // 每次显示toast组件时 都叫 正在加载 否则别人把txt的值改了
time: 0,
});
this.toast.show(); // 如果没有请求过 显示loading
}
// 请求前 增加请求队列
this.queue[url] = url; // 存入队列中
return config;
},
(err) => {
return Promise.reject(err);
}
);
instance.interceptors.response.use(
(res) => {
// 响应拦截, 关闭loading
delete this.queue[url];
if (Object.keys(this.queue).length === 0) {
this.toast.hide(); // 当队列被清空隐藏掉即可
}
if (res.data.code === 0) {
return res.data.data;
} else {
return Promise.reject(res.data);
}
},
(err) => {
delete this.queue[url]; // 请求完成后删除对应的url
if (Object.keys(this.queue).length === 0) {
this.toast.hide(); // 当队列被清空隐藏掉即可
}
return Promise.reject(err);
}
);
}
request(options) {
let instance = axios.create();
let config = {
...options,
baseURL: this.baseURL,
timeout: this.timeout,
};
this.setInterceptor(instance, options.url); // 给这个实例增加拦截功能
return instance(config); // 返回的是一个promise
}
}
export default new AjaxRequest();
42.vue 要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?
- 登录鉴权:用户登录后返回 Token,前端将 Token 保存到本地,作为用户登录的凭证,每次发送请求时会携带 Token,后端会对
- Token 进行验证。当页面刷新时我们可以使用 Token 来获得用户权限。
- 访问权限:根据用户是否登录判断能否访问某个页面,通过路由守卫实现判断用户是否有此权限。
- 页面权限:前端配置的路由分为两部分 “通用路由配置” 和 “需要权限的路由配置”。在权限路由中增加访问权限 meta(备注)。用户登录后可得到对应的权限列表,通过权限列表筛查出对应符合的路由信息,最后通过 addRoutes 方法,动态添加路由。
- 按钮权限:按钮权限一般采用自定义指令实现,当用户登录时后端会返回对应的按钮权限,在按钮上使用此指令,指令内部会判断用户是否有此按钮权限,如果没有则会移除按钮。
43.Vue 中异步组件的作用及原理
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。主要采用
ES 模块动态导入
配合 Vite 和 Webpack 这样的构建工具实现打包时的代码分割。
Vue2 异步组件的写法
-
回调写法
{ components: { "my-component": (resolve, reject) => { setTimeout(function () { resolve({ render(h){ return h('div','hello') }, }); }, 1000); }, }, }
-
Promise 写法
{ components: { "my-component": () => import(/* webpackChunkName:"B4" */ "./components/B4.vue"), }, }
-
对象写法
const AsyncComponent = () => ({ // 需要加载的组件 (应该是一个 `Promise` 对象) component: import("./MyComponent.vue"), // 异步组件加载时使用的组件 loading: LoadingComponent, // 加载失败时使用的组件 error: ErrorComponent, // 展示加载时组件的延时时间。默认值是 200 (毫秒) delay: 200, // 如果提供了超时时间且组件加载也超时了,则使用加载失败时使用的组件。默认值是:`Infinity` timeout: 3000, });
异步组件原理
- 默认渲染异步占位符节点
- 组件加载完毕后调用 $forceUpdate 强制更新,渲染加载完毕后的组件。
Vue3 异步组件写法
使用defineAsyncComponent
函数定义异步组件。这个函数接受一个工厂函数,工厂函数可以返回一个 Promise,当 Promise 解析后,组件将被加载并渲染。推荐的做法是将异步组件和 工程化工具 的 code-splitting 功能一起配合使用。
import { defineAsyncComponent } from "vue";
const AsyncComponent = defineAsyncComponent(() =>
import("./AsyncComponent.vue")
);
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import("./Foo.vue"),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000,
});
- 先基于状态,默认渲染异步占位符节点。
- 组件加载完毕后,更新状态,渲染加载完毕的组件。
44.Vue-Router 有几种钩子函数,具体是什么及执行流程是怎样的?
- 航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫 (2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
模拟守卫运行
// 1.实现sleep方法用于延迟
async function sleep(n = 1000) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, n);
});
}
// 2.模拟离开钩子
const routeLeaveGuards = [
() => {
console.log("leave1");
return sleep(3000);
},
() => {
console.log("leave2");
},
];
// 3.模拟进入钩子
const beforeEachGuards = [
(next) => {
console.log("each1");
setTimeout(() => {
next();
}, 10000);
},
() => {
console.log("each2");
},
];
// 4.当前要运行的钩子,将钩子转化成promise
let guards = [];
function guardToPromiseFn(guard) {
return () =>
new Promise((resolve, reject) => {
const next = resolve;
let guardReturn = guard(next);
Promise.resolve(guardReturn).then(next);
});
}
for (let guard of routeLeaveGuards) {
guards.push(guardToPromiseFn(guard));
}
// 5.组合钩子
function runGuardQueue(guards) {
return guards.reduce(
(promise, guard) => promise.then(() => guard()),
Promise.resolve()
);
}
runGuardQueue(guards).then(() => {
guards = [];
for (let guard of beforeEachGuards) {
guards.push(guardToPromiseFn(guard));
}
return runGuardQueue(guards);
});
45.Vue-Router 几种模式的区别?
Hash 模式
- 工作原理:在 URL 中使用哈希(#)来管理路由。例如,#/home。哈希变化时,浏览器不会重新加载页面,而是根据哈希路径变化更新页面视图。
- 优点:兼容性很好,因为哈希变化不会导致浏览器向服务器发送请求,所以不会出现 404 错误。这使得在旧浏览器和不支持 HTML5 API 的环境中使用很方便。
- 缺点:URL 中有 # 符号,不够美观,不利于 SEO 优化,因为搜索引擎通常不会解析哈希部分的内容。哈希部分的内容也无法被服务器端获取。
History 模式
- 工作原理:使用 HTML5 History API 来管理路由,URL 不再需要 # 号。例如,/home,根据路径变化更新页面视图。
- 优点:URL 更美观,不包含 # 符号,对 SEO 友好。
- 缺点:但是强制刷新时,浏览器会向服务器发送请求。需要服务器端的额外配置,以确保在任何路由请求下都返回应用的入口 HTML,否则会导致 404 错误。
- vue-router4中就是借助于history实现hash模式
memory 模式
- 工作原理:适用于不支持浏览器 API 的环境,如服务器端渲染(Server-Side Rendering,SSR)。它不依赖于浏览器历史。
- 用途:通常在服务器端渲染或非浏览器环境中使用。
46.vue 项目本地开发完成后部署到服务器后报 404 是什么原因呢?
如果你使用的是 Vue Router 的 history 模式。刷新时会像服务端发起请求,服务端无法响应到对应的资源,所以会出现 404 问题。需要
在服务端进行处理将所有请求重定向到你的 Vue 应用的入口文件。
配置后服务端不会在出现 404 问题,Vue 应用中要覆盖所有的路由情况。 配置方式
47.谈一下你对 vuex 的个人理解
概念
- Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。采用集中式存储管理应用的所有组件的状态。核心就是解决数据的共享。
- 以相应的规则保证状态以一种可预测的方式发生变化。
状态修改
- 组件中
commit('mutation')
-> 修改状态 - 组件中
dispatch(action)
(为了解决接口的复用问题,封装公共的逻辑) ->commit('mutation')
-> 修改状态
缺点
Vuex 中 store 只有一份,复杂的数据需要依赖于模块。Vuex 状态是一个树状结构,最终会将模块的状态挂载到根模块上。
- 模块和状态的名字冲突。
- 数据不够扁平化、调用的时候过长。
- 更改状态 mutation 和 action 的选取。
- 模块需要增加 namespaced
- 对 TS 支持并不友好
- 在 Vue3 中使用不支持 composition Api
- …
原理
对于 Vuex3 核心就是通过 new Vue()创建了一个 Vue 实例,进行数据共享。
对于 Vuex4 核心就是通过创建一个响应式对象进行数据共享 reactive()
在 Vue3 中使用 pinia 进行状态管理,不再使用 Vuex
48.如何监听vuex中数据的变化
- 通过 watch 监控 vuex 中状态变化
- 通过 store. Subscribe 监控状态变化
- Vue2对应源码:https://github1s.com/vuejs/vuex/blob/main/src/store.js#L199
49.页面刷新后 Vuex 的数据丢失怎么解决?
Vuex 的数据是保存在前端应用的内存中的,刷新页面会导致内存清空,数据丢失
- 使用持久化存储:将 Vuex 的数据保存到浏览器的本地存储(LocalStorage 或 SessionStorage)也可以采用
vuex-persistedstate
持久化插件进行本地存储。 - 每次获取数据前检测 Vuex 数据是否存在,不存在则发请求重新拉取数据,存储到 Vuex 中,存在则调用store.replaceStace替换。
- 应用加载时获取存储的状态进行替换。
- 状态变化后将状态同步到浏览器存储中。
那如何监听 Vuex 中数据的变化?
- 通过 watch 监控 vuex 中状态变化。
- 通过 store.subscribe (本质也是 watch) 监控状态变化。
50.mutation 和 action 的区别
- 在 action 中可以处理异步逻辑,可以获取数据后将结果提交给 mutation,mutation 中则是修改 state。
- 在 action 中可以多次进行 commit 操作,包括 action 中也可以调用 action。
- 在非 mutation 中修改数据,在严格模式下会发生异常
- dispatch 时会将 action 包装成 promise,而 mutation 则没进行包装
action 异步逻辑
dispatch (_type, _payload) {
// ...
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
// 最终返回promise
return new Promise((resolve, reject) => {
// 等待所有action执行完毕
result.then(res => {
// ...
resolve(res)
}, error => {
// ...
reject(error)
})
})
}
严格模式下发生异常
class Store {
commit(_type, _payload, _options) {
this._withCommit(() => {
// commit 中的处理
entry.forEach(function commitIterator(handler) {
handler(payload);
});
});
}
_withCommit(fn) {
const committing = this._committing;
this._committing = true;
fn(); // 如果函数内部有异步修改状态逻辑,则watch时会报错
this._committing = committing;
}
}
function enableStrictMode(store) {
watch(
() => store._state.data,
() => {
if (__DEV__) {
assert(
store._committing,
`do not mutate vuex store state outside mutation handlers.`
);
}
},
{ deep: true, flush: "sync" } // 同步监控
);
}
51.有使用过 vuex 的 module 吗?在什么情况下会使用?
使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
52.Vue3 中 CompositionAPI 的优势是?
-
在 Vue2 中采用的是 OptionsAPI,用户提供的 data, props, methods, computed, watch 等属性(用户编写)复杂业务逻辑会出现反复横跳问题,完整的功能散落到各个方法中)
-
Vue2 中所有的属性都是通过 this 访问,this 存在指向明确问题
-
Vue2 中很多未使用方法或属性依旧会被打包,并且所有全局 API 都在\ue 对象上公开。Composition API 对 tree-shaking 更加友好,代码也更容易压缩
-
组件逻辑共享问题,Vue2 采用 mixins 实现组件之间的逻辑共享;但是会有数据来源不明确,命名冲突等问题。Vue3 采用 CompositionAPI 提取公共逻辑非常方便
-
简单的组件仍然可以采用 OptionsAPI 进行编写,compositionAPI 在复杂的逻辑中有着明显的优势
53.Vue3 有了解过吗?能说说跟 Vue2 的区别吗?
-
Vue3.0 更注重模块上的拆分,在 2.0 中无法单独使用部分模块。需要引入完整的 Vuejs(例如只想使用使用使用使用响应式部分,但是需要引入完整的 Vuejs), OVue3 中的模块之间耦合度,模块可以独立使用。拆分模块
-
Vue2 中很多方法挂载到了实例中导致没有使用也会被打包(还有很多组件也是一样)。通过构建工具 Tree-shaking 机制实现按需引入,减少用户打包后体积。重写 API
-
Vue3 允许自定义渲染器,扩展能力强。不会发生以前的事情,改写 Vue 源码改造渲染方式。扩展更方便
-
在 Vue2 的时候使用 defineProperty 来进行数据的劫持,需要对属性进行重写添加 getter 及 setter 性能差。
-
当新增属性和删除属性时无法监控变化。需要通过 s e t 、 set、 set、delete 实现
-
数组不采用 defineProperty 来进行劫持(浪费性能,对所有索引进行劫持会造成性能浪费)需要对数组单独进行处理
-
Diff 算法也进行了重写。
-
Vue3 模板编译优化,采用 PatchFlags 优化动态节点,采用 BlockTree 进行靶向更新等
-
相比 Vue2 来说 Vue3 新增了很多新的特性(Fragment、teleport、suspense)
54.Vue项目中的错误如何处理的
errorCaptured 钩子
可以捕获一个来自后代组件的错误时被调用,如果全局的 config.errorHandler 被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报。
父组件(errorCaptured)-》 子组件(errorCaptured)-》 孙子组件出错时,错误会一直向上抛。如果 errorCaptured 中返回 false 则会阻断传播。
全局设置错误处理
如果在组件渲染时出现运行错误,错误将会被传递至全局 config.errorHandler
配置函数。Vue3中是app.config.errorHandler
Vue.config.errorHandler = (err, vm, info) => {
console.log(err, vm, info);
};
接口异常处理
instance.interceptors.response.use(
(res) => {
return res.data;
},
(err) => {
let res = err.response;
if (res.status >= 400) {
handleError(response); // 统一处理接口异常
}
return Promise.reject(error);
}
);
收集到错误后,提交到前端监控系统中,这样我们可以分析前端代码的异常信息啦
55.谈谈 Vue3 中模板编译做了哪些优化?
PatchFlags 优化
Diff 算法无法避免新旧虚拟 DOM 中无用的比较操作,有一些节点是动态的,则通过 patchFlags 来标记动态内容,可以实现快速 diff 算法
<div>
<h1>Hello Jiang</h1>
<span>{{name}}</span>
</div>
此 template 经过模板编译会变成以下代码:
const {
createElementVNode: _createElementVNode,
toDisplayString: _toDisplayString,
createTextVNode: _createTextVNode,
openBlock: _openBlock,
createElementBlock: _createElementBlock,
} = Vue;
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock("div", null, [
_createElementVNode("h1", null, "Hello Jiang"),
_createTextVNode(),
_createElementVNode(
"span",
null,
_toDisplayString(_ctx.name),
1 /* TEXT */
),
])
);
};
创建虚拟节点
生成的虚拟 DOM 是:
{
type: "div",
__v_isVNode: true,
children:[
{type: 'h1', props: null, key: null, …}
{type: Symbol(), props: null, key: null, …}
{type: 'span', props: null, key: null, …}
],
dynamicChildren:[{type: 'span', children: _ctx.name, patchFlag: 1}]
}
此时生成的虚拟节点多出一个 dynamicChildren 属性。这个就是 block 的作用,block 可以收集所有后代动态节点。这样后续更新时可以直接跳过静态节点,实现靶向更新
动态标识
export const enum PatchFlags {
TEXT = 1, // 动态文本节点
CLASS = 1 << 1, // 动态class
STYLE = 1 << 2, // 动态style
PROPS = 1 << 3, // 除了class\style动态属性
FULL_PROPS = 1 << 4, // 有key,需要完整diff
HYDRATE_EVENTS = 1 << 5, // 挂载过事件的
STABLE_FRAGMENT = 1 << 6, // 稳定序列,子节点顺序不会发生变化
KEYED_FRAGMENT = 1 << 7, // 子节点有key的fragment
UNKEYED_FRAGMENT = 1 << 8, // 子节点没有key的fragment
NEED_PATCH = 1 << 9, // 进行非props比较, ref比较
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
DEV_ROOT_FRAGMENT = 1 << 11,
HOISTED = -1, // 表示静态节点,内容变化,不比较儿子
BAIL = -2 // 表示diff算法应该结束
}
BlockTree
为什么我们还要提出 blockTree 的概念? 只有 block 不就挺好的么? 问题出在 block 在收集动态节点时是忽略虚拟 DOM 树层级的。
<div>
<p v-if="flag">
<span>{{a}}</span>
</p>
<div v-else>
<span>{{a}}</span>
</div>
</div>
这里我们知道默认根节点是一个 block 节点,如果要是按照之前的套路来搞,这时候切换 flag 的状态将无法从 p 标签切换到 div 标签。 解决方案:就是将不稳定的结构也作为 block 来进行处理
不稳定结构
所谓的不稳结构就是 DOM 树的结构可能会发生变化。不稳定结构有哪些呢? (v-if/v-for/Fragment)
v-if
<div>
<div v-if="flag">
<span>{{a}}</span>
</div>
<div v-else>
<p><span>{{a}}</span></p>
</div>
</div>
编译后的结果:
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock("div", null, [
_ctx.flag
? (_openBlock(),
_createElementBlock("div", { key: 0 }, [
_createElementVNode(
"span",
null,
_toDisplayString(_ctx.a),
1 /* TEXT */
),
]))
: (_openBlock(),
// 给节点增加key
_createElementBlock("div", { key: 1 }, [
_createElementVNode("p", null, [
_createElementVNode(
"span",
null,
_toDisplayString(_ctx.a),
1 /* TEXT */
),
]),
])),
])
);
};
Block(div)
Blcok(div,{key:0})
Block(div,{key:1})
父节点除了会收集动态节点之外,也会收集子 block。 更新时因 key 值不同会进行删除重新创建
v-for
随着v-for
变量的变化也会导致虚拟 DOM 树变得不稳定
<div>
<div v-for="item in fruits">{{item}}</div>
</div>
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(true),
_createElementBlock(
_Fragment,
null,
_renderList(_ctx.fruits, (item) => {
return (
_openBlock(),
_createElementBlock("div", null, _toDisplayString(item), 1 /* TEXT */)
);
}),
256 /* UNKEYED_FRAGMENT */
)
);
}
可以试想一下,如果不增加这个 block,前后元素不一致是无法做到靶向更新的。因为 dynamicChildren 中还有可能有其他层级的元素。同时这里还生成了一个 Fragment,因为前后元素个数不一致,所以称之为不稳定序列。
稳定 Fragment
这里是可以靶向更新的, 因为稳定则有参照物
<div>
<div v-for="item in 3">{{item}}</div>
</div>
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock("div", null, [
(_openBlock(),
_createElementBlock(
_Fragment,
null,
_renderList(3, (item) => {
return _createElementVNode(
"div",
null,
_toDisplayString(item),
1 /* TEXT */
);
}),
64 /* STABLE_FRAGMENT */
)),
])
);
}
静态提升
<div>
<span>hello</span>
<span a="1" b="2">{{name}}</span>
<a><span>{{age}}</span></a>
</div>
我们把模板直接转化成 render 函数是这个酱紫的,那么问题就是每次调用render
函数都要重新创建虚拟节点。
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock("div", null, [
_createElementVNode("span", null, "hello"),
_createElementVNode(
"span",
{
a: "1",
b: "2",
},
_toDisplayString(_ctx.name),
1 /* TEXT */
),
_createElementVNode("a", null, [
_createElementVNode(
"span",
null,
_toDisplayString(_ctx.age),
1 /* TEXT */
),
]),
])
);
}
const _hoisted_1 = /*#__PURE__*/ _createElementVNode(
"span",
null,
"hello",
-1 /* HOISTED */
);
const _hoisted_2 = {
a: "1",
b: "2",
};
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock("div", null, [
_hoisted_1,
_createElementVNode(
"span",
_hoisted_2,
_toDisplayString(_ctx.name),
1 /* TEXT */
),
_createElementVNode("a", null, [
_createElementVNode(
"span",
null,
_toDisplayString(_ctx.age),
1 /* TEXT */
),
]),
])
);
}
静态提升则是将静态的节点或者属性提升出去,标记成跳过diff算法。静态提升是以树为单位。也就是说树中节点有动态的不会进行提升。
预字符串化
静态提升的节点都是静态的,我们可以将提升出来的节点字符串化。 当连续静态节点超过 20 个时,会将静态节点序列化为字符串。
<div>
<span></span>
... ...
<span></span>
</div>
const _hoisted_1 = /*#__PURE__*/ _createStaticVNode("<span>....</span>", 20);
缓存函数
<div @click="e=>v=e.target.value"></div>
每次调用 render 的时都要创建新函数
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock(
"div",
{
onClick: (e) => (_ctx.v = e.target.value),
},
null,
8 /* PROPS */,
["onClick"]
)
);
}
开启函数缓存后,函数会被缓存起来,后续可以直接使用
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock("div", {
onClick: _cache[0] || (_cache[0] = (e) => (_ctx.v = e.target.value)),
})
);
}
56.你知道哪些 Vue3 新特性?
-
Composition API
- 使用函数的方式编写 vue 组件。
- 组合式 API (响应式 API
ref()、reactive()
,生命周期钩子onMounted()、onUnmounted()
,依赖注入inject()、provide()
) - 组合式 API 并不是函数式编程。
-
SFC Composition API Syntax Sugar (
参考文档:Vue3 迁移指南
56.Vue3 对⽐ Vue2 的变化
- 性能优化(更快):
- 使用了Proxy替代 Object.defineProperty 实现响应式。(为什么?defineProperty 需要对属性进行递归重写添加
getter
及setter
性能差,同时新增属性和删除属性时无法监控变化,需要 s e t 、 set、 set、delete 方法。此方法对数组劫持性能差,同时不支持 map 和 set 的数据结构。) - 模板编译优化。给动态节点增添 PatchFlag 标记;对静态节点进行静态提升;对事件进行缓存处理等。
- Diff 算法优化,全量 diff 算法中采用最长递增子序列减少节点的移动。在非全量 diff 算法中只比较动态节点,通过 PatchFlag 标记更新动态的部分。
- 使用了Proxy替代 Object.defineProperty 实现响应式。(为什么?defineProperty 需要对属性进行递归重写添加
- 体积优化(更小):
- Vue3 移除了不常用的 API
- 移除 inline-template (Vue2 中就不推荐使用)
- o n 、 on、 on、off、$once (如果有需要可以采用 mitt 库来实现)
- 删除过滤器 (可以通过计算属性或者方法来实现)
- c h i l d r e n 移除(可以通过 p r o v i d e , i n j e c t 方法构建 children移除 (可以通过provide,inject方法构建 children移除(可以通过provide,inject方法构建children)
- 移除
.sync
.native
)修饰符 (.sync
通过v-model:xxx
实现,.native
为 Vue3 中的默认行为) 以及不在支持 keycode 作为v-on
修饰符(@keyup.13 不在支持) - 移除全局 API。Vue.component、Vue.use、Vue.directive (将这些 api 挂载到实例上)
- 通过构建工具 Tree-shaking 机制实现按需引入,减少用户打包后体积。
- Vue3 移除了不常用的 API
- 支持自定义渲染器:
- 用户可以自定义渲染 API 达到跨平台的目的。扩展能力更强,无需改造 Vue 源码。
- TypeScript 支持:
- Vue3 源码采用 Typescript 来进行重写 , 对 Ts 的支持更加友好。
- 源码结构变化:
- Vue3 源码采用 monorepo 方式进行管理,将模块拆分到 package 目录中,解耦后可单独使用。