likes
comments
collection
share

关于前端面试那些题(二)

作者站长头像
站长
· 阅读数 4

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

摘自 vue3js.cn/interview/

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初始化及生命周期

关于前端面试那些题(二)

初始化渲染

  1. 解析模板,通过编译生成AST语法树
  2. 使用AST生成 render渲染函数,从而生成虚拟DOM树
  3. 通过diff算法,对比新旧虚拟DOM树
  4. 根据虚拟DOM树生成渲染成真实DOM

关于前端面试那些题(二)

拓展
  • runtime-compiler:template -> ast -> render() -> VDom -> 真实DOM
  • runtime-only:render() -> VDom -> 真实DOM

相关问题

  1. 为什么组件只有一个根节点?

    因为Vue2的模版编译器在编译模板时,会将模板转换成一个render函数,而render函数只能返回一个单一根节点。但Vue3使用的是Fragment,可以使用<template>Fragment组件包裹多个根元素。

  2. render函数封装有什么特别?

    可以更灵活控制组件渲染,提高组件性能。

    • 可以使用JSX语法;
    • 可以使用函数式组件(没有状态和实例的组件,仅接受props作为参数并返回一个VNode);
    • 可以使用插槽;
    • 可以使用动态属性

生命周期各阶段使用场景

  • beforeCreate:执行一些初始化任务,此时获取不到 propsdata、computed、watch 中的数据以及methos中的方法。
  • created:组件初始化完毕,可以访问各种数据,获取接口数据等。请求不宜过多,避免白屏时间太长。
  • beforeMount:此时开始创建 VDOM
  • mounteddom已创建渲染,可用于获取访问数据和dom元素;访问子组件等。
  • beforeUpdate:此时view层还未更新,可用于获取更新前各种状态
  • updated:完成view层的更新,更新后,所有状态已是最新
  • beforeDestroy:实例被销毁前调用,可用于一些定时器或订阅的取消
  • destroyed:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
  • keep-alive 独有的生命周期,分别为 activateddeactivated。用 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作为一个占位符,被解析成一个函数。具体编译步骤如下:

  1. 先解析父组件A,把子组件当成子元素B处理,把插槽当成孙子元素C处理。
  2. 解析子元素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。

原理

  1. 获取keep-alive组件(是抽象组件,不渲染)包裹的第一个子元素对象。【keep-alive要求同时只有一个子元素被渲染】
  2. 根据黑白名单进行条件匹配,决定是否匹配。如果匹配则缓存,否则直接返回组件实例。
  3. 根据组件ID和tag生成缓存key,在缓存对象中查找是否已缓存过,存在,则直接取出缓存值并更新key的位置(影响置换算法);如果不存在,在cache对象中存储该组件实例并保存key值。
  4. 检查缓存实例数是否超过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所识别(且获取)的特性绑定(classstyle除外)。可以通过vbind="attrs" 传入内部组件
    • listeners:包含了父作用域中的(不含.native修饰器的)v−on事件监听器。它可以通过v−on="listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="listeners:包含了父作用域中的(不含.native修饰器的)von事件监听器。它可以通过von="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);

不⾜:

  1. ⽤于多个组件时, 可能会多出很多不必要的选项或属性 (⽆线拆分, 冗余) 命名冲突
  2. ⽆法通过函数的参数进⾏控制, ⽆法通过动态传参调整 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')