Vue最全面试总结,没有之一

814 阅读10分钟

Vue概念篇

Vue的优缺点

优点:

  1. 数据驱动试图:虚拟DOMdiff算法、响应式、观察者、异步队列,最小代码更新DOM,渲染视图
  2. 组件化
  3. 丰富的API
  4. Vue ssr服务端渲染
  5. 生命周期
  6. 社区、生态 缺点:
  7. 不支持IE8及以下浏览器(Object.defineProperty实现响应式)
  8. 不利于seo优化

Vue基础篇

watch 和计算属性有什么区别?

  1. computed计算属性是基于它们的响应式依赖进行缓存的,响应式依赖变化时才会重新求值。性能得到很大的提升。
  2. watch 侦听器监听数据变化执行异步或开销较大的操作。
  3. watch详细用法

v-showv-if 的区别

  1. v-show通过 css display属性控制元素的显示与隐藏
  2. v-if 组件和元素真正的渲染和销毁
  3. 频繁切换使用v-show,初始化后状态不再改变用v-if

v-for为什么要使用key

  1. key必须存在,并且不能使用下标和随机数
  2. diff算法时,通过标签tagkey对比识别VNode
  3. 减少渲染次数,提升渲染性能

v-forv-if不能同时使用

  1. v-for 的优先级比 v-if 更高,先循环渲染,再判断销毁,增加了不必要的渲染
  2. 可以使用计算属性过滤,再循环
  3. 可以在循环外层,根据数据的长度判断整体的显示与隐藏

监听键盘事件

  1. 按键修饰符@keyup
  2. addEventListenerremoveEventListener监听和移除keyup事件

组件内 data 为什么必须是一个函数?

每个实例可以维护一份被返回对象的独立的拷贝,否则相同数据会共用

生命周期

所有生命周期钩子的 this 上下文将自动绑定至实例中,因此你可以访问 data、computed 和 methods。这意味着你不应该使用箭头函数来定义一个生命周期方法 分为四个过程:创建、挂载、更新、销毁

创建

  • beforeCreate初始化实例,实例上只有一些默认的生命周期和默认的事件,其他均为创建
  • created实例创建完成,以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数

挂载

  • beforeMount render函数被调用,生成虚拟DOM
  • mounted是在模板渲染成HTML之后调用的,此时datael都已准备好,可以操作htmldom节点

更新

数据更新调用beforeUpdateupdated

  • beforeUpdate数据更新后,新的虚拟DOM生成,还没和旧的虚拟DOM对比打补丁,适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。
  • updated数据更改后,虚拟DOM重新渲染和更新完毕,可以进行DOM操作

销毁

  • beforeDestroy实例销毁之前调用。在这一步,实例仍然完全可用。可清除定时器和手动添加的事件监听器。
  • destroyed实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。

异步请求应该在那个生命周期访问?

created钩子函数data就已经创建,所以createdbeforeMountmounted都可以进行异步请求,主要看异步请求后是否有DOM操作,需要DOM操作在mounted钩子函数异步请求。

父子组件生命周期更新过程

渲染过程

父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted

父子组件实现双向数据流更新过程

父 beforeUpdate->子 beforeUpdate->子 updated->父 updated

销毁过程

父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

$nextTick是什么?

  1. Vue是异步渲染
  2. data改变之后,DOM不会立即渲染
  3. $nextTick会在DOM渲染之后触发,以获取最新的DOM节点

Vue组件的通讯方式有哪些?

Vue组件的通讯方式

Vue进阶篇

自定义组件的 v-model

  1. 一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件。
  2. 像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的,子组件种可以使用model.prop更改默认的 prop名,model.event更改默认的事件名。
// 父组件
// 默认形式
<VmodelComponent :value="cval" @input="cval = $event"></VmodelComponent>
// v-model形式
<VmodelComponent v-model="cval"></VmodelComponent>
// 子组件
// 注:input要使用:value,不能用v-model
<template>
  <div>
    <input type="text" :value="value" @input="$emit('input', $event.target.value)"/>
  </div>
</template>
<script>
export default {
    props: ["value"]
}
</script>

插槽 slot

具名插槽

组件内有多个插槽时,使用具名插槽。

// 子组件
<slot name="default"></slot>
// 父组件
<template v-slot:default>
    父组件给子组件插入的内容
</template>
// 简写
<template #default></template>

作用域插槽

插槽内容使用子组件内的数据。

// 子组件
<slot name="default" :child="child"></slot>
// 父组件
<template v-slot:default="slotProps">
    {{ slotProps.child }}
</template>
// 解构
<template v-slot:default="{child}">
    {{ child }}
</template>
// 解构并重新命名
<template v-slot:default="{child: childRename}">
    {{ childRename }}
</template>
// 当子组件没有传值时,添加后备内容
<template v-slot:default="{child='后备内容'}">
    {{ child }}
</template>

keep-alive的作用

  1. 缓存组件
  2. 频繁切换,不需要重新渲染(tab页切换)

混入 mixin

  1. 多个组件有相同的逻辑,抽离出来 mixin和组件有冲突时:
  • datamethodscomponentsdirectives:组件覆盖混入的
  • 生命周期:混入在组件之前执行
  1. mixin并不是完美的解决方案,会有一些问题
  • 变量来源不明确,不便于理解
  • mixin会造成命名冲突
  • mixin和组件可能出现多对多的关系,复杂度较高

自定义指令

指令本质上是装饰器,是 vueHTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。

// 复制指令
Vue.directive('copy', {
    bind (el, { value }) {
        el.$value = value
        el.handler = () => {
            debugger
            // 创建input标签
            const input = document.createElement('input')
            // 将input的值设置为需要复制的内容
            input.value = el.$value
            // 添加input标签
            document.body.appendChild(input)
            // 选中input标签
            input.select()
            // 执行复制
            document.execCommand('copy')
            // 移除input标签
            document.body.removeChild(input)
        }
        // 绑定点击事件,就是所谓的一键 copy 啦
        el.addEventListener('click', el.handler)
    },
    // 当传进来的值更新的时候触发
    componentUpdated (el, { value }) {
        el.$value = value
    },
    // 指令与元素解绑的时候,移除事件绑定
    unbind (el) {
        el.removeEventListener('click', el.handler)
    }
})
// 应用
<template>
    <div>
        <input v-model="inputVal" />
        <div v-copy="inputVal">复制</div>
    </div>
</template>
<script>
export default {
    data () {
        return {
            inputVal: ""
        }
    }
}
</script>

Vue.use()做了什么?

Vue.use() 执行插件的install方法,它需要在你调用 new Vue() 启动应用之前完成。比如路由的引用。

Vue高级篇

MVVM的理解 Model-View-ViewModel

核心概念:数据驱动视图View视图层,Model数据层,ViewModel数据和视图之间的连接层。

图中的DOM ListenersData Bindings看作两个工具,它们是实现双向绑定的关键。

View侧看,ViewModel中的DOM Listeners工具会帮我们监测页面上DOM元素的变化,如果有变化,则更改Model中的数据;

Model侧看,当我们更新Model中的数据时,Data Bindings工具会帮我们更新页面中的DOM元素。

image.png

响应式原理

  1. 数据劫持:Vue 将遍历data选项种所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter
  2. 观察者模式:每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
  3. 异步更新队列:Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。

Object.defineProperty缺点:

  1. 深度监听,需要深度递归遍历
  2. 无法监测新增和删除属性(Vue.set Vue.delete
  3. 无法原生监听数组,需要特殊处理(Object.create重写数组方法'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
  4. 不支持 IE8 以及更低版本浏览器

image.png

是么是虚拟 DOMdiff 算法

虚拟 DOM

Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM,通过 JS 模拟 DOM 结构,计算出最小的变更,操作 DOM;数据驱动试图的模式下,有效控制 DOM 操作。

Vue 通过 createElement 来创建虚拟节点,即 VNode,虚拟 DOM 是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

// createElement参数
createElement(
    // {String | Object | Function}
    // 一个 HTML 标签名、组件选项对象,或者
    // resolve 了上述任何一种的一个 async 函数。必填项。
    'div',

    // {Object}
    // 一个与模板中 attribute 对应的数据对象。可选。
    {
        // (详情见下一节)
    },

    // {String | Array}
    // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
    // 也可以使用字符串来生成“文本虚拟节点”。可选。
    [
        '先写一些文字',
        createElement('h1', '一则头条'),
        createElement(MyComponent, {
            props: {
                someProp: 'foobar'
            }
        })
    ]
)

diff 算法 - 双端比较

新旧 vnode 都包含 children 子元素且不相等的情况下,diff 算法计算出最优的更新方案。

双端比较:旧列表和新列表头头、尾尾、头尾、尾头作比较,对比个过程中指针不断向内靠拢,直到循环结束

// 来源 snabbdom 源码
function updateChildren (parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0, 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);
        }
        // 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
  
        // 没对应上
        if (isUndef(idxInOld)) { // New element
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
          newStartVnode = newCh[++newStartIdx];
        
        // 对应上了
        } else {
          // 对应上 key 的节点
          elmToMove = oldCh[idxInOld];

          // sel 是否相等(sameVnode 的条件)
          if (elmToMove.sel !== newStartVnode.sel) {
            // New element
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
          
          // sel 相等,key 相等
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
          newStartVnode = newCh[++newStartIdx];
        }
      }
    }
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      } else {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }

模板编译

vue-template-compiler 将模板编译为 render 函数的过程:

  • parse函数的作用就是把字符串型的 template 转化为 AST 抽象语法树结构
  • optimize优化,对静态节点做标记。减少了 diff 算法比较的过程
  • generate将 AST 生成 render 函数(with 语法)

渲染过程

vue-template-compiler 将模板编译为 render 函数,执行 render 函数生成vnode,通过vnode 组件成的树称为虚拟 DOM,虚拟 DOM 通过 diff 算法更新为真实 DOM 结构。

初次渲染

  • 解析模板为 render 函数
  • 触发响应式,监听 data 属性的 gettersetter
  • 执行 render 函数,生成 vnode, 通过 diff 算法生成真实 DOM

更新过程

  • 修改 data ,触发 setter(初次渲染时已经监听)
  • 重新执行 render 函数,生成 newVnode
  • 通过 diff 算法生成真实 DOM

vue-router路由管理器

常用的路由钩子函数

全局守卫

  1. beforeEach 全局前置守卫
  • 判断是否有 token,跳转登录页
  • 通过角色权限动态添加路由
  • 修改页面的 title
  • 判断当前所处浏览器,跳转非微信环境页面
  1. beforeResolve 全局解析守卫
  2. afterEach 全局后置守卫
  • 第三方埋点的注册

路由守卫

  1. beforeEnter 路由独享守卫

组件守卫

  1. beforeRouteEnter
  • 守卫执行前,组件实例还没创建,不能使用 this
  • 可以在 next 回调函数种访问组件实例
  1. beforeRouteUpdate
  • 动态路由组件复用时调用
  1. beforeRouteLeave
  • 禁止未保存修改前离开
  • 清除定时器和事件监听

组件复用导致动态参数失效?

路由参数更改时,组件实例被复用,但组件的生命周期钩子不会再被调用。那怎么实时获取动态参数呢?

  1. 监听路由的变化
  • 监听路由 immediatetrue时,首次也会进行监听,不需要在生命周期内获取动态参数
  • 监听路由 immediatefalse时,需要在 mounted 生命周期进行首次获取动态参数
mounted () {
    console.log("mouted", this.$route.params.id)
},
watch: {
    $route: {
        handler: function (to, from) {
            // 当前的动态参数 to.params.id
            console.log("watch", to.params.id)
        },
        immediate: true // 立即执行
    }
}
  1. 组件内路由 beforeRouteUpdate,组件复用时调用,需要在mounted 生命周期进行首次获取动态参数
mounted () {
    console.log("mouted", this.$route.params.id)
},
// 在当前路由改变,但是该组件被复用时调用
beforeRouteUpdate (to, from, next) {
    console.log("beforeRouteUpdate", to.params.id)
}
  1. 添加 key 阻止组件复用,不建议使用,组件销毁再创建,增加开销
<router-view :key="$route.fullPath" />

$router$route 的区别?

路由全局注入后,组件内可以通过 this.$router 访问 router 的实例,就可以调用实例方法 pushgo等;this.$route 获取当前路由信息对象,获取路由上的 paramsquery

路由模式

hash

  • 通过 location.hash 获取 urlhash 值(从 # 号开始的部分)
  • hash 的变化会触发网页的跳转,即浏览器的前进和后退
  • hash 的变化不会触发页面刷新
  • window.onhashchange 监听 hash 的变化

history

  • 依赖 HTML5 History API 和服务器配置
  • history.pushState 路由的跳转,不刷新页面
  • window.onpopstate 监听路由的前进和后退

路由懒加载

打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

const router = new VueRouter({
  routes: [{ path: '/foo', component: () => import(/* webpackChunkName: "group-foo" */ './Foo.vue'}]
})

Vuex

Vuex是什么?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

应用场景:

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态

基本概念:

  • State:单一的状态数,存储在 Vuex 中的数据和 Vue 实例中的 data 遵循相同的规则
  • Gettersstore 的计算属性,跟计算属性一样也会有缓存
  • Mutations:更改state的唯一方法,通过 store.commit 函数触发,必须是同步函数
  • Actions:不能直接更改state,通过 store.dispatch 函数触发,可以包含异步函数
  • Modules: 模块,将store分割成模块,根据不同的功能分割模块,便于状态管理 image.png

欢迎大家补充,让更多的XDJM学习并传播。码字不易,评论、点赞、收藏三连哟。