1. 说下vite的原理
vite主要是基于esbuild跟rollup,依靠浏览器自身ESM编译功能,实现机制开发体验的新一代构建工具。具体的实现原理主要有:
esbuild编译,esbuild采用go编写,编译速度更快。
依赖预构建,模块化兼容,现在的项目中仍然存在多少模块化规范的代码,vite在预构建阶段讲模块化代代转换成浏览器
可识别的ESM;
利用esbuild,将大量内部模块的ESM关系转换成单个模块,以减少import模块的请求次数。
缓存:充分利用http缓存做优化,文件系统缓存,预构建阶段,将构建后的依赖缓存到node_modules/vite,相关的配置
发生修改时,才重新构建,提高构建速度。
重写模块路径。
优势:
构建速度非常快
高度集成,开箱就用
基于ESM急速热更新,无需打包编译。
基于esbuild的依赖预处理,比webpack等node编写的编译器快几个数量级。
兼容rollup庞大的插件机制,插件开发更加便捷。
天然支持TS
不足:
vue支持较多,对react支持不够
实践较少
线上跟线下打包的最终代码不一致。
2. router && route
$router指的是全局路由对象,相当于获取整个路由文件, beforeEach beforeResolve push replace方法
$route指的是当前的路由,是一个只读属性
3. vue中hash跟history方式的区别
hash模式是#hash的形式,当#后面的内容发生变化时,并不会重新请求,而是会触发hasChange事件
histroy模式主要的原理是h5的pushState跟replaceState的原理,二者的区别是
hash模式改变的路由是#后面的内容,而history是整个url
history需要后端支持,一般用统一的nginx配置就好
4.computes属性是如何进行缓存的
计算属性会被加入到一个依赖栈,当依赖栈里的某一个数据被改了之后,会派发更新,将代表数据是否脏的字
段dirty置为true,等到下次读取的时候,如果dirty为true,那么就会重新计算。
5. vue-loader做了哪些事情
vue-loader将.vue文件转化为浏览器可识别的javascript。
vue-loader的工作原理:
template部分通过compile生成render,staticRanderFns
获取scripts部分返回的配置项对下scriptExports
styles部分,会通过css-loader,vue-style-loader,添加到head中去,或者是通过css-loader,miniCssExtractPlugins提取到一个公共的css文件中去。
使用vue-loader提供normalizeComponent方法,合并scriptExport,render,staticRenderFns,返回构建vue组件需要的配置项options
css scope的工作原理:
使用vue-loader处理.vue文件,生成一个vue文件的hash值
如果该文件的css属性存在scope,且使用less/scss,则先用sass-loader,scss-loader把css转换成css,再通过postCss为每一个css选择器新增一个含有该随机键值的属性,最后使用style-loader转换成css样式添加到header中,给每个组件配置项新增这个hash的属性
6.什么是keep-live
keep-live是vue的一个组件,可以使用被包含的组件保留状态,避免被重复渲染,也就是所谓的组件缓存
include属性 知道那些组件被缓存,可以为正则或者字符串
exclude属性,与include相反
原理: 是一个抽象组件,自身不会渲染成dom
#### 判断当前组件是否要被缓存
#### 命中缓存则直接获取,同时更新key的位置(LRU)
#### 不命中缓存则设置进缓存,同时检查缓存的实例数量是否超过 max
#### 将当前组件实例的 keepAlive 属性设置为true,这个在缓存选中过程中会用到
7. vue3 composition API
解决了什么问题:
代码的可读性随着组件的变大而变差。
typeScript的支持有限
总结:
在逻辑组织核逻辑复用方面1,composition Api是优于Options APi
因为composition API几乎全是函数,会有更好的类型推断。
composition API与tree sharking友好,代码也更容易压缩。
compoistion API没有this的使用,减少了this指向不明的情况。
小型组件,继续使用optionsAPI也是很友好的。
8. vue3的性能提升主要是体现在哪几方面的
1. 编译优化
diff算法优化 新增了静态标记,下次发生变化时直接从该地方进行比较,重写了虚拟dom的实现
静态提升 对于不参与更新的元素,做静态提升,只会被创建一次,在渲染时直接复用
事件监听缓存 事件监听默认情况下被当做动态绑定,每次都会追踪变化
SSR优化
2. 源码体积
采用tree-sharking 减少了项目源码的体积
3. 响应式系统
采用proxy进行对象劫持,而不是之前的defineProperty来劫持对象属性,性能提升更大
4. 对typeScript更加友好
5. composition API使得维护更加的方便
9. vue组件的通信方式
props
$emit
ref
eventBus $emit $on
$parents $root
attrs与listeners
Provide Inject
vuex
10. 为什么组件的data必须是一个函数而不是对象
为了防止公用data造成的数据污染
11. vue示例挂载的过程
new vue的时候调用_init方法, 定义$set,$get,$watch方法
定义$on #off $emit $off等事件
定义_update $forceUpdate $destory等生命周期
调用$mount进行页面的挂载
执行render函数生成虚拟dom
_update将虚拟的dom生成真实的dom结构,并且渲染到页面
12. vue diff算法简单说一下
当组件创建和更新时,vue会执行内部的update函数,该函数使用render函数生成的虚拟dom树,将新旧两树进行对比,找到差异点,最终更新到真实dom
对比差异的过程叫diff,vue在内部通过一个叫patch的函数完成该过程
在对比时,vue采用深度优先、同级比较的方式进行比对。同级比较就是说它不会跨越结构进行比较
在判断两个节点是否相同时,vue是通过虚拟节点的key和tag来进行判断的
具体来说,首先对根节点进行对比,如果相同则将旧节点关联的真实dom的引用挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom。
在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。
这样一直递归的遍历下去,直到整棵树完成对比。
v3的diff算法是采用最长递增子序列的方式,
13. vue为啥不建议用index作为key
当我们使用index作为key时,当去操作该数组里的元素时,比如删掉一个,当进行diff算法时,找到相同的key,但是没有元素内容不同,这时候会创建一个新的,然后把之前的删掉,这也导致了存在多次不必要的操作,原本可以通过移动元素解决的事情,多做了几次的增删操作。
14. 说下对vue的理解
vue的历史 -> vue是什么 -> vue的核心特性 -> vue && jquery -> vue && react
历史
静态网页 -> jsp asp -> jquery -> sqa
vue是什么
是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用的Web应用框架。
vue的核心特性
数据驱动
M(model):数据层,用于跟服务端进行交互
V (view):视图层,用于将数据展示给用户
VM(viewModel):视图模型层,用来连接Model和View,是Model和View之间的通信桥梁
组件化
就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件2.组件化的优势
指令系统
指令 (Directives) 是带有 v- 前缀的特殊属性作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM
vue跟传统开发的区别
摒弃了传统开发(jqury)直接操作dom的思想,而采用虚拟dom的方式,对底层dom的修改采用封装的方式,用户在开发的时候不用考虑怎么去改变dom,而是直接去控制数据即可
vue跟react的对比
见下一个问题
15. react跟vue的对比
共同点
- 都是采用虚拟dom
- 组件化的思想
- 都支持服务端渲染
- 都有多端的技术解决方案 weex rn
- 都是数据驱动视图
- 都有自己的构建工具 creat-app vue-cli
不同点
-
react是单向数据流,vue是双向数据流(数据绑定)
-
组件化的通信方式不同,react主要采用回调的方式,而vue则是事件跟回调参数
-
diff算法不同,react是采用- diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。
Vue使用双向指针,边对比,边更新DOM
16. vue router进行跳转的时候都会执行那些函数
首先执行的是旧路由的boforeRouterLeave
执行全局的路由守卫beforeEach
执行新路由的beforeRouterUpdate
执行路由配置beforeEnter
执行新路由的beforeRouterEnter
执行全局路由守卫的beforeResolve
导航被确认
执行全局路由守卫afterEach
触发dom更新
执行beforeRouteEnter,渲染dom
17. 数据请求在created跟mounted执行的区别,并说说对vue生命周期的理解
vue生命周期
beforeCreate -> created -> beforeMount -> mounted -> beforeUpdate -> updated -> beforeDestory -> destoryed
更新: beforeUpdate -> updated
mounted 跟created的区别
created只是初始化了vue示例,并没有进行dom的挂载,所以在created里无法获取dom,所以在mounted里进行页面请求,此时dom已经挂载,请求的数据如果改变dom,则可能造成页面闪动。
18. 为什么v-for跟v-if不建议同时使用
v-for的优先级高于v-if,在同时使用的时候,会造成先进行遍历,再进行判断,会带来性能上的浪费,解决的方式是
通过计算方式进行先行过滤
19. spa首屏加载速度慢的问题
首先分析下加载慢的原因:
- 下载的文件过大
- 资源是否重复加载
- 网络问题
- 脚本执行阻塞
减少入口文件大小
- 懒加载,打包的时候会封装成多个文件
- 静态资源本地缓存 cache-contro etag last-modified service-work 离线缓存 localstorage
- 外部资源的按需引入,例如ui框架
- 组件的提取,避免重复打包 CommonsChunkPlugin
- 图片的压缩 字体图标的引用
- gzip的压缩 compression-webpack-plugin 同时nginx也需要开启gzip的压缩
- ssr渲染降低首屏渲染时间
- 合理的cdn
20.说说对$nextTick的理解
如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()
$nextTick() 会返回一个 Promise 对象,可以是用async/await完成相同作用的事情
具体实现:
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
// cb 回调函数会经统一处理压入 callbacks 数组
callbacks.push(() => {
if (cb) {
// 给 cb 回调函数执行加上了 try-catch 错误处理
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
// 执行异步延迟函数 timerFunc
if (!pending) {
pending = true;
timerFunc();
}
// 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve;
});
}
}
timerFunc函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有:
Promise.then、MutationObserver、setImmediate、setTimeout
总结:
- 把回调函数放入callbacks等待执行
- 将执行函数放到微任务或者宏任务中
- 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调
21. 说下你对mixin的理解
mixin是vue提供的提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能
- 替换型策略有
props、methods、inject、computed,就是将新的同名参数替代旧的参数 - 合并型策略是
data, 通过set方法进行合并和重新赋值 - 队列型策略有生命周期函数和
watch,原理是将函数存入一个数组,然后正序遍历依次执行 - 叠加型有
component、directives、filters,通过原型链进行层层的叠加
22. 说下对slot的理解
HTML中 slot 元素 ,作为 Web Components 技术套件的一部分,是Web组件内的一个占位符
该占位符可以在后期使用自己的标记语言填充
slot分为匿名插槽,具名插槽跟作用域插槽
总结:
v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使用- 默认插槽名为
default,可以省略default直接写v-slot - 缩写为
#时不能不写参数,写成#default - 可以通过解构获取
v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"
原理:
resolveSlots函数会对children节点做归类和过滤处理,返回slots
function resolveSlots (
children,
context
) {
if (!children || !children.length) {
return {}
}
var slots = {};
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
var data = child.data;
// remove slot attribute if the node is resolved as a Vue slot node
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot;
}
// named slots should only be respected if the vnode was rendered in the
// same context.
if ((child.context === context || child.fnContext === context) &&
data && data.slot != null
) {
// 如果slot存在(slot="header") 则拿对应的值作为key
var name = data.slot;
var slot = (slots[name] || (slots[name] = []));
// 如果是tempalte元素 则把template的children添加进数组中,这也就是为什么你写的template标签并不会渲染成另一个标签到页面
if (child.tag === 'template') {
slot.push.apply(slot, child.children || []);
} else {
slot.push(child);
}
} else {
// 如果没有就默认是default
(slots.default || (slots.default = [])).push(child);
}
}
// ignore slots that contains only whitespace
for (var name$1 in slots) {
if (slots[name$1].every(isWhitespace)) {
delete slots[name$1];
}
}
return slots
}
23. 了解过vue.observeable吗?
在非父子组件通信时,可以使用通常的bus或者使用vuex,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable就是一个很好的选择
stroe.js
// 引入vue
import Vue from 'vue
// 创建state对象,使用observable让state对象可响应
export let state = Vue.observable({
name: '张三',
'age': 38
})
// 创建对应的方法
export let mutations = {
changeName(name) {
state.name = name
},
setAge(age) {
state.age = age
}
}
在.vue文件中直接使用即可
template>
<div>
姓名:{{ name }}
年龄:{{ age }}
<button @click="changeName('李四')">改变姓名</button>
<button @click="setAge(18)">改变年龄</button>
</div>
</template>
import { state, mutations } from '@/store
export default {
// 在计算属性中拿到值
computed: {
name() {
return state.name
},
age() {
return state.age
}
},
// 调用mutations里面的方法,更新数据
methods: {
changeName: mutations.changeName,
setAge: mutations.setAge
}
}
24. 你知道vue中的key吗,说说你对他的理解
key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点
在进行diff算法时,会进行sameVnode的判断,如果无法找到同key的则会直接创建,用index作为key时,虽然可以找到同key的元素,但是key里面的内容无法发生变化,还需要对对组件的子节点做进一步的更新,原本这些都是可以通过调换顺序去发生变更的。无法达到性能优化的效果
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
25. 你了解自定义指令吗?
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能
}
})
自定义指令也像组件那样存在钩子函数:
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新componentUpdated:指令所在组件的VNode及其子VNode全部更新后调用unbind:只调用一次,指令与元素解绑时调用
所有的钩子函数的参数都有以下:
-
el:指令所绑定的元素,可以用来直接操作DOM -
binding:一个对象,包含以下property:name:指令名,不包括v-前缀。value:指令的绑定值,例如:v-my-directive="1 + 1"中,绑定值为2。oldValue:指令绑定的前一个值,仅在update和componentUpdated钩子中可用。无论值是否改变都可用。expression:字符串形式的指令表达式。例如v-my-directive="1 + 1"中,表达式为"1 + 1"。arg:传给指令的参数,可选。例如v-my-directive:foo中,参数为"foo"。modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar中,修饰符对象为{ foo: true, bar: true }
-
vnode:Vue编译生成的虚拟节点 -
oldVnode:上一个虚拟节点,仅在update和componentUpdated钩子中可用
26 vue3 中的reflect对象了解过吗?干嘛用的
reflect对象主要是用于配合proxy使用的,恰恰是为什么触发代理对象的劫持时保证正确的 this 上下文指向。因为proxy存在get陷阱(其他方法同样),原因在于get的第三个参数receiver对象,首先,在 Proxy 中 get 陷阱的 receiver 不仅仅会表示代理对象本身同时也还有可能表示继承于代理对象的对象,具体需要区别与调用方。这里显然它是指向继承与代理对象的 obj 。其次,我们在 Reflect 中 get 陷阱中第三个参数传递了 Proxy 中的 receiver 也就是 obj 作为形参,它会修改调用时的 this 指向。