Vue面试题--更新中......
自己整理的vue面试题,本文的特点就是大部分问题都设计到Vue源码和一些细节,可以在面试当做亮点进行回答,文章中的答案仅供参考,答案掺杂个人对问题的理解,如有错误欢迎大家在评论区进行指正。不算很全但是应该都是出现频率较高的问题
Vuex
📚vuex介绍(是什么、作用、怎么用、原理)
是什么:状态管理模式,其实就是一个数据仓库,用来存储和管理数据状态的
什么场景使用:一般来说用于多个视图和实例使用同一种状态,多个视图改变同一个状态,
概念:state、mutation、action、getter,
-
state:单状态树,也就是说所有的vue实例使用的都是同一个state对象的数据(支持模块化,最后需要合并而已),是用来管理和储存数据状态的,一般存放的是全局状态数据,比如说用户的登录信息、用户的权限信息,购物车数据,主题信息等需要多个视图和vue实例使用的信息(问:什么信息可以存储到vuex中).
- 使用:在store中state定义,...mapState()导出(当做Vue的computed属性来使用)
-
getter:其实就是store中的计算属性,依赖于state,...mapGetters()
-
📚Mutations: 用来修改state的唯一方法,functions(state,payload),store.commit()进行调用,📚必须是同步的,因为假如异步的话devtools无法追踪到 回调函数 修改state的过程,可能给我们的debug带来bug(问:为什么mutations中的函数都是同步的)
-
action:支持异步操作,可以调用mutations来变更state、可以异步、$store.dispatch来派发acitons,因为action返回一个promise,所以可以使用dispatch.then来顺序处理多个dispatch请求
组合式Api中怎么使用(Vue3使用vuex):
const store=useStore()
// 因为没有了全局对象this所以需要注册一个store实例
store.state.xxx
pinia介绍(是什么、作用、怎么用、原理)
也是状态管理器
核心概念和vuex几乎一样,只是在语法上有些许不同
vue和pinia的区别
冷知识:vuex的作者是🦑,pinia的作者是Eduardo Morote(Vue核心成员)
在我看来pinia和vuex在核心概念和业务使用场景上几乎完全相同,那么最大的不同主要是pinia支持多种多样的写法,不同于vuex中单一且繁琐的写法(其实这里可以例举react-redux?面试中可以加分),既支持vuex中的写法也支持compositionAPI的写法,
可以单独创建一个store实例然后在vue实例中调用,扁平的结构,不用像vuex中进行模块化整合了。
支持TypeScript,便携开发,pinia文档中也重点提到了ts的自动补全
可扩展性?高级pinia玩法(pinia-plugin-persist)可以用来数据持久化储存,pinia.use(),将数据自动保存在sessionStorge中*(pinia-plugin-persist在我的项目中就使用到了,我在我的vue项目模版中安装了pinia-plugin-persist,在特定的store中开启后,pinia会自动将这个store实例中的信息存入sessionStorage中去)
什么信息可以存储到VueX中
一般存放的是全局状态数据,比如说用户的登录信息、用户的权限信息,购物车数据,主题信息等需要多个视图和vue实例使用的信息
VueX中的用户信息是什么时候放进去的?什么时候会使用?
实例dispatch派发acitons然后从后端获取到数据后调用mutations对state中的用户信息进行更新。一般回用于用户路由权限,和判断一些功能的使用权限,展示用户信息
假设菜品存储在Vuex,单价发生了改变后什么时候同步到vuex中,后台不刷新,前台永远是老的价格?(说的有点模糊)
单价改变发生在前端的话,直接同步到vuex进行修改(使用mutation修改vuex中的state),保证前端是最新的(前提是前端依赖的数据是从vuex获取到的)
如果发生在后端,就直接修改vuex就行。
vuex储存的实例化是存放在哪的
储存在vue根根实例,因为这样才能保证从全局能调用到,保证多个视图共享同一个数据状态
vuex和vue怎么连接起来了,我们改vuex的状态为啥页面也会响应式更新?
笼统的讲肯定是vuex中的数据被vue响应式处理了,进行依赖收集了,具体来讲肯定是vuex源码和vue中use()函数的使用了。
Vue-router
📚vue-router是干什么的,为什么用vue-router,为什么不直接用url,权限控制为什么要前端控制
- 📚干什么的:单页应用 (SPA) 中将浏览器的 URL 和用户看到的内容绑定起来,当用户在应用中浏览不同页面时,URL 会随之更新,但页面不需要从服务器重新加载(单页面应用的特点) 通过配置路由告诉url路径来显示哪些个组件
- 为什么用vue-router:spa单页面应用实现url改变页面不刷新,有路由守卫可以实现权限管理,支持两种路由模式(hash、history)、实现管理路由跳转和嵌套等功能
- 为什么不直接用url:因为用url的话url一改变就会使得整个页面刷新,影响页面性能
vue-router实现核心
- 添加hashchange事件监听、首先创建一个Router实例,调用addroute给实例维护的一个routes对象添加路径,和回调函数,然后init,给window添加load(页面加载完毕会调用)和hashchange(hash值发生改变)事件监听,添加的事件对url中的hash值进行split并在维护的routes对象中找到对应的key,然后调用该key的对应的函数即可
不用vue-router,用原生js实现路由和路由权限
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li><a onclick="location . href='#/'">11111</a></li>
<li><a href="#/admin">22222222</a></li>
<li><a onclick="location . href='#/server'">333333333</a></li>
</ul>
<div id="div">展示</div>
<script type="text/javascript">
let res=document . getElementById("div")
const Router=function(){
this . route={}
this . curUrl=''
this . mode=''
this . addRoute=(path,cb)=>{
console . log(path);
this . route[path]=cb||function(){}
}
this . refresh=()=>{
if(this . mode='history'){
this . curUrl=location . pathname
this . route[this . curUrl]()
}else{
this . curUrl=location . hash . split('#')[1]||'/'
this . route[this . curUrl]()
}
}
this . push=(url)=>{
history . pushState({},null,url)
this . refresh()
}
this . replace=(url)=>{
history . replaceState({},null,url)
this . refresh()
}
let res=document . getElementById('div')
this . init=({mode})=>{
this . mode=mode
if(mode=='history'){
window . addEventListener("load",this . refresh,false)
window . addEventListener("popstate",this . refresh,false)
}else{
window . addEventListener("load",this . refresh,false)
window . addEventListener("hashchange",this . refresh,false)
}
}
}
const router=new Router()
const baseUrl='/node-template/src/js/easyRoute.html'
router . addRoute(baseUrl,()=>{
res . style . backgroundColor='pink'
res . innerHTML='11111'
})
router . addRoute('/node-template/src/js/history.html',()=>{
res . style . backgroundColor='black'
res . innerHTML='11111'
})
router . init({
mode:"history"
})
router . push('/node-template/src/js/history.html')
</script>
</body>
</html>
📚什么是路由守卫,有哪些钩子
就是在进行页面跳转前的一些钩子函数,一般可以用来进行权限判断是否放跳转或者取消
- beforeEach(全局守卫,每一次路由切换都会执行)
- 📚beforeEnter (路由独享守卫,在路由中写的用来对特定的路由进行跳转的时候设置的守卫)
📚onBeforeRouteUpdate(路由改变但是当前组件不变时调用)onBeforeRouteLeave(离开时) 组件内的守卫
怎么做权限管理
- 前端来处理:meta元信息添加该路由的权限信息,在beforeEach中获取用户权限筛选生成路由数组,通过addRoute加入到路由数组和vuex或者pinia中,然后放行。
注意:假如将权限信息直接存放在sessionStorage中,会被代替和修改。
- 后端处理:直接返回路由数组然后addRoute添加即可
调用不同跳转api分别会触发哪些事件(使用了哪些原生的api)
- hash模式调用了 绑定在window上的hashchange和load事件
- history:pushState、replaceState(不会被popState监听)、go、forward、back=>受popState监听(监听历史记录的变化,一旦变化就触发回调函数)
- 注意:history.go histroy.forward默认也是调用popState方法
📚vue-router的两种模式,他们的区别
history模式和hash模式
-
📚原理:hash模式监听hashchange事件来实现路由和页面的改变,history使用pushState和replaceState仅仅使得state对象从而改变当前url地址,不会触发popState,popState只会在go、forward、back等操作才会触发.
-
表现方式不一样,hash模式有一个hash值,在url中#的后面,可以通过location.hash直接获取到hash值,histroy就是标准的url格式,/xxx/xxx的格式。
Vue原理
diff算法(虚拟dom)
- Vue2:其实就是snabbdom库。
源码架构
-src
-moduel
- attributes
- props
- styles
- class
...core
⭐️ sameVnode:判断两个vnode节点是否相同
源码: 通过判断vnode节点上的key、sel、幽灵标签或者文本等等是否相等,这里说一下sel是选择器(div或者class等等),key就是唯一标识
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
const isSameKey = vnode1 . key === vnode2 . key;
const isSameIs = vnode1 . data ?. is === vnode2 . data ?. is;
const isSameSel = vnode1 . sel === vnode2 . sel;
const isSameTextOrFragment =
!vnode1 . sel && vnode1 . sel === vnode2 . sel
? typeof vnode1 . text === typeof vnode2 . text
: true;
return isSameSel && isSameKey && isSameIs && isSameTextOrFragment;
}
⭐️ init:核心core代码,根据传入的module,返回一个函数patch(里面是核心逻辑)
⭐️ patch:整个流程核心,
暂时无法在飞书文档外展示此内容
return function patch(
oldVnode: VNode | Element | DocumentFragment,
vnode: VNode
): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
for (i = 0; i < cbs . pre . length; ++i) cbs . pre[i]();
// if (isElement(api, oldVnode)) {
// oldVnode = emptyNodeAt(oldVnode);
// } else if (isDocumentFragment(api, oldVnode)) {
// oldVnode = emptyDocumentFragmentAt(oldVnode);
// }
if (sameVnode(oldVnode, vnode)) {
// 如果是相同节点
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
// 如果不是相同节点
elm = oldVnode . elm!;
parent = api . parentNode(elm) as Node;
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
api . insertBefore(parent, vnode . elm!, api . nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
for (i = 0; i < insertedVnodeQueue . length; ++i) {
insertedVnodeQueue[i] . data! . hook! . insert!(insertedVnodeQueue[i]);
}
for (i = 0; i < cbs . post . length; ++i) cbs . post[i]();
return vnode;
};
}
⭐️ patchVnode:对比两个节点的差异,这里设计的情况比较多,源码比较的复杂,所以我引用@渣渣xiong 这位大佬的思维导图
具体源码:
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
const hook = vnode . data ?. hook;
hook ?. prepatch ?. (oldVnode, vnode);
const elm = (vnode . elm = oldVnode . elm)!;
if (oldVnode === vnode) return;
if (
vnode . data !== undefined ||
(vnode . text !== undefined && vnode . text !== oldVnode . text)
) {
vnode . data ??= {};
oldVnode . data ??= {};
for (let i = 0; i < cbs . update . length; ++i)
cbs . update[i](oldVnode, vnode);
vnode . data ?. hook ?. update ?. (oldVnode, vnode);
}
const oldCh = oldVnode . children as VNode[];
const ch = vnode . children as VNode[];
if (vnode . text === undefined) {
if (oldCh !== undefined && ch !== undefined) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
} else if (ch !== undefined) {
if (oldVnode . text !== undefined) api . setTextContent(elm, "");
addVnodes(elm, null, ch, 0, ch . length - 1, insertedVnodeQueue);
} else if (oldCh !== undefined) {
removeVnodes(elm, oldCh, 0, oldCh . length - 1);
} else if (oldVnode . text !== undefined) {
api . setTextContent(elm, "");
}
} else if (oldVnode . text !== vnode . text) {
if (oldCh !== undefined) {
removeVnodes(elm, oldCh, 0, oldCh . length - 1);
}
api . setTextContent(elm, vnode . text);
}
hook ?. postpatch ?. (oldVnode, vnode);
}
⭐️ updateChildren:判断子节点的差异,这里也就是diff算法的核心,
diff算法是干什么的就显而易见了,就是为了对比虚拟节点中子节点的差异的,当然diff算法有好多版本,传统版本采用的是逐一比较,复杂度是O(n^3)显然复杂度较高,这里snabbdom采用的是同一层级进行比较(因为变化是局部的,大多数的变化都是在同一层的),这样大大节省了时间复杂度
源码:这里首先是分别定义了新旧节点的开始索引和结束索引
使用while循环,当新节点的开始索引和结束索引碰头了,或者旧节点的开始和结束索引碰头了,那我们就跳出循环,这里也就是说我们的diff算法进行完毕,已经比较出差异
单次循环:
- 新、旧开始节点相同,patchVnode(发现差异并更新),两者的**
StartIdx**都++ - 新、旧结束节点相同,patchVnode(发现差异并更新),两者的**
EndIdx都--** - 新节点的开始节点、旧节点结束节点相同,patchVnode(发现差异并更新)
newStartIdx++****oldEndIdx--
修改结束节点的真实dom然后移动到最前面
- 新节点的结束节点、旧节点开始节点相同,patchVnode(发现差异并更新)
newEndIdx--oldStartIdx--
修改开始节点的真实dom然后移动到最后面
- 如果都不满足则直接在旧节点中遍历寻找相同的旧节点,然后patchVnode(发现差异并更新)
newStartIdx++, 直接将新节点对应的真实dom插入到最前面
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue
) {
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = oldKeyToIdx[newStartVnode.key!];
if (idxInOld === undefined) {
// `newStartVnode` is new, create and insert it in beginning
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
newStartVnode = newCh[++newStartIdx];
} else if (oldKeyToIdx[newEndVnode.key!] === undefined) {
// `newEndVnode` is new, create and insert it in the end
api.insertBefore(
parentElm,
createElm(newEndVnode, insertedVnodeQueue),
api.nextSibling(oldEndVnode.elm!)
);
newEndVnode = newCh[--newEndIdx];
} else {
// Neither of the new endpoints are new vnodes, so we make progress by
// moving `newStartVnode` into position
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
newStartVnode = newCh[++newStartIdx];
}
}
}
if (newStartIdx <= newEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
}
if (oldStartIdx <= oldEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
diff算法的理解
是干什么的:vue2中snabbdom中对比子节点差异使用的一种算法(updateChildren),snabbdom把时间复杂度降低到了On,传统的diff算法是逐个进行比较的,复杂度为On^3
是什么:比较两个虚拟dom对象树的差异,双端比较,使用同层比较的方法,时间复杂度为On
详解:5种情况
📚Vue3中的diff算法
⭐️ 预处理(快捷路径)
- 借鉴了文本预处理的方式
就像这样:
新、旧前置节点比较和新、旧后置节点进行比较,如果有一端的前后索引碰头的话就代表已经diff完成不用进行核心的diff处理,这样在只添加或者删除节点,并且顺序没有改变的情况下节省了性能
- 具体操作方法:
前置节点: 我们可以建立索引 j,初始值为0。
后置节点: 因为可能两组节点长度不同,所以我们在尾端设置新旧两个指针
两个while循环: 让j递增,让newEnd和oldEnd递减,遇到不同的节点就停下,相同的节点就打补丁(调用patch进行更新)
跳出循环后判断:
-
j<=newEnd && j > oldEnd 说明有新节点,那么就在newEnd+1的索引位置添加新的节点
-
j>newEnd && j<=oldEnd1 说明有旧的节点被删除了,就卸载 j到oldEnd之间的节点。
⭐️ 📚判断是否要进行dom移动操作
预处理因为只是针对在节点顺序不改变的情况下,增添和删除节点的情况(比较理想化的情况).
构造一个source数组,长度为预处理之后剩余的未处理节点的数量,储存未处理节点在旧节点数组的索引,后面将会使用它计算出一个最长递增子序列,并用于辅助完成 DOM 移动的操作
填充source数组: 构造一个索引表,遍历未处理新节点数组,key为当前节点的key,value是新节点的索引值,
遍历旧节点对应的数组,oldVnode.key->查询索引表->newVnode索引->source[newVnode索引]=oldVNode索引
定义k(最大的key值)和move(是否需要移动): 如果当前key小于k那说明需要移动,大于k则不需要移动
问:为什么要通过最大的key值来判断是否需要移动呢
因为正常情况下oldNode的key值是递增的趋势,所以newNode中小于k的就说明应该在前面某个位置,所以我们需要用一个当前循环中最大的key值来的判断是否需要移动这个节点
问:为什么要维护一张索引表,而不直接去遍历旧节点一个个的去寻找呢
因为那样两层嵌套循环的复杂度为On^2,这样做能够将时间复杂度降到On,
⭐️ 📚如何移动元素
根据source数组计算一个最大递增子序列, 返回的是source数组中最长序列元素的索引,
重新对未处理的子节点数组索引编排,第一个元素为索引为0
在source和新节点数组末尾定义两个索引s和i,判断i==seq[s]
-
不相等(i--,s不变)
- 是-1,表示是新增节点,在i+newStart中挂载
- 不是-1,通过insert移动
-
相等(i--,s--)
📚Vue2和Vue3中diff算法的差异
-
vue2用的是双端diff算法,vue3使用的是快速diff算法
-
vue2中分5种情况(头对头,头对尾,尾对头,尾对尾) 进行判断然后移动、添加或者卸载节点,vue3经过预处理,生成最长递增子序列判断是否需要移动、添加或者卸载, 然后对需要移动的节点进行移动
-
在 Vue 2 中,Diff 算法的时间复杂度为 O(n) 。而在 Vue 3 中,Diff 算法的时间复杂度为📚 O(n log N)。
diff算法的复杂度,其本质是一个什么算法
Vue2其实就是找dom树树之间的差异,Vue3其实就是最长递增子序列
响应式
📚响应式原理(响应式的构造过程)
Vue2:defineProperty给属性添加set和get方法,并递归进行响应式处理,同时对数组的push,pop,shift,unshift、sort、reverse、splice、slice等(改变数组长度和排序的方法)进行重写,以便检测到数组的更新,具体的方法就是object.create(Array.property)这样既能保证原有的array方法也能重写新的方法。
缺点:不能劫持到对象属性的添加和删除,
使用发布订阅者模式dep和watcher来实现数据改变来通知视图更新,
Vue3:使用proxy代理拦截属性的变化,(我们在vue3项目中打印响应式的数据的时候,会发现在控制台输出的都是proxy数据)
这里为什么使用reflect
- 使得代码可读性更高,get、set、delet一眼便知
- 假如操作失败会返回一个布尔值,或者undefined
- Reflect所有方法都是静态的,所以target的this指向不会改变
- 可以和proxy搭配使用,因为这里的set、deletProperty方法需要返回一个布尔值,而reflect.set、reflect.deletProperty如果成功添加了也会返回一个布尔值
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
补充一下proxy、reflect在es6的知识
Proxy:在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
实际上就是重载行为,用自己的定义覆盖了一些原始的定义。
const proxy = new Proxy(target, {
get: function () {},
set: function () {}
})
📚Reflect:也是一个对象,其api和用法和Object几乎一样,相当于Object对象的复制体,但是reflect上有一些api和Object用法一样但是名字更短,而且返回的结果更加符合我们的预期,比如在某个对象上没有某个属性那么object可能会直接抛出一个错误,而reflect会返回一个boolean值,这样更有利于我们书写整个程序的逻辑
比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false ,比如使用try catch来捕获defineProperty的错误,而reflect中defineProperty直接会返回一个布尔值
Vue2和Vue3中响应式原理的区别
Vue2对象添加属性怎么实现相应式
- Vue2:this.$set()
- Object.assign()
KeepAlive
📚是什么,原理是什么、怎么用、什么场景下用keepalive
<KeepAlive> 是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。
-
怎么用:用keepalive标签组件包裹要被缓存的组件,为了保持某种状态,让其在组件切换的时候仍保持切换前的状态,可以使用keepalive进行包裹
- 配合component动态组件、或者router-view路由组件进行使用
- 属性:exclude、include、max(遵循LRU的缓存机制,谁活跃谁在前,不活跃的被踢)
- 生命周期:
onActivated()和onDeactivated()
-
📚原理:首先获取组件名、根据include和exclude的规则进行条件匹配、根据组件名在cache(Map哈希表中)中获取组件,并吧key放进队列中,
- 如果在cache中有的话直接取出来,并且把放在队列头部。
- 如果没有的话,首先查看key队列里有没有达到max的限制,如果已经达到了,删除队尾缓存的组件key,然后在队头添加新组件key(LRU的缓存策略)
最后将组件的keepAlive属性设置为true,
- 使得生命周期不再和正常组件一样(activated和deactivated),
- 首次加载将包裹的组件进行缓存,然后第二次渲染的时候拿出来
keep-alive如何重新发起请求
keep-alive有自己的生命周期 activated和deactivated,每一次被缓存的组件进行挂载的时候都会执行activated,所以在activated函数中再次发起请求即可
keep-alive缓存的是什么内容
key和vnode,key用于保存在cache队列中,vnode则是keepalive标签包裹的组件的实例
keep-alive标签的作用
包裹被缓存的组件,保存被包裹组件的状态
虽然keep-alive并不会产生真正的dom节点(设置了abstract:true)
📚包裹组件渲染
咱们再来说说被keep-alive包裹着的组件是如何使用缓存的吧。
刚刚说了VNode -> 真实DOM是发生在patch的阶段,而其实这也是要细分的:VNode -> 实例化 -> _update -> 真实DOM,而组件使用缓存的判断就发生在实例化这个阶段,而这个阶段调用的是createComponent函数,那我们就来说说这个函数吧:
// src/core/vdom/patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) // 将缓存的DOM(vnode.elm)插入父元素中
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
-
在第一次加载被包裹组件时,因为keep-alive的
render先于包裹组件加载之前执行,所以此时vnode.componentInstance的值是undefined,而keepAlive是true,则代码走到i(vnode, false /*hydrating */)就不往下走了 -
再次访问包裹组件时,
vnode.componentInstance的值就是已经缓存的组件实例,那么会执行insert(parentElm, vnode.elm, refElm)逻辑,这样就直接把上一次的DOM插入到了父元素中。
📚如何更新keep-alive包裹下的子组件的状态
- 在activated中更新组件状态即可
- Component 的is属性设置为动态的,更新is的属性值会触发render更新
nexttick
当我们改变数据后想立即从dom中获取到修改的数据,或者对新的dom进行一些操作的时候,我们要把修改dom的操作放在nexttick中去实现,nexttick就是将函数进行异步处理,
优雅降级
最好promise.resolve.then()=>ie:set...
$nextTick 是什么的 什么场景用 什么原理
是什么:将回调函数推入一个队列,然后在下一个tick执行该队列,其实就是将传入的回调延时执行
使用场景:
- 在dom更新后执行某些操作需要用到nexttick
举例:修改数据后立即从获取dom上修改过的数据
testVal . value = '321'
console . log(testDom . value . innerHTML)
nextTick(() => {
console . log(testVal . value)
})
- 在create、created等生命周期中执行dom操作需要用到nexttick、
- 用来监听子组件的变化?
原理:将传入的cb加入到一个队列中,然后将他们逐个异步执行。
-
用到了pollify优雅降级,promise为最优先,settimeOut为最次级
-
用到pending做判断是否正在执行回调函数,保证回调队列的执行顺序
使用场景、
nextTick的代码实现有了解过吗
关键词:callback队列、pending、优雅降级
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks: Array<Function> = []
let pending = false
function flushCallbacks() {
pending = false
const copies = callbacks . slice(0)
callbacks . length = 0
for (let i = 0; i < copies . length; i++) {
copies[i]()
}
}
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise . resolve()
timerFunc = () => {
p . then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver . toString() === '[object MutationObserverConstructor]')
) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document . createTextNode(String(counter))
observer . observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode . data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick(): Promise<void>
export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
/**
* @internal
*/
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
callbacks . push(() => {
if (cb) {
try {
cb . call(ctx)
} catch (e: any) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
nextTick可以做什么不可以做什么?
可以用来操作dom元素,dom更新后的一些操作
最好不要执行同步操作,因为同步操作在这里会被当成异步任务去处理
Mixin
📚是什么,怎么用,什么场景,原理是什么,有什么优缺点
是什么:
将组件的公共逻辑或者配置提取出来,哪个组件需要用到时,直接将提取的这部分混入到组件内部即可。这样既可以减少代码冗余度,也可以让后期维护起来更加容易。
怎么用:
格式:导出一个对象,key同vue2组件的key相同
export const mixins = {
data() {
return {};
},
computed: {},
created() {},
mounted() {},
methods: {},
};
-
局部混入:mixins:[mixin]注册,声明周期会合并,相同名字的变量和方法会被组件覆盖
-
全局混入: Vue.mixin(),每一个组件都会被混用
📚原理:
mergeOptions方法,传入本身的options和mixin,
- 合并options,遍历两者的options,如果没有传入starts策略则采用默认策略,如果冲突默认采用组件的
- 合并声明周期,定义声明周期的常变量,使用concat将mixin和options进行合并,然后返回options
在生命周期调用前挂载在vm实例上,然后这样mixin中的内容就会被执行
📚优缺点:
优点:可以复用逻辑节省开发时间,无需传递状态
缺点:滥用的话维护不方便,排查bug难以溯源,命名很容易冲突
📚vue的mixin和extends讲一下
mixin:通用逻辑混入到组件中,可以混入多个mixin,如果出现冲突的话,生命周期合并成一个,数据方法使用组件中的。
extends:继承一个组件的数据,但是不会合并混合,如果出现冲突都以组件实例中的为基准。一般用于基础组件的定制来创建新的组件。
📚mixin冲突怎么解决,mixin底层是怎么实现的
对于生命周期函数,直接进行合并,先执行mixin中的,在执行组件中的,对于变量和方法冲突,以组件中的为标准执行
mergeOptions方法, 如果是生命周期的话,传入一个方法,如果组件和mixin有相同的生命周期的时候会concat成一个数组,
如果是方法属性的话,mergeOptions方法有三个参数parent、child、vm,这里的parent代表的是父组件传递的options或者默认的options,这里的child代表的是组件实例的options, 首先会查看实例中有没有mixin混入,然后遍历mixin数组的options(如果有冲突这里以数组中的最后一个mixin的标准覆盖)并合并到parent中,然后进行parent和child合并,在不传入strategy的情况下遍历parent和child,默认三元表达式 childVal===undefined?parentVal:childVal ,这也就表示默认使用组件中的数据
最后将options对象挂载到vue实例当中
// src/util/index.js
// 定义生命周期
export const LIFECYCLE_HOOKS = [ "beforeCreate", "created", "beforeMount", "mounted", "beforeUpdate", "updated", "beforeDestroy", "destroyed",];
// 合并策略
const strats = {};
//生命周期合并策略
function mergeHook(parentVal, childVal) {
// 如果有儿子
if (childVal) {
if (parentVal) {
// 合并成一个数组
return parentVal.concat(childVal);
} else {
// 包装成一个数组
return [childVal];
}
} else {
return parentVal;
}
}
// 为生命周期添加合并策略
LIFECYCLE_HOOKS.forEach((hook) => {
strats[hook] = mergeHook;
});
// mixin核心方法
export function mergeOptions(parent, child) {
const options = {};
// 遍历父亲
for (let k in parent) {
mergeFiled(k);
}
// 父亲没有 儿子有
for (let k in child) {
if (!parent.hasOwnProperty(k)) {
mergeFiled(k);
}
}
//真正合并字段方法
function mergeFiled(k) {
if (strats[k]) {
options[k] = strats[k](parent[k], child[k]);
} else {
// 默认策略
options[k] = child[k] ? child[k] : parent[k];
}
}
return options;
}
vue3如何实现vue2中mixin的方式
可以使用类似react Hook的方式来实现
Vue3为什么取消了混入mixin
因为vue3使用了compositionAPI,将组件的内容拆分成更小的部分,使得组件更加灵活多变。所以混入可以说是有一些鸡肋了,Vue3也不像vue2那样格式如此复杂
面试题
📚介绍一下MVVM
辅助背诵:一种前端设计模式、三个模块、vm双向数据绑定、视图和模型、优缺点
是一种前端的设计模式分成以下三个部分
view:视图层
model:数据逻辑层
- 📚ViewModel 层:实现双向数据绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
优点:📚MVVM中视图和模型之间的实现双向数据绑定耦合度较低,界面和业务逻辑分离出来提高代码的可维护性,可测试性,代码复用性从而提升开发效率
缺点:涉及到复杂的状态管理,项目复杂性高,不适合小项目(小题大做)
Vue中的mvvm模式
辅助背诵:view:html、绑定并展示数据、接受数据。model:数据、data、
view-model:桥梁,数据绑定、dom事件监听、vue核心原理
view:就是视图,对应的是html模版,负责将数据绑定在视图上并展示,接受用户的输入
model:就是模型,代表着数据和逻辑关系,在Vue中对应的是data
view-model:用数据绑定和dom事件监听将view视图和model数据模型关联起来,具体体现有双向数据绑定、watcher,dep观察订阅者模式、数据劫持,模版解析,虚拟dom等等
和MVC的区别
C是controller,最大的区别就是mv手动传递数据,mvvm实现双向绑定,不需要处理视图和数据间的同步
为什么data是一个函数
因为假如是对象的话,会出现多个组件同用一个data的现象,会出现相互影响的现象,而每一个函数是一个单独的作用域,组件之间互不影响
Vue通讯方式(数据传递方式)
辅助记忆:vue2和vue3,传递,接受,调用
-
父=>子
- Vue2:props属性传递给子组件,子组件通过props接受,可以是数组,也可以是对象(可以设置默认值和数据类型)
- Vue3:defineProps,同上,
props.name调用
-
子=>父
- Vue2:
this.emits("name",params),父组件使用@name='fun'来传递 - Vue3:
const emit=defineEmits(数组),调用emit('方法名')
- Vue2:
-
爷孙
- provide和inject依赖注入,vue3
provide(key,value)const val=inject(key) - listeners
- provide和inject依赖注入,vue3
-
ref
vue2中用this.$refs.name获取组件实例,通过实例获取值
v-if和v-show区别
-
v-show
- 动态显示和隐藏:
v-show是通过 CSS 的display属性来实现动态显示和隐藏的,元素在 DOM 中一直存在,只是在display: none和display: block之间切换。 - 性能开销: 由于元素一直存在于 DOM 中,
v-show在条件变化时的性能开销相对较小。适用于频繁切换显示和隐藏的场景。 - 适用场景: 适用于需要频繁切换显示和隐藏的场景,如折叠面板、切换视图等。
- 动态显示和隐藏:
-
v-if
- 动态渲染:
v-if是动态的,元素的渲染状态会根据表达式的变化而动态变化。当isShown从false变为true时,元素会被添加到 DOM 中;反之,从true变为false时,元素会从 DOM 中移除。 - 性能开销: 因为
v-if控制的元素在条件不满足时是完全从 DOM 中移除的,所以当条件频繁变化时,会有一定的性能开销,因为元素的创建和销毁涉及到 DOM 操作。 - 适用场景: 适用于在条件不满足时,完全移除元素的场景,如在切换页面或切换视图时。
- 动态渲染:
在源码上其实就是将含有v-if的元素的虚拟dom删除并缓存,然后通过if else判断把 结果为true得标签重新渲染
v-show就是el.style.display = value ? originalDisplay : 'none'
v-for和v-if的优先级
-
vue2:v-for的优先级更高,先解析v-for再解析v-if,会导致进行不必要的判断,从而浪费性能
-
vue3:v-if的优先级更高,做了一个v-if的提升优化,但是在标签内部仍然取不到不符合条件的item项
computed计算属性和watch的区别
- computed不可以处理异步,watch可以处理异步任务
- computed支持缓存,watch不支持缓存
- computed一般是依赖一个响应式的属性然后返回一个响应式属性,当依赖项发生改变computed也会更新,watch一般是用于监听某个值的改变,当该值发生改变会触发相应的回调函数,可以有deep(深度监听对象属性的变化)和immediate(组件加载立即触发回调函数执行)两种模式
小tips:这里如果硬要说oldValue难道不是缓存下来的数据吗,这样为什么还说watch不支持缓存呢,这里oldValue是在每一次用的时候都会把上次的值再次赋值给oldValue,它并不会直接返回上次的value,而是每次数据变化都会赋值给oldValue。computed就是只有在计算时才会改变,computed一直缓存着上次计算的结果,使用的时候也会直接返回
❎ watch和computed源码
Vue2.0 响应式数据的原理
- observe方法
如果是对象(或数组)的话,对传入的data进行响应式处理,返回一个Observer实例
- 创建一个Observe类,
传入data数据,首先给data添加__ob__属性,值是当前Observe,防止被重复观测,然后判断data数据是数组还是对象,如果是数组的话, 对数组的8种方法进行重写以便能观测到其变化,如果是对象的话, 将对象遍历进行响应式处理(defineReactive)
- defineReactive方法
首先在次调用observe方法(对其多层判断递归),使用Object.defineProperty给对象添加get和set方法
-
get:首先进行依赖收集,然后直接返回value
-
set:改变原来的value值,将newVal进行observe响应式处理,然后dep.notify()通知视图进行更新
Vue3响应式数据的处理
使用proxy方法创建一个proxy实例,然后对对象进行劫持,添加get、set和deleteProperty的方法,然后返回Rflect.get()、set()、deleteProperty()的方法,这样可以检测到对象删除属性的操作,这里可以解释一下为什么用proxy和reflect(在上文)
Vue如何检测数组的变化
首先创建一个空对象原型为Array(AOP的思想),然后对8个方法一次进行重写和挂载。调用Array上的方法得到执行结果,然后给数组打上标志__ob__=this 防止重复观测,最后如果有新增操作,需要对新增数组进行响应式处理,以便能被监听到变化。最后触发视图的更新
// src/obserber/array.js
// 先保留数组原型
const arrayProto = Array.prototype;
// 然后将arrayMethods继承自数组原型
// 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能
export const arrayMethods = Object.create(arrayProto);
let methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"reverse",
"sort",
];
methodsToPatch.forEach((method) => {
arrayMethods[method] = function (...args) {
// 这里保留原型方法的执行结果
const result = arrayProto[method].apply(this, args);
// 这句话是关键
// this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4) this就是a ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例
const ob = this.__ob__;
// 这里的标志就是代表数组有新增操作
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
default:
break;
}
// 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测
if (inserted) ob.observeArray(inserted);
// 之后咱们还可以在这里检测到数组改变了之后从而触发视图更新的操作--后续源码会揭晓
return result;
};
});
❎ Vue3响应式数据的多种处理方法
Vue 的父子组件生命周期钩子函数执行顺序
- 加载渲染过程
父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
- 子组件更新过程
父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
- 父组件更新过程
父 beforeUpdate->父 updated
- 销毁过程
父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
虚拟dom是什么、有什么优缺点
虚拟dom其实就是将真实dom进行解析然后用js对象来描述真实dom,这个js对象就被称为虚拟dom,如果数据改变我们直接去操作真实dom的话肯定会消耗很多性能,所以我们先通过操作虚拟dom然后把虚拟dom渲染为真实dom,这样大大减少了操作真实dom的次数,既在一些情况下节约了性能又提升了用户体验
优缺点:
-
保证性能下限: 它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
-
无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
-
跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
❎ Vue中SSR
v-model的原理
v-model是用在input标签上的语法糖
其实就是:value和@input的语法糖,@input通过检测input中数据的改变,通过event.targe.value将赋值给:value绑定的响应式数据,从而input内容发生改变
❎ vue 中使用了哪些设计模式
1.工厂模式 - 传入参数即可创建实例
虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例
vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.装饰模式: (@装饰器的用法)
6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
...其他模式欢迎补充
❎ Vue 事件绑定原理
Vue.set方法原理
Vue3中使用proxy时移舍弃,使用ref和reactive已经能将数据变成响应式的
Vue的模版编译原理
-
生成
ast🌲- parseHTML,具体原理用一个while循环,用正则表达式去匹配html中的内容,然后不断去截段html直到html为null
-
codgen:根据ast🌲去生成render函数-
genChildren:对
Children处理, -
genProps:对
props进行处理,其中要对style进行特殊处理,生成对象的那种key和value的键值对,_c(tag,props,children)这种形式,_s()表示数据绑定的变量也就是{{name}},_v()就是普通字符串
-
-
render函数,定义一个new Function(with(this){return ${code}})将函数绑定在vm实例上,因为这样在_s(name)中才能访问到vm.data中的变量
Vue生命周期钩子是如何实现的
Ref和Reactive的区别
辅助记忆:数据类型、调用、修改、深度监听原理、watch深度监听的不同
-
ref可以用于原始数据类型和引用数据类型,在模版中直接使用,在js中通过.value使用,
- 这里为什么用.value是因为在模版中可以通过getter和setter方法对数据进行监听,在js中通过.value的方式来触发数据的响应式原理从而产生更新
- 这里如果ref传入的是对象的话,那么其内部也是会调用reactive,因为reactive的原理就是对对象进行深层监听。
-
reactive用于引用数据类型,深层次的监听对象。reactive生成的响应式数据可直接进行访问和修改。reactive在赋值的时候不能对原对象进行完全覆盖,因为这样会破坏响应式
- 为什么会破坏响应式
-
toRefs将响应式的对象转换为普通对象
-
toRef将对象转换为响应式的
总结:
ref可以存储基本数据类型而reactive则不能(存储数据类型不同),可以给ref的值重新分配给一个新对象,而reactive只能修改当前代理的属性(ref、reactive修改对象)、 ref() 在原始数据位Object类形时,会通过 reactive 包装原始数据后再赋值给_value(ref、reactive深度监听对象)、watch中如果不设置deep为true那么watch不会对ref进行深度监听只会对ref.value进行监听(ref本身会调用reactive的原理对对象进行深度监听),当然watch对reactive默认进行深度监听
watch和watchEffect的区别
-
watchEffect不用指定监听哪一个数据源,其会对回调函数出现的响应式数据进行监听,类似于computed。而且只能回去到修改后的值,并且当值发生改变会立即执行
-
watch需要指定数据源,可以获取到新旧数据,会等数据源改变后再执行回调( 可以通过immediate进行执行)。
Teleport是什么
传送组件,将包裹的dom元素移动到指定的父元素位置下面。有两个参数to、disabled,可以用来实现模态框、下拉菜单(用一个响应式的Boolean属性控制父标签的显示隐藏,然后把teleport传送到#app上即可)
- to组件是强制性的prop,这里可以填选择题、标签都可以
- disabled如果是false,那么就不会进行转移,只会当做普通标签渲染
<teleport to="#to-parent-id" />
<teleport to=".to-parent-id" />
<teleport to="[to-parent-data-teleport]" />
// disabled
<teleport to="#popup" :disabled="popupOrInline">
<video src="./my-movie.mp4">
</teleport>
❎vue3.0编译做了哪一些优化?
TreeShaking在vue中的使用
Key在vue中的作用
key用于进行diff算法比较,能通过diff算法来找到新旧节点的差异以最小的性能损耗来更新新旧节点从而节省性能,
Vue中如何处理跨域的
❓Vue3和Vue2的区别
响应式原理的不同:vue2中使用defineProperty、Vue3中使用proxy进行监听
diff算法的不同:Vue2中使用双端diff算法的原理,Vue3中使用快速diff算法
❓编译原理的不同:
compositionAPI:使用ref和reactive定义响应式数据
❓TreeShaking:
Axios的封装:
create一个axios实例然后选择路由模式(histroy or hash),定义请求拦截器(处理网络连接、header请求头自定义、token身份验证)和响应拦截器(code的处理)