vue面试(二)

  1. 1. diff算法
    1. 1.1. diff概念
    2. 1.2. diff比较流程
  2. 2. vue中key的作用和原理
    1. 2.1. key的概念
    2. 2.2. key的作用
  3. 3. Vue.use是干什么的
    1. 3.1. use概念
    2. 3.2. 插件的功能
    3. 3.3. 实现原理
  4. 4. vue.extend作用
    1. 4.1. extend概念
    2. 4.2. 分析
  5. 5. vue组件data为什么必须是个函数
  6. 6. 函数组件的优势
  7. 7. vue中的过滤器
  8. 8. v-once的使用场景
    1. 8.1. v-once概念
  9. 9. Vue.mixin的使用场景和原理
    1. 9.1. Vue.mixin概念
    2. 9.2. 混入方式
    3. 9.3. mixin合并策略
  10. 10. Vue中slot如何实现
    1. 10.1. 什么是插槽
    2. 10.2. 何时使用
  11. 11. 对双向绑定的理解
    1. 11.1. 概念
    2. 11.2. 表单元素中的v-model
    3. 11.3. 组件中的v-model
  12. 12. vue中.sync修饰符的作用
  13. 13. vue中递归组件理解
    1. 13.1. 模板递归
  14. 14. 组件中的name属性
  15. 15. vue中的修饰符
  16. 16. vue中异步组件作用及其原理
    1. 16.1. 异步组件概念
    2. 16.2. 异步组件写法
    3. 16.3. 异步组件原理
  17. 17. 对nextTick的理解
    1. 17.1. nextTick概念
  18. 18. keep-alive组件平时在哪里使用
    1. 18.1. 使用场景
    2. 18.2. 原理
  19. 19. 自定义指令的应用场景
    1. 19.1. 指令的生命周期
    2. 19.2. 常见的指令编写
  20. 20. vue中使用了哪些设计模式
  21. 21. vue中的性能优化有哪些

diff算法

diff概念

vue 基于虚拟 DOM 做更新 。diff 的核心就是比较两个虚拟节点的差异。Vue 的 diff 算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针的方式进行比较。

源码 core/vdom/patch.ts

diff比较流程

  1. 先比较是否相同节点key tag

  2. 相同节点比较属性,并复用老节点(将老的虚拟dom复用给新的虚拟节点DOM)

  3. 比较儿子节点,考虑老节点和新节点儿子的情况

    • 老的没儿子,现在有儿子。直接插入新的儿子
    • 老的有儿子,新的没儿子。直接删除页面节点
    • 老的儿子是文本,新的儿子是文本,直接更新文本节点即可
    • 老的儿子是一个列表,新的儿子也是一个列表 updateChildren
  4. 优化比较:头头、尾尾、头尾、尾头

  5. 比对查找进行复用

vue3中采用最长递增子序列来实现diff优化

听完了,理解并不透彻,仍需加强

vue中key的作用和原理

key的概念

  • key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧nodes 对比时辨识 VNodes。如果不使用key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。
  • 当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染

key的作用

  • Vue 在 patch 过程中通过 key 可以判断两个虚拟节点是否是相同节点。(可以复用老节点)
  • 无 key 会导致更新的时候出问题
  • 尽量不要采用索引作为 key

Vue.use是干什么的

use概念

安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install方法。install 方法调用时,会将 Vue 作为参数传入,这样插件中就不在需要依赖 Vue 了。

插件的功能

  • 添加全局指令、全局过滤器、全局组件
  • 通过全局混入来添加一些组件选项
  • 添加vue实例方法,通过把他们添加到vue.prototype上实现

实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vue.use =function(pugin:Functionobject{
// 插件缓存
const installedplugins = this._installedpluginsl||(this._installedplugins =[]);
if(installedplugins.indexof(plugin)>-1){
//如果已经有插件 直接返回
return this;
}
// additional parameters
const args = toArray(arguments1);// 除了第一项其他的参数整合成数组
args.unshift(this);// 将vue 放入到数组中
if(typeof plugin.insta11 ==="function"){
// 调用insta11方法
plugin.insta11.apply(plugin, args);
}else if(typeof plugin === "function"){
// 直接调用方法
plugin.apply(nu11,args);
}
insta1ledplugins.push(plugin);// 缓存插催
return this;
}

vue.extend作用

extend概念

使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
data 选项是特例,需要注意-在 vue.extend()中它必须是函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
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();

在底层是构造一个sub(VueComponent实例),这个sub是继承了vue的构造函数

分析

  • 所有的组件创建时都会调用vue.extend方法进行创建
  • 有了此方法我们可以手动挂载组件
  • 后端存储的字符串模板我们可以通过Vue.exntend方法将其进行渲染,但是需要引入编译时。

vue组件data为什么必须是个函数

  • 根实例对象data可以时对象也可以是函数“单例”,不会产生数据污染情况
  • 组件实例对象data必须为函数,目的是防止多个组件实例对象之间共用一个data,产生数据污染。所以需要通过工厂函数返回全新的data作为组件数据源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function vue(){}

Vue.extend = function(options){
function Sub(){
// 会将data存起来
this.data = this.constructor.options.data;
}
Sub.options = options;
return Sub;
};

let Cild = 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);

函数组件的优势

函数式组件的特性:无状态、无生命周期、无this,但是性能高。 正常组件是一个类继承了 Vue, 函数式组件就是普通的函数,没有 new 的过程。最终就是将返回的虚拟 DOM 变成真实 DOM 替换对应的组件。

底层方法:在createComponent中会判断有没有一个ctor.options.funcitonal,如果存在则直接调用createFunctionalComponent创建函数式组件

函数式组件不会被记录在组件的父子关系中,在Vue3 中因为所有的组件都不用 new 了,所以在性能上没有了优势~。

vue中的过滤器

过滤器是指不改变原始数组,只是对数据进行加工处理后返回过滤后的数据再进行第哦啊用处理,我们也可以理解成纯函数。

常见场景:单位转换、千分符、文本格式化、时间格式化等操作。

写个方法不香吗,vue3中果断放弃了过滤器

1
2
3
vue.filter('filerA',funciton(value){
// 返回处理后的值
})

v-once的使用场景

v-once概念

v-once是vue中的内置指令,只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化性能。

v-for中使用v-once

vue3.2之后,增加了v-memo指令,通过依赖列表的方式控制页面渲染。

Vue.mixin的使用场景和原理

Vue.mixin概念

mixin可以用来扩展组件,将公共逻辑进行抽离。在需要该逻辑时进行“混入”,采用册罗模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件数据为准。

mixin中有很多缺陷:”命名冲突问题“、”数据来源问题“,vue3采用compositionAPI提取公共逻辑非常方便

混入方式

在vue中我们可以局部混入和全局混入。一般情况下全局混入用于编写插件。局部混入用于复用逻辑。

mixin合并策略

核心就是:对象的合并处理

  • props、methods、inject、computed同名时会被替换
  • data会被合并
  • 生命周期和watch方法会被合并成队列
  • components、directives、filters会在原型链上叠加

组件的扩展除了mixin只歪还有一个属性叫extends,但是不怎么常用

Vue中slot如何实现

什么是插槽

插槽设计来源于 Web Components 规范草案,利用s1ot进行占位,在使用组件时,组件标签内部内容会分发到对应的 slot 中。

何时使用

通过插槽可以让用户更好的对组件进行扩展和定制化。可以通过具名插槽指定染的位置。常用的组件例如:弹框组件、布局组件、表格组件、树组件…

对双向绑定的理解

概念

vue中双向绑定靠的是指令vodel,可以绑定一个动态值到视图上,同时修改视图能改变数据对应的值(能修改的视图就是表单组件)经常会听到一句话:v-model是value+input 的语法糖。

表单元素中的v-model

内部会根据标签的不同解析出不同的语法。并且这里有“额外“的处理逻辑

  • 例如 文本框会被解析成 value +input事件

  • 例如 复选框会被解析成 checked+change事件

  • ……

底层是将v-model编译为一个directive指令

组件中的v-model

组件上的 v-mode1 默认会利用名为 va1ue 的 prop和名为 input 的事件。对于组件而言 v-model 就是value +input 的语法糖。可用于组件中数据的双向绑定。

名字也可以修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
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的。vue3中可以通过以下方法进行绑定。

1
<my v-model:a="a" v-model:b="b" v-model :c="c"></my>

vue中.sync修饰符的作用

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”,这时可以使用.sync来实现。`v-model默认只能双向绑定一个属性,这里就可以通过.sync修饰符绑定多个属性。

vue3中.sync语法被移除

vue中递归组件理解

菜单组件:

1
2
3
4
5
6
7
8
9
10
<el-menu>
<el-menu-item>根1</e1-menu-item>
<e1-submenu>
<template slot="title">根2</template>
<e1-menu-item>根2-1</e]-menu-item>
<e1-menu-item>根2-2</e1-menu-item>
</e1-submenu>
<e1-menu-item>根3</e1-menu-item>
<el-menu-item>根4</e]-menu-item>
</el-menu>

模板递归

1
2
3
4
5
<e]-menu>
<template v-for="item in data">
<resub :data="item" :key="item.id"></resub>
</template>
</e1-menu>

编写递归组件resub,在组件中调用自己

1
2
3
4
5
6
<e1-submenu :key="data.id" v-if="data.children">
<template slot="title">{{data.title}}</template>
<template v-for="item in data.children">
<resub :key="item.id" :data="item"></resub>
</template>
</e1-submenu>

组件中的name属性

  • 增加 name 选项会在components属性中增加组件本身,实现组件的递归调用。
  • 可以标识组件的具体名称方便调试(devtools)和查找对应组件。
  • $children.filter(item=>item.$options.name === ‘xxx’) 查找
1
Sub.options.components[name]=sub;//子组件会通过name属性,将自己也注册到组件中

vue中的修饰符

  • 表单修饰符iazy、trim、number
  • 事件修饰符stop、prevent、self、once、capture、passive、native
  • 鼠标按键修饰符 left、right、middle
  • 键值修饰符 对 keyCode 处理
  • .sync 修饰符

vue中异步组件作用及其原理

异步组件概念

Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。推荐的做法是将异步组件和 webpack的 code-splitting 功能一起配合使用。

异步组件写法

回调写法

1
2
3
4
5
6
7
8
9
10
11
12
13
{
components:{
'my-comonents':(resolve,reject)=>{
setTimeout(fucntion(){
resolve({
render(h){
return h('div','hello')
}
})
},1000)
}
}
}

promise写法

1
2
3
4
5
{
components:{
'my-comonents': () => {import(/* webpackChunkName*/ "./components/b4.vue")}
},
}

对象写法

1
2
3
4
5
6
7
8
9
10
11
12
const Asynccomponent= ()=>({
//需要加载的组件(应该是一个`Promise`对象)
component: import("./Mycomponent.vue"),
//异步组件加载时使用的组件
1oading:Loadingcomponent
//加载失败时使用的组件
error: Errorcomponent,
//展示加载时组件的延时时间。默认值是 200(亳秒)delay:200,
//如果提供了超时时间且组件加载也超时了,则使用加载失败时使用的组件。默认值是:`Infinity
timeout: 3000.3;
})

异步组件原理

  • 默认渲染异步占位符节点
  • 组件加载完毕后调用$forceUpdate强制更新,渲染加载完毕后的组件

对nextTick的理解

nextTick概念

  • Vue 中视图更新是异步的,使用 nextTick 方法可以保证用户定义的逻辑在更新之后执行。
  • 可用于获取更新后的 DOM,多次调用 nextTick 会被合并。

keep-alive组件平时在哪里使用

keep-alive 是 vue 中的内置组件,能在组件切换过程会缓存组件的实例,而不是销毁它们。在组件再次重新激活时可以通过缓存的实例拿到之前渲染的 DOM 进行渲染,无需重新生成节点。

使用场景

动态组件可以使用keep-alive进行缓存

1
2
3
<keep-alive :include="whiteList" :exclude="blackList":max="count">
<component :is="component"></component>
</keep-alive>

在路由中使用keep-alive

1
2
3
<keep-alive :include="whiteList" :exclude="blackList":max="count">
<router-view></router-view>
</keep-alive>

也可以指定meta属性指定哪些页面需要缓存,哪些不需要

1
2
3
4
5
6
7
8
<div id="app">
<keep-alive :include="whiteList" :exclude="blackList":max="count">
<!--需要缓存的视图组件-->
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!--不需要缓存的视图组件-->
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>

原理

……有待深化

自定义指令的应用场景

Vue 除了内置指令之外,同时 Vue 也允许用户注册自定义指令来对 Vue 进行扩展。指令的目的在于可以将操作 DOM 的逻辑进行复用。

指令的生命周期

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。
  • componentupdated:指令所在组件的VNode 及其子VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

常见的指令编写

  • 图片懒加载v-lazy
  • 防抖v-debounce
  • 按钮权限v-has
  • 拖拽指令v-draggable、mousemove、mouseup、dragenter、drop、可视化拖拽编辑器
  • 点击事件处理v-click-outside
1
<div v-click-outside="hide"><input type="text" @focus="'show" /><div v-if="isshow">显示面板</div></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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(e1){
document.removeEventListener("click",el.hander);
},
});

vue中使用了哪些设计模式

  • 单例模式:单例模式就是整个程序中有且金瓯一个实例,vuex中的store
  • 工厂模式:传入参数即可创建实例(createElment)
  • 发布订阅者模式:订阅者吧自己想订阅的事件注册到调度中心,当该事件触发时,发布者发布该事件到调度中心,有调度中心统一调度订阅者注册到调度中心的处理代码
  • 观察者模式:watcher和dep的关系
  • 代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用
  • 装饰模式:vue2装饰器的用法(对功能进行增强@)
  • 中介者模式:中介者是一个行为设计模式,通过提供一个统一的接口让胸痛的不同部分进行通信。
  • 策略模式:指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案
  • 外观模式:提供了统一的接口,用来访问子系统中的一群接口
  • ……

vue中的性能优化有哪些

  • 数据层级不易过深,合理设置响应式数据
  • 通过Object.freeze()方法冻结属性
  • 使用数据时缓存值的结果,不频繁取值
  • 合理设置key属性
  • v-show和v-if的选取
  • 控制组件粒度->vue采用组件级更新
  • 采用函数式组件->函数式组件开销低
  • 采用异步组件->借助webpack分包的能力
  • 使用keep-alive缓存组件、v-once
  • 分页、虚拟滚动、时间分片等策略