关于前端面试那些题(二)
Vue部分
Vue2 双向绑定
当视图发生变化时,数据也发生变化,反之亦然。双向数据绑定的数据和DOM是一个双向关系。数据响应式是双向绑定的一环。
MVVM
MVVM是一种软件架构设计模式,是由三个部分组成:
-
Model-数据模型:应用的数据及业务逻辑
-
View-UI视图:应用的展示效果,各类UI组件
-
ViewModel:负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作。由两个重要部分组成。
- Obsever(监听器):对所有数据属性进行监听
- Compiler(解析器):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定响应的更新函数。
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
数据响应式
数据响应式作用是 数据驱动。Vue采用 数据劫持 + 发布者-订阅者模式。
原理设计
创建Vue实例时,Vue遍历传入的数据对象,对其进行递归遍历,并使用Object.defineProperty
将对象属性(propety)转化为响应式属性(getter/setter)。转化时,Vue为每个属性创建一个 Dep
(依赖)对象,收集当前属性的依赖关系。
getter 方法用来进行依赖追踪,判断当前是否存在一个全局Dep对象,存在则将当前属性的Dep对象添加到全局Dep对象的依赖列表中,然后将属性和对象的Watcher对象关联起来。
当属性发生变化时,会触发setter方法,并通知该属性的Dep对象,然后Dep对象遍历依赖列表,通知每个依赖的Watcher对象进行更新操作。
Watcher对象作用是建立依赖关系,检测数据变化,并在数据变化时执行相应的更新操作。Watcher机制是基于异步更新的,这样能避免频繁更新,提高性能和优化用户体验。
Object.defineProperty是ES5中无法shim的特性,所以Vue不支持IE8以及更低版本浏览器
Object.defineProperty() 进行数据劫持有什么缺点?
对于数组、对象,部分操作不能触发组件重新渲染(见下方注意事项),Vue通过内部重写函数解决这个问题。Vue3不再使用这种方法,而是通过Proxy对对象进行代理,实现数据劫持。
注意事项
Vue不能检测数组和对象的变化。
对于对象
属性必须在data对象上存在才能被Vue转换为响应式。对于已创建的实例,不允许动态添加根级别的响应式属性,但可以使用 vue.set(objet, propertyName, value)
或者 this.$set(object, propertyName, value)
向对象添加响应式属性。
对于数组
无法检测的数组变动:
-
利用索引直接设置一个数组项时;
- 解决方法:利用
Vue.set(vm.items, indexOfItem, newValue)
或者vm.items.splice(indexOfItem, 1, newValue)
或者this.$set()
- 解决方法:利用
-
修改数组长度时;
- 解决方法:
vm.items.splice(newlength)
- 解决方法:
异步更新 nextTick
作用:在DOM循环结束后执行延迟回调
- 接收回调函数
- 把回调函数放到任务队列中(数组)
- 主线程执行完毕,开始遍历数组,依次执行
Vue3 响应式原理
基于proxy的Observer。从对象层面进行拦截,解决了vue2中使用Object.defineProperty的一些缺陷:
- 深度递归,性能消耗大
- 无法拦截新增/修改属性
- 无法拦截原生数组索引操作。
原理:
- 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
- 通过Reflect(反射): 对源对象的属性进行操作。
虚拟DOM
React 、Vue都涉及到 虚拟DOM(Virtual DOM)这个概念。实际上,它只是对真实DOM的抽象,以JavaScript
对象 (VNode
节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上。
实际表现为 Object对象,并且最少包含标签名tag、属性attrs、子元素对象children。
为什么需要虚拟DOM?
因为真实的DOM节点,包含许多的属性,操作起来不方便,且频繁操作会出现页面卡顿问题,影响用户体验。例如:更新10个DOM节点,虚拟DOM不会立马操作,而是将10次更新的diff内容,保存到本地的一个js对象中,最终一次性attach到DOM上。
- 具备跨平台优势:针对不同平台进行渲染
- 提高渲染性能
很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI
vue实现虚拟DOM
实现VNode:通过createElement
生成 VNode
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
functionalContext: Component | void; // only for functional component root nodes
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data
/*当前节点的子节点,是一个数组*/
this.children = children
/*当前节点的文本*/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
/*当前节点的名字空间*/
this.ns = undefined
/*编译作用域*/
this.context = context
/*函数化组件作用域*/
this.functionalContext = undefined
/*节点的key属性,被当作节点的标志,用以优化*/
this.key = data && data.key
/*组件的option选项*/
this.componentOptions = componentOptions
/*当前节点对应的组件的实例*/
this.componentInstance = undefined
/*当前节点的父节点*/
this.parent = undefined
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*静态节点标志*/
this.isStatic = false
/*是否作为跟节点插入*/
this.isRootInsert = true
/*是否为注释节点*/
this.isComment = false
/*是否为克隆节点*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next https://github.com/answershuto/learnVue*/
get child (): Component | void {
return this.componentInstance
}
}
- 所有对象的context(作用域) 指向 Vue实例
- elm属性指向真实对应的DOM节点
如何利用虚拟DOM来更新真实DOM
利用diff算法,vue的diff算法和 react的diff算法有区别。后续单独整理。
在构建DOM的过程中,由diff过程就是比对计算DOM变动的地方,核心是由patch算法将变动映射到真实DOM上。
vue中key的作用
key作为vue中vnode的唯一标记,diff算法可以通过这个key准确快速进行操作。
- v-if使用key:使用key保证元素不会被复用,标识一个独立元素。
- v-for使用key:v-for默认使用“就地复用”策略,key便于Vue追踪元素,实现高效复用。
不推荐inde作为key值的原因:不管顺序怎么颠倒,index都是0,1,2,3...,会导致vue复用错误节点。
Vue初始化及生命周期
初始化渲染
- 解析模板,通过编译生成AST语法树
- 使用AST生成 render渲染函数,从而生成虚拟DOM树
- 通过diff算法,对比新旧虚拟DOM树
- 根据虚拟DOM树生成渲染成真实DOM
拓展
- runtime-compiler:template -> ast -> render() -> VDom -> 真实DOM
- runtime-only:render() -> VDom -> 真实DOM
相关问题
-
为什么组件只有一个根节点?
因为Vue2的模版编译器在编译模板时,会将模板转换成一个render函数,而render函数只能返回一个单一根节点。但Vue3使用的是Fragment,可以使用
<template>
或Fragment
组件包裹多个根元素。 -
render函数封装有什么特别?
可以更灵活控制组件渲染,提高组件性能。
- 可以使用JSX语法;
- 可以使用函数式组件(没有状态和实例的组件,仅接受props作为参数并返回一个VNode);
- 可以使用插槽;
- 可以使用动态属性
生命周期各阶段使用场景
beforeCreate
:执行一些初始化任务,此时获取不到props
、data
、computed、watch 中的数据以及methos中的方法。created
:组件初始化完毕,可以访问各种数据,获取接口数据等。请求不宜过多,避免白屏时间太长。beforeMount
:此时开始创建 VDOMmounted
:dom
已创建渲染,可用于获取访问数据和dom
元素;访问子组件等。beforeUpdate
:此时view
层还未更新,可用于获取更新前各种状态updated
:完成view
层的更新,更新后,所有状态已是最新beforeDestroy
:实例被销毁前调用,可用于一些定时器或订阅的取消destroyed
:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器keep-alive
独有的生命周期,分别为activated
和deactivated
。用keep-alive
包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行deactivated
钩子函数,命中缓存渲染后会执行actived
钩子函数。
常见问题
1、父子组件生命周期顺序
加载渲染:父beforeCreate - 父created - 父beforeMount- 子beforeCreate - 子created - 子beforeMount -子mounted - 父mounted
更新过程:父beforeUpdate - 子beforeUpdate - 子updated - 父updated
销毁过程:父beforeDestroy - 子beforeDestory - 子destroyed - 父destroyed
2、created 和 mounted这两个生命周期中请求数据的区别?
created 请求时,dom节点还未生成;mounted是在dom节点渲染后执行的。
放在mounted请求可能导致页面闪动。
插槽 slot
插槽是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。这一个标签元素是否显示,以及怎么显示是由父组件决定的。
slot分三类:
- 默认插槽:当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
- 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。
- 作用域插槽:可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
<!-- 匿名插槽 -->
<slot></slot>
<!-- 具名插槽, v-slot只能用在template上 -->
<slot name="content"></slot>
<template v-slot:content></template>
<!-- 匿名插槽 -->
<!-- 子组件 -->
<template>
<slot name="footer" testProps="子组件的值">
<h3>没传footer插槽</h3>
</slot>
</template>
<!-- 父组件 -->
<child>
<!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
<template v-slot:default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
</template>
<template #default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
</template>
</child>
在编译时,slot作为一个占位符,被解析成一个函数。具体编译步骤如下:
- 先解析父组件A,把子组件当成子元素B处理,把插槽当成孙子元素C处理。
- 解析子元素B,slot成函数_t('插槽名称'),返回解析后的节点
小结
v-slot
属性只能在<template>
上使用,但在只有默认插槽时可以在组件标签上使用- 默认插槽名为
default
,可以省略default直接写v-slot
- 缩写为
#
时不能不写参数,写成#default
- 可以通过解构获取
v-slot={user}
,还可以重命名v-slot="{user: newName}"
和定义默认值v-slot="{user = '默认值'}"
keep-alive
可以实现组件缓存,组件切换时不对当前组件进行卸载。
包含三个属性:include、exclude、max。包含两个生命周期:activeed 、deactivated。
原理
- 获取keep-alive组件(是抽象组件,不渲染)包裹的第一个子元素对象。【keep-alive要求同时只有一个子元素被渲染】
- 根据黑白名单进行条件匹配,决定是否匹配。如果匹配则缓存,否则直接返回组件实例。
- 根据组件ID和tag生成缓存key,在缓存对象中查找是否已缓存过,存在,则直接取出缓存值并更新key的位置(影响置换算法);如果不存在,在cache对象中存储该组件实例并保存key值。
- 检查缓存实例数是否超过max值,根据LRU置换策略删除最近最久未使用的实例。
例子
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
拓展:
keep-alive
在各个生命周期里都做了什么:
created
:初始化一个cache、keys
,前者用来存缓存组件的虚拟dom集合,后者用来存缓存组件的key集合mounted
:实时监听include、exclude
这两个的变化,并执行相应操作destroyed
:删除掉所有缓存相关的东西
v-for、v-if、v-show
v-show 和 v-if 的区别?
v-if是真实的条件渲染,只有在首次为true时才会被渲染。v-show是被css的display控制,无论如何都会被渲染。v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show
较好;如果在运行时绑定条件很少改变,则 v-if
会更合适
v-for 和 v-if 不共存的原因?
vue2中,v-for的优先级比v-if高,编译时会先循环,在循环中进行判断。假如实际只需要2个节点,但循环了3次,会造成性能浪费。
vue3中,v-if的优先级高于v-for,先判断是否渲染,确定了再走循环,所以提高了性能,但是还是会报错,判断的依据在循环体外,无法拿到。
// 🌰:
<template>
<div v-for="item in [1,2,3]" v-if="item !== 2"></div>
</template>
// 编译后是
<template>
<tempalte v-if="item !== 2">
<div v-for="item in [1, 2, 3]" />
</tempalte>
</template>
vue默认就地更新,数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。
Vue-Router
是Vue官方路由器。搭配组件<router-link>
和<router-view>
组件使用。
动态路由
有时候我们的需求需要将给定匹配模式映射到同一个组件,此时可以使用一个动态字段来实现,这个动态字段称为 路径参数。使用 :
标识,当路由被匹配时,会以$route.params的形式暴露出来。同一个路径可以设置多个路由参数。
// 🌰
const routes= [
{ path: '/users/:username' }, // eg: /users/lumi ==> $route.params{ username: lumi }
{ path: '/users/:username/:id' }, // eg: /users/lumi/001 ==> $route.params{ username: lumi, id: 001 }
]
匹配规则
可重复参数
如果需要匹配多个部分路由,可以使用 *
(0个或多个) 和 +
(1个或多个)将参数标记重复。最终会提供成一个参数数组。
const routes = [
// /:chapters -> 匹配 /one, /one/two, /one/two/three, 等
// params:{ chapters: [] }
{ path: '/:chapters+' },
// /:chapters -> 匹配 /, /one, /one/two, /one/two/three, 等
{ path: '/:chapters*' },
]
可选参数
可以通过?
修饰符(0/1个)将参数标记为可选
历史模式
- Hash模式:使用
createWebHashHistory()
创建,在实际URL前面使用了一个#。对于SEO有不好的影响。 - Memory模式:用
createMemoryHistory()
创建。不会与URL交互也不会自动触发初始导航,需要使用**app.use(router)
之后手动 push 到初始导航**。适合Node环境和SSR。不会有历史记录,不能前进或后退 - HTML5模式:用
createWebHistory()
创建 。如果没有适当服务配置,访问一个特定页面会得到404【解决:在服务器添加一个回退路由】
导航守卫
主要用来通过跳转或取消的方式守卫导航。守卫是异步执行。
全局钩子有三个:beforeEach、beforeResolve、afterEach
路由独享守卫有:beforeEnter
组件内守卫有:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
全局前置守卫
使用router.beforeEach
注册。当触发导航时,前置守卫按照顺序调用。
接收两个参数:
- to:即将进入的目标
- from:当前导航要离开的路由
- next(可选)
可以返回的值:
- false:取消当前导航。重置到from的地址
- 一个路由地址:重定向到另一个不同的地址
- undefined / true:导航有效
- 错误:抛出Error,取消导航并调用 router.onError()注册过的回调
全局解析守卫
使用 router.beforeResolve 注册。在每次导航时都会被调用,在导航被确定前、组件内守卫和异步路由组件被解析后调用。
是获取数据或执行其他操作的理想位置。
懒加载
一般来说,js包会很大,这会影响到页面的加载,利用路由懒加载可以等路由被访问时才加载对应组件,更高效。
给component
选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。
- 箭头函数+import
- 箭头函数+ require
- webpack的require.ensure技术,实现按需引入
通信方式
-
props + emit:父组件通过props向下传递数据给子组件。子组件使用emit:父组件通过props向下传递数据给子组件。子组件使用emit:父组件通过props向下传递数据给子组件。子组件使用emit,父组件以v-on方式接收实现子组件向父组件传值。
-
emit+emit + emit+on:通过一个空Vue实例作为事件总线,用它来触发事件和监听事件,实现任何组件间的通信。
-
vuex(后续单独讲)
-
attrs+attrs + attrs+listeners:
- attrs:包含了父作用域中不被prop所识别(且获取)的特性绑定(class和style除外)。可以通过v−bind="attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。可以通过 v-bind="attrs:包含了父作用域中不被prop所识别(且获取)的特性绑定(class和style除外)。可以通过v−bind="attrs" 传入内部组件
- listeners:包含了父作用域中的(不含.native修饰器的)v−on事件监听器。它可以通过v−on="listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="listeners:包含了父作用域中的(不含.native修饰器的)v−on事件监听器。它可以通过v−on="listeners" 传入内部组件。
-
provide + inject:允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。注意:例子所写并非是可响应时的
// 祖父组件: export default { provide: { name: lumi } } // 孙子组件 export default { inject:['name'], methods: { console.log(this.name) } }
-
parent/parent / parent/children + ref
混入Mixin
mixin通常使⽤在复⽤请求逻辑, 公共内容注册(组件, 指令, 过滤器)
// 在 myMixin ⽂件声明
const myMixin = {
data() {
return {
a: 1,
b: 2,
c: 3
};
}
};
// 局部调⽤
import { defineComponent } from 'vue';
import myMixin from '@/mixins/myMixin';
export default defineComponent({
mixins: [myMixin],
});
// 全局调⽤
import myMixin from '@/mixins/myMixin';
app.mixin(myMixin);
不⾜:
- ⽤于多个组件时, 可能会多出很多不必要的选项或属性 (⽆线拆分, 冗余) 命名冲突
- ⽆法通过函数的参数进⾏控制, ⽆法通过动态传参调整 mixin 的 option 的混⼊情况 (⼲扰合理性复⽤)
Extend
是一个全局的API,提供了一种灵活挂载组件方法。常用于一些独立组件开发场景。
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')
转载自:https://juejin.cn/post/7353178694972866600