写点什么

美团前端一面高频 vue 面试题整理

作者:bb_xiaxia1998
  • 2023-02-19
    浙江
  • 本文字数:14759 字

    阅读完需:约 48 分钟

params 和 query 的区别

用法:query 要用 path 来引入,params 要用 name 来引入,接收参数都是类似的,分别是 this.$route.query.namethis.$route.params.name


url 地址显示:query 更加类似于 ajax 中 get 传参,params 则类似于 post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示


注意:query 刷新不会丢失 query 里面的数据 params 刷新会丢失 params 里面的数据。

Vue3 有了解过吗?能说说跟 vue2 的区别吗?

1. 哪些变化



从上图中,我们可以概览Vue3的新特性,如下:


  • 速度更快

  • 体积减少

  • 更易维护

  • 更接近原生

  • 更易使用


1.1 速度更快


vue3相比vue2


  • 重写了虚拟Dom实现

  • 编译模板的优化

  • 更高效的组件初始化

  • undate性能提高 1.3~2 倍

  • SSR速度提高了 2~3 倍



1.2 体积更小


通过webpacktree-shaking功能,可以将无用模块“剪辑”,仅打包需要的


能够tree-shaking,有两大好处:


  • 对开发人员,能够对vue实现更多其他的功能,而不必担忧整体体积过大

  • 对使用者,打包出来的包体积变小了


vue可以开发出更多其他的功能,而不必担忧vue打包出来的整体体积过多



1.3 更易维护


compositon Api


  • 可与现有的Options API一起使用

  • 灵活的逻辑组合与复用

  • Vue3模块可以和其他框架搭配使用



更好的 Typescript 支持


VUE3是基于typescipt编写的,可以享受到自动的类型定义提示



1.4 编译器重写



1.5 更接近原生


可以自定义渲染 API



1.6 更易使用


响应式 Api 暴露出来



轻松识别组件重新渲染原因



2. Vue3 新增特性


Vue 3 中需要关注的一些新功能包括:


  • framents

  • Teleport

  • composition Api

  • createRenderer


2.1 framents


Vue3.x 中,组件现在支持有多个根节点


<!-- Layout.vue --><template>  <header>...</header>  <main v-bind="$attrs">...</main>  <footer>...</footer></template>
复制代码


2.2 Teleport


Teleport 是一种能够将我们的模板移动到 DOMVue app 之外的其他位置的技术,就有点像哆啦 A 梦的“任意门”


vue2中,像 modals,toast 等这样的元素,如果我们嵌套在 Vue 的某个组件内部,那么处理嵌套组件的定位、z-index 和样式就会变得很困难


通过Teleport,我们可以在组件的逻辑位置写模板代码,然后在 Vue 应用范围之外渲染它


<button @click="showToast" class="btn">打开 toast</button><!-- to 属性就是目标位置 --><teleport to="#teleport-target">    <div v-if="visible" class="toast-wrap">        <div class="toast-msg">我是一个 Toast 文案</div>    </div></teleport>
复制代码


2.3 createRenderer


通过createRenderer,我们能够构建自定义渲染器,我们能够将 vue 的开发模型扩展到其他平台


我们可以将其生成在canvas画布上



关于createRenderer,我们了解下基本使用,就不展开讲述了


import { createRenderer } from '@vue/runtime-core'
const { render, createApp } = createRenderer({ patchProp, insert, remove, createElement, // ...})
export { render, createApp }
export * from '@vue/runtime-core'
复制代码


2.4 composition Api


composition Api,也就是组合式api,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管理



关于compositon api的使用,这里以下图展开



简单使用:


export default {    setup() {        const count = ref(0)        const double = computed(() => count.value * 2)        function increment() {            count.value++        }        onMounted(() => console.log('component mounted!'))        return {            count,            double,            increment        }    }}
复制代码


3. 非兼容变更


3.1 Global API


  • 全局 Vue API 已更改为使用应用程序实例

  • 全局和内部 API 已经被重构为可 tree-shakable


3.2 模板指令


  • 组件上 v-model 用法已更改

  • <template v-for>和 非 v-for节点上key用法已更改

  • 在同一元素上使用的 v-ifv-for 优先级已更改

  • v-bind="object" 现在排序敏感

  • v-for 中的 ref 不再注册 ref 数组


3.3 组件


  • 只能使用普通函数创建功能组件

  • functional 属性在单文件组件 (SFC)

  • 异步组件现在需要 defineAsyncComponent 方法来创建


3.4 渲染函数


  • 渲染函数API改变

  • $scopedSlots property 已删除,所有插槽都通过 $slots 作为函数暴露

  • 自定义指令 API 已更改为与组件生命周期一致

  • 一些转换class被重命名了:

  • v-enter -> v-enter-from

  • v-leave -> v-leave-from

  • 组件 watch 选项和实例方法 $watch不再支持点分隔字符串路径,请改用计算函数作为参数

  • Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x 现在使用应用程序容器的 innerHTML


3.5 其他小改变


  • destroyed 生命周期选项被重命名为 unmounted

  • beforeDestroy 生命周期选项被重命名为 beforeUnmount

  • [prop default工厂函数不再有权访问 this 是上下文

  • 自定义指令 API 已更改为与组件生命周期一致

  • data 应始终声明为函数

  • 来自 mixindata 选项现在可简单地合并

  • attribute 强制策略已更改

  • 一些过渡 class 被重命名

  • 组建 watch 选项和实例方法 $watch不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。

  • <template> 没有特殊指令的标记 (v-if/else-if/elsev-forv-slot) 现在被视为普通元素,并将生成原生的 <template> 元素,而不是渲染其内部内容。

  • Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x 现在使用应用容器的 innerHTML,这意味着容器本身不再被视为模板的一部分。


3.6 移除 API


  • keyCode 支持作为 v-on 的修饰符

  • $on$off$once 实例方法

  • 过滤filter

  • 内联模板 attribute

  • $destroy 实例方法。用户不应再手动管理单个Vue 组件的生命周期。

Vue-router 除了 router-link 怎么实现跳转

声明式导航


<router-link to="/about">Go to About</router-link>
复制代码


编程式导航


// literal string pathrouter.push('/users/1')// object with pathrouter.push({ path: '/users/1' })// named route with params to let the router build the urlrouter.push({ name: 'user', params: { username: 'test' } })
复制代码


回答范例


  • vue-router导航有两种方式:声明式导航和编程方式导航

  • 声明式导航方式使用router-link组件,添加to属性导航;编程方式导航更加灵活,可传递调用router.push(),并传递path字符串或者RouteLocationRaw对象,指定pathnameparams等信息

  • 如果页面中简单表示跳转链接,使用router-link最快捷,会渲染一个 a 标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航

  • 实际上内部两者调用的导航函数是一样的

Watch 中的 deep:true 是如何实现的

当用户指定了 watch 中的 deep 属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新


源码相关


get () {     pushTarget(this) // 先将当前依赖放到 Dep.target上     let value     const vm = this.vm     try {         value = this.getter.call(vm, vm)     } catch (e) {         if (this.user) {             handleError(e, vm, `getter for watcher "${this.expression}"`)         } else {             throw e         }     } finally {         if (this.deep) { // 如果需要深度监控         traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法     }popTarget() }
复制代码

用过 pinia 吗?有什么优点?

1. pinia 是什么?


  • Vue3中,可以使用传统的Vuex来实现状态管理,也可以使用最新的pinia来实现状态管理,我们来看看官网如何解释pinia的:PiniaVue 的存储库,它允许您跨组件/页面共享状态。

  • 实际上,pinia就是Vuex的升级版,官网也说过,为了尊重原作者,所以取名pinia,而没有取名Vuex,所以大家可以直接将pinia比作为Vue3Vuex


2. 为什么要使用 pinia?


  • Vue2Vue3都支持,这让我们同时使用Vue2Vue3的小伙伴都能很快上手。

  • pinia中只有stategetteraction,抛弃了Vuex中的MutationVuexmutation一直都不太受小伙伴们的待见,pinia直接抛弃它了,这无疑减少了我们工作量。

  • piniaaction支持同步和异步,Vuex不支持

  • 良好的Typescript支持,毕竟我们Vue3都推荐使用TS来编写,这个时候使用pinia就非常合适了

  • 无需再创建各个模块嵌套了,Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia中每个store都是独立的,互相不影响。

  • 体积非常小,只有1KB左右。

  • pinia支持插件来扩展自身功能。

  • 支持服务端渲染


3. pinna 使用


pinna文档(opens new window)


  1. 准备工作


我们这里搭建一个最新的Vue3 + TS + Vite项目


npm create vite@latest my-vite-app --template vue-ts
复制代码


  1. pinia基础使用


yarn add pinia
复制代码


// main.tsimport { createApp } from "vue";import App from "./App.vue";import { createPinia } from "pinia";const pinia = createPinia();
const app = createApp(App);app.use(pinia);app.mount("#app");
复制代码


2.1 创建store


//sbinsrc/store/user.tsimport { defineStore } from 'pinia'
// 第一个参数是应用程序中 store 的唯一 idexport const useUsersStore = defineStore('users', { // 其它配置项})
复制代码


创建store很简单,调用 pinia中的defineStore函数即可,该函数接收两个参数:


  • name:一个字符串,必传项,该store的唯一id

  • options:一个对象,store的配置项,比如配置store内的数据,修改数据的方法等等。


我们可以定义任意数量的store,因为我们其实一个store就是一个函数,这也是pinia的好处之一,让我们的代码扁平化了,这和Vue3的实现思想是一样的


2.2 使用store


<!-- src/App.vue --><script setup lang="ts">import { useUsersStore } from "../src/store/user";const store = useUsersStore();console.log(store);</script>
复制代码


2.3 添加state


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },});
复制代码


2.4 读取state数据


<template>  <img alt="Vue logo" src="./assets/logo.png" />  <p>姓名:{{ name }}</p>  <p>年龄:{{ age }}</p>  <p>性别:{{ sex }}</p></template><script setup lang="ts">import { ref } from "vue";import { useUsersStore } from "../src/store/user";const store = useUsersStore();const name = ref<string>(store.name);const age = ref<number>(store.age);const sex = ref<string>(store.sex);</script>
复制代码


上段代码中我们直接通过store.age等方式获取到了store存储的值,但是大家有没有发现,这样比较繁琐,我们其实可以用解构的方式来获取值,使得代码更简洁一点


import { useUsersStore, storeToRefs } from "../src/store/user";const store = useUsersStore();const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的
复制代码


2.5 修改state数据


<template>  <img alt="Vue logo" src="./assets/logo.png" />  <p>姓名:{{ name }}</p>  <p>年龄:{{ age }}</p>  <p>性别:{{ sex }}</p>  <button @click="changeName">更改姓名</button></template><script setup lang="ts">import child from './child.vue';import { useUsersStore, storeToRefs } from "../src/store/user";const store = useUsersStore();const { name, age, sex } = storeToRefs(store);const changeName = () => {  store.name = "张三";  console.log(store);};</script>
复制代码


2.6 重置state


  • 有时候我们修改了state数据,想要将它还原,这个时候该怎么做呢?就比如用户填写了一部分表单,突然想重置为最初始的状态。

  • 此时,我们直接调用store$reset()方法即可,继续使用我们的例子,添加一个重置按钮


<button @click="reset">重置store</button>// 重置storeconst reset = () => {  store.$reset();};
复制代码


当我们点击重置按钮时,store中的数据会变为初始状态,页面也会更新


2.7 批量更改state数据


如果我们一次性需要修改很多条数据的话,有更加简便的方法,使用store$patch方法,修改app.vue代码,添加一个批量更改数据的方法


<button @click="patchStore">批量修改数据</button>// 批量修改数据const patchStore = () => {  store.$patch({    name: "张三",    age: 100,    sex: "女",  });};
复制代码


  • 有经验的小伙伴可能发现了,我们采用这种批量更改的方式似乎代价有一点大,假如我们state中有些字段无需更改,但是按照上段代码的写法,我们必须要将 state 中的所有字段例举出了。

  • 为了解决该问题,pinia提供的$patch方法还可以接收一个回调函数,它的用法有点像我们的数组循环回调函数了。


store.$patch((state) => {  state.items.push({ name: 'shoes', quantity: 1 })  state.hasChanged = true})
复制代码


2.8 直接替换整个state


pinia提供了方法让我们直接替换整个state对象,使用store$state方法


store.$state = { counter: 666, name: '张三' }
复制代码


上段代码会将我们提前声明的state替换为新的对象,可能这种场景用得比较少


  1. getters属性


  • gettersdefineStore参数配置项里面的另一个属性

  • 可以把getter想象成Vue中的计算属性,它的作用就是返回一个新的结果,既然它和Vue中的计算属性类似,那么它肯定也是会被缓存的,就和computed一样


3.1 添加getter


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 10,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return state.age + 100;    },  },})
复制代码


上段代码中我们在配置项参数中添加了getter属性,该属性对象中定义了一个getAddAge方法,该方法会默认接收一个state参数,也就是state对象,然后该方法返回的是一个新的数据


3.2 使用getter


<template>  <p>新年龄:{{ store.getAddAge }}</p>  <button @click="patchStore">批量修改数据</button></template><script setup lang="ts">import { useUsersStore } from "../src/store/user";const store = useUsersStore();// 批量修改数据const patchStore = () => {  store.$patch({    name: "张三",    age: 100,    sex: "女",  });};</script>
复制代码


上段代码中我们直接在标签上使用了store.gettAddAge方法,这样可以保证响应式,其实我们state中的name等属性也可以以此种方式直接在标签上使用,也可以保持响应式


3.3 getter中调用其它getter


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return state.age + 100;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },});
复制代码


3.3 getter传参


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return (num: number) => state.age + num;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },});
复制代码


<p>新年龄:{{ store.getAddAge(1100) }}</p>
复制代码


  1. actions属性


  • 前面我们提到的stategetters 属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data数据和computed计算属性一样。

  • 那么,如果我们有业务代码的话,最好就是卸载actions属性里面,该属性就和我们组件代码中的methods相似,用来放置一些处理业务逻辑的方法。

  • actions属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法


4.1 添加actions


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return (num: number) => state.age + num;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },  actions: {    // 在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前store    saveName(name: string) {      this.name = name;    },  },});
复制代码


4.2 使用actions


使用actions中的方法也非常简单,比如我们在App.vue中想要调用该方法


const saveName = () => {  store.saveName("poetries");};
复制代码


总结


pinia的知识点很少,如果你有 Vuex 基础,那么学起来更是易如反掌


pinia 无非就是以下 3 个大点:


  • state

  • getters

  • actions

vue3.2 自定义全局指令、局部指令

// 在src目录下新建一个directive文件,在此文件夹下新建一个index.js文件夹,接着输入如下内容const directives =  (app) => {  //这里是给元素取得名字,虽然是focus,但是实际引用的时候必须以v开头  app.directive('focus',{    //这里的el就是获取的元素    mounted(el) {      el.focus()      }  })}
//默认导出 directivesexport default directives
复制代码


// 在全局注册directiveimport { createApp } from 'vue'import App from './App.vue'import router from './router'import store from './store'import directives from './directives'
const app = createApp(App)directives(app)
app.use(store).use(router).mount('#app')
复制代码


<!-- 在你需要的页面进行自定义指令的使用 --><template>  <div class="container">    <div class="content">      <input type="text"  v-focus>      内容    </div>  </div></template>
<script setup>import { reactive, ref } from 'vue'// const vMove:Directive = () =>{
// }</script>
复制代码


vue3.2 setup语法糖模式下,自定义指令变得及其简单


<input type="text" v-model="value" v-focus>
<script setup>//直接写,但是必须是v开头const vFocus = { mounted(el) { // 获取input,并调用其focus()方法 el.focus() }}</script>
复制代码


<!-- demo 进去页面自动获取焦点,然后让盒子的颜色根据你input框输入的内容变色,并且作防抖处理 -->
<template> <div class="container"> <div class="content" v-move="{ background: value }"> 内容 <input type="text" v-model="value" v-focus @keyup="see"> </div> </div></template>
<script setup>import { reactive, ref } from 'vue'const value = ref('')
const vFocus = { mounted(el) { // 获取input,并调用其focus()方法 el.focus() }}
let timer = null
const vMove = (el, binding) => { if (timer !== null) { clearTimeout(timer) } timer = setTimeout(() => { el.style.background = binding.value.background console.log(el); }, 1000);}
</script>
<style lang="scss" scoped>.container { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center;
.content { border-top: 5px solid black; width: 200px; height: 200px; cursor: pointer; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; }}</style>
复制代码


参考 前端进阶面试题详细解答

MVVM 的优缺点?

优点:


  • 分离视图(View)和模型(Model),降低代码耦合,提⾼视图或者逻辑的重⽤性: ⽐如视图(View)可以独⽴于 Model 变化和修改,⼀个 ViewModel 可以绑定不同的"View"上,当 View 变化的时候 Model 不可以不变,当 Model 变化的时候 View 也可以不变。你可以把⼀些视图逻辑放在⼀个 ViewModel⾥⾯,让很多 view 重⽤这段视图逻辑

  • 提⾼可测试性: ViewModel 的存在可以帮助开发者更好地编写测试代码

  • ⾃动更新 dom: 利⽤双向绑定,数据更新后视图⾃动更新,让开发者从繁琐的⼿动 dom 中解放


缺点:


  • Bug 很难被调试: 因为使⽤双向绑定的模式,当你看到界⾯异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得⼀个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地⽅就变得不那么容易了。另外,数据绑定的声明是指令式地写在 View 的模版当中的,这些内容是没办法去打断点 debug 的

  • ⼀个⼤的模块中 model 也会很⼤,虽然使⽤⽅便了也很容易保证了数据的⼀致性,当时⻓期持有,不释放内存就造成了花费更多的内存

  • 对于⼤型的图形应⽤程序,视图状态较多,ViewModel 的构建和维护的成本都会⽐较⾼。

使用 Object.defineProperty() 来进行数据劫持有什么缺点?

在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。


在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用 Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。

diff 算法

答案


时间复杂度: 个树的完全 diff 算法是一个时间复杂度为 O(n*3) ,vue 进行优化转化成 O(n)


理解:


  • 最小量更新, key 很重要。这个可以是这个节点的唯一标识,告诉 diff 算法,在更改前后它们是同一个 DOM 节点

  • 扩展 v-for 为什么要有 key ,没有 key 会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改 DOM),加 key 只会移动减少操作 DOM。

  • 只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。

  • 只进行同层比较,不会进行跨层比较。


diff 算法的优化策略:四种命中查找,四个指针


  1. 旧前与新前(先比开头,后插入和删除节点的这种情况)

  2. 旧后与新后(比结尾,前插入或删除的情况)

  3. 旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)

  4. 旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)


--- 问完上面这些如果都能很清楚的话,基本 O 了 ---


以下的这些简单的概念,你肯定也是没有问题的啦😉

如果让你从零开始写一个 vue 路由,说说你的思路

思路分析:


首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。


  • 借助hash或者 history api实现url跳转页面不刷新

  • 同时监听hashchange事件或者popstate事件处理跳转

  • 根据hash值或者state值从routes表中匹配对应component并渲染


回答范例:


一个SPA应用的路由需要解决的问题是 页面跳转内容改变同时不刷新 ,同时路由还需要以插件形式存在,所以:


  1. 首先我会定义一个createRouter函数,返回路由器实例,实例内部做几件事


  • 保存用户传入的配置项

  • 监听hash或者popstate事件

  • 回调里根据path匹配对应路由


  1. router定义成一个Vue插件,即实现install方法,内部做两件事


  • 实现两个全局组件:router-linkrouter-view,分别实现页面跳转和内容显示

  • 定义两个全局变量:$route$router,组件内可以访问当前路由和路由器实例

slot 是什么?有什么作用?原理是什么?

slot 又名插槽,是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot 又分三类,默认插槽,具名插槽和作用域插槽。


  • 默认插槽:又名匿名查抄,当 slot 没有指定 name 属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。

  • 具名插槽:带有具体名字的插槽,也就是带有 name 属性的 slot,一个组件可以出现多个具名插槽。

  • 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。


实现原理:当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

用 VNode 来描述一个 DOM 结构

虚拟节点就是用一个对象来描述一个真实的 DOM 元素。首先将 template (真实 DOM)先转成 ast ast 树通过 codegen 生成 render 函数, render 函数里的 _c 方法将它转为虚拟 dom

v-if 和 v-show 的区别

  • 手段:v-if 是动态的向 DOM 树内添加或者删除 DOM 元素;v-show 是通过设置 DOM 元素的 display 样式属性控制显隐;

  • 编译过程:v-if 切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show 只是简单的基于 css 切换;

  • 编译条件:v-if 是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show 是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且 DOM 元素保留;

  • 性能消耗:v-if 有更高的切换消耗;v-show 有更高的初始渲染消耗;

  • 使用场景:v-if 适合运营条件不大可能改变;v-show 适合频繁切换。

Vue computed 实现

  • 建立与其他属性(如:dataStore)的联系;

  • 属性改变后,通知计算属性重新计算


实现时,主要如下


  • 初始化 data, 使用 Object.defineProperty 把这些属性全部转为 getter/setter

  • 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。

  • Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性重新计算。

  • 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他的依赖收集

Vue 项目本地开发完成后部署到服务器后报 404 是什么原因呢

如何部署

前后端分离开发模式下,前后端是独立布署的,前端只需要将最后的构建物上传至目标服务器的web容器指定的静态目录下即可


我们知道vue项目在构建后,是生成一系列的静态文件


常规布署我们只需要将这个目录上传至目标服务器即可




复制代码


web容器跑起来,以nginx为例


server {  listen  80;  server_name  www.xxx.com;
location / { index /data/dist/index.html; }}
复制代码


配置完成记得重启nginx


// 检查配置是否正确nginx -t 
// 平滑重启nginx -s reload
复制代码


操作完后就可以在浏览器输入域名进行访问了


当然上面只是提到最简单也是最直接的一种布署方式


什么自动化,镜像,容器,流水线布署,本质也是将这套逻辑抽象,隔离,用程序来代替重复性的劳动,本文不展开

404 问题

这是一个经典的问题,相信很多同学都有遇到过,那么你知道其真正的原因吗?


我们先还原一下场景:


  • vue项目在本地时运行正常,但部署到服务器中,刷新页面,出现了 404 错误


先定位一下,HTTP 404 错误意味着链接指向的资源不存在


问题在于为什么不存在?且为什么只有history模式下会出现这个问题?


为什么 history 模式下有问题


Vue是属于单页应用(single-page application)


SPA是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个index.html


现在,我们回头来看一下我们的nginx配置


server {  listen  80;  server_name  www.xxx.com;
location / { index /data/dist/index.html; }}
复制代码


可以根据 nginx 配置得出,当我们在地址栏输入 www.xxx.com 时,这时会打开我们 dist 目录下的 index.html 文件,然后我们在跳转路由进入到 www.xxx.com/login


关键在这里,当我们在 website.com/login 页执行刷新操作,nginx location 是没有相关配置的,所以就会出现 404 的情况


为什么 hash 模式下没有问题


router hash 模式我们都知道是用符号 #表示的,如 website.com/#/login, hash 的值为 #/login


它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对服务端完全没有影响,因此改变 hash 不会重新加载页面


hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 website.com/#/login 只有 website.com 会被包含在请求中 ,因此对于服务端来说,即使没有配置location,也不会返回404错误

解决方案

看到这里我相信大部分同学都能想到怎么解决问题了,


产生问题的本质是因为我们的路由是通过 JS 来执行视图切换的,


当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404


所以我们只需要配置将任意页面都重定向到 index.html,把路由交由前端处理


nginx配置文件.conf修改,添加try_files $uri $uri/ /index.html;


server {  listen  80;  server_name  www.xxx.com;
location / { index /data/dist/index.html; try_files $uri $uri/ /index.html; }}
复制代码


修改完配置文件后记得配置的更新


nginx -s reload
复制代码


这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件


为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面


const router = new VueRouter({  mode: 'history',  routes: [    { path: '*', component: NotFoundComponent }  ]})
复制代码

二、如何解决

解决跨域的方法有很多,下面列举了三种:


  • JSONP

  • CORS

  • Proxy


而在vue项目中,我们主要针对CORSProxy这两种方案进行展开


CORS


CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的 HTTP 头组成,这些 HTTP 头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应


CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源


只要后端实现了 CORS,就实现了跨域


!



koa框架举例


添加中间件,直接设置Access-Control-Allow-Origin响应头


app.use(async (ctx, next)=> {  ctx.set('Access-Control-Allow-Origin', '*');  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');  if (ctx.method == 'OPTIONS') {    ctx.body = 200;   } else {    await next();  }})
复制代码


ps: Access-Control-Allow-Origin 设置为*其实意义不大,可以说是形同虚设,实际应用中,上线前我们会将Access-Control-Allow-Origin 值设为我们目标host


Proxy


代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击


方案一


如果是通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象


通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果 web 应用和接口服务器不在一起仍会跨域


vue.config.js文件,新增以下代码


amodule.exports = {    devServer: {        host: '127.0.0.1',        port: 8084,        open: true,// vue项目启动时自动打开浏览器        proxy: {            '/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的                target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址                changeOrigin: true, //是否跨域                pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替                    '^/api': ""                 }            }        }    }}
复制代码


通过axios发送请求中,配置请求的根路径


axios.defaults.baseURL = '/api'
复制代码


方案二


此外,还可通过服务端实现代理请求转发


express框架为例


var express = require('express');const proxy = require('http-proxy-middleware')const app = express()app.use(express.static(__dirname + '/'))app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false                      }));module.exports = app
复制代码


方案三


通过配置nginx实现代理


server {    listen    80;       location / {        root  /var/www/html;        index  index.html index.htm;        try_files $uri $uri/ /index.html;    }    location /api {        proxy_pass  http://127.0.0.1:3000;        proxy_redirect   off;        proxy_set_header  Host       $host;        proxy_set_header  X-Real-IP     $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;    }}
复制代码

谈一谈对 Vue 组件化的理解

  • 组件化开发能大幅提高开发效率、测试性、复用性等

  • 常用的组件化技术:属性、自定义事件、插槽

  • 降低更新频率,只重新渲染变化的组件

  • 组件的特点:高内聚、低耦合、单向数据流

双向绑定的原理是什么

我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成


  • 数据层(Model):应用的数据及业务逻辑

  • 视图层(View):应用的展示效果,各类 UI 组件

  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来


而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理


理解 ViewModel


它的主要职责就是:


  • 数据变化后更新视图

  • 视图变化后更新数据


当然,它还有两个主要部分组成


  • 监听器(Observer):对所有数据的属性进行监听

  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

Vue.js 的 template 编译

简而言之,就是先转化成 AST 树,再得到的 render 函数返回 VNode(Vue 的虚拟 DOM 节点),详细步骤如下:


首先,通过 compile 编译器把 template 编译成 AST 语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile 是 createCompiler 的返回值,createCompiler 是用以创建编译器的。另外 compile 还负责合并 option。


然后,AST 会经过 generate(将 AST 语法树转化成 render funtion 字符串的过程)得到 render 函数,render 的返回值是 VNode,VNode 是 Vue 的虚拟 DOM 节点,里面有(标签名、子节点、文本等等)

描述下 Vue 自定义指令

在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。一般需要对 DOM 元素进行底层操作时使用,尽量只用来操作 DOM 展示,不修改内部的值。当使用自定义指令直接修改 value 值时绑定 v-model 的值也不会同步更新;如必须修改可以在自定义指令中使用 keydown 事件,在 vue 组件中使用 change 事件,回调中修改 vue 数据;


(1)自定义指令基本内容


  • 全局定义:Vue.directive("focus",{})

  • 局部定义:directives:{focus:{}}

  • 钩子函数:指令定义对象提供钩子函数

  • o bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  • o inSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。

  • o update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。

  • o ComponentUpdate:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • o unbind:只调用一次,指令与元素解绑时调用。

  • 钩子函数参数 o el:绑定元素

  • o bing: 指令核心对象,描述指令全部信息属性

  • o name

  • o value

  • o oldValue

  • o expression

  • o arg

  • o modifers

  • o vnode 虚拟节点

  • o oldVnode:上一个虚拟节点(更新钩子函数中才有用)


(2)使用场景


  • 普通 DOM 元素进行底层操作的时候,可以使用自定义指令

  • 自定义指令是用来操作 DOM 的。尽管 Vue 推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的 DOM 操作,并且是可复用的。


(3)使用案例


初级应用:


  • 鼠标聚焦

  • 下拉菜单

  • 相对时间转换

  • 滚动动画


高级应用:


  • 自定义指令实现图片懒加载

  • 自定义指令集成第三方插件


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
美团前端一面高频vue面试题整理_Vue_bb_xiaxia1998_InfoQ写作社区