24届前端秋招面试题知识点总结

305 阅读14分钟

1、防抖和节流

接口防抖和节流是网站性能优化方案的重要组成部分之一。通过防抖和节流可以有效减少网络请求和服务端负载。通过编写防抖和节流代码的方式,可以确保前端页面不会出现过多的请求数量,从而使网页加载速度更快、响应更灵敏。

防抖

“防抖”指的是在一定时间内只能触发一次事件,通常是输入框、滚动事件、提交事件。假如没有防抖控制,页面会出现一些异常情况,例如:搜索框输入过快、滚动条跳动频繁等。按钮点击触发接口,接口响应慢,用户多点了几次,可能查询接口还没什么问题,如果业务复杂的地方,部分按钮的操作涉及到一些数据计算和后端多次交互更新数据的情况,就会出现错误。

解决方案

1、添加防抖函数 按钮点击添加防抖函数,设置合理的时间。

function debounce(func, wait) {
  let timeout;
  return function () {
    if(timeout) clearTimeout(timeout);
    timeout = setTimeout(func, wait)
  }
}

注意点: 业务复杂情形,设置时间短了,接口请求慢,用户多次点击还会出现问题,时间过长,用户体验感差。

2、设置按钮禁用

设置按钮的 disabled 相关属性,按钮点击后设置禁用效果,业务代码执行结束后取消禁用。this.disable = true this.diaable = false 原生按钮和使用UI的按钮设置简单,对于div、icon、svg等自定义按钮处理比较麻烦。

3、请求拦截器中添加loading

在请求拦截器中根据请求类型显示 loading,请求结束后隐藏。

4、添加loading组件

新增一个 loading 组件,绑定到全局变量中,按钮点击触发显示 loading,业务执行结束后隐藏。

防抖的实现原理非常简单,就是通过对要执行的函数进行延迟处理,以此来控制函数执行的次数。具体流程如下:

  1. 定义一个变量用于保存计时器。
  2. 在函数执行前判断计时器是否存在,如果存在则清除计时器。
  3. 为函数设置时间延迟,将返回结果保存到计时器变量中。
  4. 等待时间超过设定的阈值后,执行相应的回调函数。
// 防抖
function debounce(fn, delay = 500) {
  let timer = null;
  return function(...args) {
    if (timer !== null) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  };
}

节流

“节流”通常指的是在一定时间内只能执行一次事件,例如:下拉加载更多、页面滚动等。

实现原理 通过设置一个固定时间间隔,在这个时间间隔内只能执行一次相应的回调函数。具体流程如下:

  1. 定义一个变量用于保存上一次执行函数的时间。
  2. 在执行函数前获取当前的时间戳。
  3. 判断当前时间与上一次执行时间是否大于设定的间隔时间,如果大于,则执行相应的回调函数,并更新上一次执行时间。
  4. 如果小于设定的间隔时间,则等待下一次执行。
// 节流
function throttle(fn, delay = 500) {
  let last = 0;
  return function(...args) {
    let now = new Date().getTime();
    if (now - last > delay) {
      last = now;
      fn.apply(this, args);
    }
  };
}

一个完整的示例代码,包括了防抖和节流。

import axios from 'axios'

function request(config) {
  const instance = axios.create({
    baseURL: 'http://localhost:3000/api',
    timeout: 10000
  })

// 防抖
const debounceTokenCancel = new Map()
instance.interceptors.request.use(config => {
  const tokenKey = `${config.method}-${config.url}`
  const cancel = debounceTokenCancel.get(tokenKey)
  if (cancel) {
    cancel()
  }
  return new Promise(resolve => {
    const timer = setTimeout(() => {
      clearTimeout(timer)
      resolve(config)
    }, 800)
    debounceTokenCancel.set(tokenKey, () => {
      clearTimeout(timer)
      resolve(new Error('取消请求'))
    })
  })
}, error => {
  console.log(error)
  return Promise.reject(error)
})

instance.interceptors.response.use(response => {
  return response
}, error => {
  console.log(error)
  return Promise.reject(error)
})

// 节流
let lastTime = new Date().getTime()
instance.interceptors.request.use(config => {
  const nowTime = new Date().getTime()
  if (nowTime - lastTime < 1000) {
    return Promise.reject(new Error('节流处理中,稍后再试'))
  }
  lastTime = nowTime
  return config
}, error => {
  console.log(error)
  return Promise.reject(error)
})

return instance(config)
}

export default request

2、promise

① Promise 是一个构造函数

创建 Promise 的实例 const p = new Promise(),new 出来的 Promise 实例对象,代表一个异步操作

② Promise.prototype 上包含一个 .then() 方法

可以通过原型链的方式访问到 .then() 方法,例如 p.then()

③ .then() 方法用来预先指定成功和失败的回调函数

p.then(成功的回调函数,失败的回调函数)

p.then(result => { }, error => { })

调用 .then() 方法时,成功的回调函数是必选的、失败的回调函数是可选的.

如果上一个 .then() 方法中返回了一个新的 Promise 实例对象,则可以通过下一个 .then() 继续进行处理。Promise 支持链式调用,通过 .then() 方法的链式调用,就解决了回调地狱的问题。

在 Promise 的链式操作中如果发生了错误,可以使用 Promise.prototype.catch 方法进行捕获和处理:

Promise.all() 方法

Promise.all() 方法会发起并行的 Promise 异步操作,等所有的异步操作全部结束后才会执行下一步的 .then 操作(等待机制)。

Promise.race() 方法

Promise.race() 方法会发起并行的 Promise 异步操作,只要任何一个异步操作完成,就立即执行下一步的.then 操作(赛跑机制)。

async/await 的使用注意事项

① 如果在 function 中使用了 await,则 function 必须被 async 修饰

② 在 async 方法中,第一个 await 之前的代码会同步执行,await 之后的代码会异步执行

同步任务和异步任务

为了防止某个耗时任务导致程序假死的问题,JavaScript 把待执行的任务分为了两类:

① 同步任务(synchronous)

⚫ 又叫做非耗时任务,指的是在主线程上排队执行的那些任务

⚫ 只有前一个任务执行完毕,才能执行后一个任务

② 异步任务(asynchronous)

⚫ 又叫做耗时任务,异步任务由 JavaScript 委托给宿主环境进行执行

⚫ 当异步任务执行完成后,会通知 JavaScript 主线程执行异步任务的回调函数

image.png JavaScript 主线程从“任务队列”中读取异步任务的回调函数,放到执行栈中依次执行。这 个过程是循环不断的,所以整个的这种运行机制又称为 EventLoop(事件循环)。

JavaScript 把异步任务又做了进一步的划分,异步任务又分为两类,分别是:

① 宏任务(macrotask)

异步 Ajax 请求、setTimeout、setInterval、文件操作等

② 微任务(microtask)

Promise.then、.catch 和 .finally 、process.nextTick等

image.png

3、v-if 与 v-show 的区别

实现原理不同:

⚫ v-if 指令会动态地创建或移除 DOM 元素,从而控制元素在页面上的显示与隐藏;

⚫ v-show 指令会动态为元素添加或移除 style="display: none;" 样式,从而控制元素的显示与隐藏;

性能消耗不同:

v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。

⚫ 如果需要非常频繁地切换,则使用 v-show 较好

⚫ 如果在运行时条件很少改变,则使用 v-if 较好

  • 区别点:

1、手段

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

2、编译过程

v-if 切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件

v-show 只是简单的基于 css 切换

3、编译条件

v-if 是惰性的,如果初始条件为假,则什么也不做。只有在条件第一次变为真时才开始局部编译

v-show 是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且 DOM 元素保留

4、性能消耗

v-if 有更高的切换消耗

v-show 有更高的初始渲染消耗

5、使用场景

v-if 适合运营条件不大可能改变 v-show 适合频繁切换

4、computed 计算属性 vs methods 方法

computed 计算属性:

作用:封装了一段对于数据的处理,求得一个结果。

语法:

① 写在 computed 配置项中

② 作为属性,直接使用 → this.计算属性 {{ 计算属性 }}

methods 方法:

作用:给实例提供一个方法,调用以处理业务逻辑。

语法:

① 写在 methods 配置项中

② 作为方法,需要调用 → this.方法名( ) {{ 方法名() }} @事件名="方法名"

5、watch监听器

watch侦听器的语法有几种?

① 简单写法 → 监视简单类型的变化

watch: {
数据属性名 (newValue, oldValue) {
一些业务逻辑 或 异步操作。
},
'对象.属性名' (newValue, oldValue) {
一些业务逻辑 或 异步操作。
}
}

② 完整写法 → 添加额外的配置项 (深度监视复杂类型,立刻执行)

watch: {// watch 完整写法
数据属性名: {
deep: true, // 深度监视(针对复杂类型)
immediate: true, // 是否立刻执行一次handler
handler (newValue) {
console.log(newValue)
}}}

6、Vue生命周期和生命周期的四个阶段

Vue生命周期:一个Vue实例从 创建 到 销毁 的整个过程。

生命周期四个阶段:① 创建 ② 挂载 ③ 更新 ④ 销毁

Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。

1、beforeCreate(创建前) :数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。

2、created(创建后) :实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性。

3、beforeMount(挂载前) :在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面上。

4、mounted(挂载后) :在el被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html 页面中。此过程中进行ajax交互。

5、beforeUpdate(更新前) :响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。

6、updated(更新后):在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。

7、beforeDestroy(销毁前) :实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例。

8、destroyed(销毁后) :实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。

另外还有 keep-alive 独有的生命周期,分别为 activateddeactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数。

7、Vue组件样式冲突

style中的样式 默认是作用到全局的,加上scoped可以让样式变成局部样式 组件都应该有独立的样式,推荐加scoped(原理)

scoped原理:

1.给当前组件模板的所有元素,都会添加上一个自定义属性 data-v-hash值 data-v-5f6a9d56 用于区分开不通的组件

2.css选择器后面,被自动处理,添加上了属性选择器 div[data-v-5f6a9d56]

8、Vue组件通信

父传子 组件通信语法 / 父传子 / 子传父 / 非父子 (扩展)

父子关系:props和$emit

1. 父传子:父组件通过 props 将数据传递给子组件

给子组件以添加属性的方式传值,子组件内部通过props接收,模版中直接使用。

image.png

2. 子传父:子组件利用 $emit 通知父组件修改更新

$emit('changeTitle','子向父传值')触发时间,给父组件发送消息,父组件监听事件,提供处理函数,形参中获取参数

image.png

非父子关系:provide&inject 或 eventbus

Eventbus

image.png

provide & inject 作用:跨层级共享数据。

image.png

通用解决方案:Vuex(适用复杂业务场景)

引进 Vuex 来进行状态管理,负责组件中的通信,高效地实现组件之间的数据共享,提高开发效率,方便维护代码。

/* src/store/index.js */
 Vue import Vue from 'vue'
 import Vuex from 'vuex' 
// 把 Vuex 注册到Vue 上 
Vue.use(Vuex) 
export default new Vuex.Store(
{ // 在开发环境开启严格模式 这样修改数据 就必须通过 mutation 来处理 
strict:products.env.NODE_ENV !== 'production', 
// 状态
state: {
    name:'xiaowu',
    age:18
}, 
// 用来处理状态
mutations: { 
    changeName(state, newName) {
    state.name = newName
    },
},
// 用于异步处理 
actions: {
    /** context 上下文默认传递的参数 * @param {*} newName 自己传递的参数 */// 定义一个异步的方法 context是 store 
    changeNameAsync(context, newName) {
    // 这里用 setTimeout 模拟异步
        setTimeout(() => { // 在这里调用 mutations 中的处理方法
        context.commit('changeName', newName) }, 2000) },
    }, 
getters: { 
// 在这里对 状态 进行包装 
// 第一个参数默认就是 state ,名字随便取
decorationName(state) { 
    return `大家好我的名字叫${state.name}今年${state.age}岁`
    },
},
// 用来挂载模块 
modules: { } 
})

state 

获取到 state 有两种方式

1.直接使用 this.$store.state[属性] ,(this 可以省略)

this.$store.state.name

2.使用 mapState

通过 mapState把 store 映射到 组件的计算属性,就相当于组件内部有了 state 里的属性

computed: {
    // name2 和 age2 都是别名
    ...mapState({ name2: 'name', age2: 'age'}])
}

Mutation

两种方法在组件触发 mutations 中的方法

1.this.$store.commit() 触发

2.使用 mapMutations

<script> 
// 从 Vuex 中导入 mapState 
import { mapState } from 'vuex' 
export default { 
name: 'App', 
computed: {
    // 将 store 映射到当前组件的计算属性
    ...mapState(['name', 'age']) },
methods: { 
    handleClick() { 
    // 触发 mutations 中的 changeName 
    this.$store.commit('changeName', '小浪')
    // 将 mutations 中的 changeName 方法映射到 methods 中
    ...mapMutations(['changeName'])
} 
},
} 
</script>

Action

ActionMutation 区别

Action 同样也是用来处理任务,不过它处理的是异步任务,异步任务必须要使用 Action,通过 Action 触发 Mutation 间接改变状态,不能直接使用 Mutation 直接对异步任务进行修改

先在 Action 中定义一个异步方法来调用 Mutation 中的方法

methods: {
        changeName2(newName) { // 使用 dispatch 来调用 actions 中的方法 
            this.$store.dispatch('changeNameAsync', newName)
        }
        // 映射 actions 中的指定方法 
        ...mapActions(['changeNameAsync'])
        
 },

Getter

Getter类似计算属性,对state做一些包装简单展示到视图中。

this.$store.getters[名称]

{{ this.$store.getters.decorationName }}

使用 mapGetters

<template>
<div id="app"> {{ decorationName }} </div> 
</template> <script> 
// 从 Vuex 中导入 mapGetters 
import { mapGetters } from 'vuex' 
export default { 
name: 'App', 
computed: {
// 将 getter 映射到当前组件的计算属性 
...mapGetters(['decorationName']) 
},
} 
</script>

module

新建一个.js文件,每个模块允许有属于自己的state,getter,action,mutation,使用

export default{
// 开启命名空间 方便之后使用 mapXXX 
namespaced: true,
state,
mutations
}

Vue.use(Vuex) export default new Vuex.Store({ modules: { animal, }, }) 使用this.$store.state[在module中挂载的模块名][挂载的模块里的属性]

9、v-model语法糖

在Vue.js中使用v-model时,实际上是将一个组件的props属性和事件绑定到了一个数据属性上。这样,当数据属性发生变化时,组件会自动更新显示的值;当用户在组件上输入内容时,数据属性也会随之更新。例如应用在输入框上,就是 value属性 和 input事件 的合写。

<template>
<div id="app" >
<input v-model="msg" type="text">
<input :value="msg" @input="msg = $event.target.value" type="text">
</div>
</template>

10、.sync修饰符 ref 和 $refs

作用:可以实现 子组件 与 父组件数据 的 双向绑定,简化代码

特点:prop属性名,可以自定义,非固定为 value

场景:封装弹框类的基础组件, visible属性 true显示 false隐藏

本质:就是 :属性名 和 @update:属性名 合写

image.png 作用:利用 ref 和 $refs 可以用于 获取 dom 元素, 或 组件实例

特点:查找范围 → 当前组件内 (更精确稳定)

① 获取 dom:② 获取组件:

  1. 目标标签 – 添加 ref 属性 <div ref="chartRef">我是渲染图表的容器</div>

  2. 目标组件 – 添加 ref 属性<BaseForm ref="baseForm"></BaseForm>

  3. 通过 this.$refs.xxx, 获取目标标签, this.$refs.xxx调用组件对象里面的方法

mounted () { console.log(this.$refs.chartRef) }, this.$refs.baseForm.组件方法()

11、Vue异步更新、$nextTick

$nextTick:等 DOM 更新后, 才会触发执行此方法里的函数体

语法: this.$nextTick(()=>{ })

12、v-loading指令封装

  1. 通过指令相关语法,封装了指令 v-loading 实现了请求的loading效果

  2. 核心思路:

(1) 准备类名 loading,通过伪元素提供遮罩层

(2) 添加或移除类名,实现loading蒙层的添加移除

(3) 利用指令语法,封装 v-loading 通用指令

inserted 钩子中,binding.value 判断指令的值,设置默认状态

update 钩子中,binding.value 判断指令的值,更新类名状态

directives: {
    loading: {
      inserted (el, binding) {
        binding.value ? el.classList.add('loading') : el.classList.remove('loading')
      },
      update (el, binding) {
        binding.value ? el.classList.add('loading') : el.classList.remove('loading')
      }
    }
  }

13、声明式导航-路由跳转 查询参数、动态路由传参

两种传参方式的区别

  1. 查询参数传参 (比较适合传多个参数)

① 跳转:to="/path?参数名=值&参数名2=值"

② 获取:$route.query.参数名

  1. 动态路由传参 (优雅简洁,传单个参数比较方便)

① 配置动态路由:path: "/path/:参数名"

② 跳转:to="/path/参数值"

③ 获取:route.params.参数名要对同一个组件中参数的变化做出响应的话,你可以简单地watch route.params.参数名 要对同一个组件中参数的变化做出响应的话,你可以简单地 watch `route 对象上的任意属性,或者,使用 beforeRouteUpdate` ,它也可以取消导航.

const User = {
  template: '...',
  created() {
    this.$watch(
      () => this.$route.params,
      (toParams, previousParams) => {
        // 对路由变化做出响应...
      }
    )
  },
  async beforeRouteUpdate(to, from) { // 对路由变化做出响应
  this.userData = await fetchUser(to.params.id) },
}

14、组件缓存keep-alive

keep-alive 是一个抽象组件:它自身不会渲染成一个 DOM 元素,也不会出现在父组件链中。 在组件切换过程中 把切换出去的组件保留在内存中,防止重复渲染DOM, 减少加载时间及性能消耗,缓存了所有被切换的组件,提高用户体验性。

keep-alive的三个属性 ① include : 组件名数组,只有匹配的组件会被缓存 ② exclude : 组件名数组,任何匹配的组件都不会被缓存 ③ max : 最多可以缓存多少组件实例

keep-alive的使用会触发两个生命周期函数

activated 当组件被激活(使用)的时候触发 → 进入这个页面的时候触发

deactivated 当组件不被使用的时候触发 → 离开这个页面的时候触发

组件缓存后就不会执行组件的created, mounted, destroyed 等钩子了

15、 Vue3.0中的响应式原理

vue2.x的响应式

  • 实现原理:

    • 对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)。

    • 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。

       Object.defineProperty(data, 'count', {
           get () {}, 
           set () {}
       })
      
  • 存在问题:

    • 新增属性、删除属性, 界面不会更新。
    • 直接通过下标修改数组, 界面不会自动更新。

Vue3.0的响应式

  • 实现原理:

    • 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
    • 通过Reflect(反射): 对源对象的属性进行操作
new Proxy(data, {
	// 拦截读取属性值
    get (target, prop) {
    	return Reflect.get(target, prop)
    },
    // 拦截设置属性值或添加新属性
    set (target, prop, value) {
    	return Reflect.set(target, prop, value)
    },
    // 拦截删除属性
    deleteProperty (target, prop) {
    	return Reflect.deleteProperty(target, prop)
    }
})

proxy.name = 'tom' 

16、 reactive对比ref

  • 从定义数据角度对比:

    • ref用来定义:基本类型数据
    • reactive用来定义:对象(或数组)类型数据
    • 备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过reactive转为代理对象
  • 从原理角度对比:

    • ref通过Object.defineProperty()getset来实现响应式(数据劫持)。
    • reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
  • 从使用角度对比:

    • ref定义的数据:操作数据需要.value,读取数据时模板中直接读取不需要.value
    • reactive定义的数据:操作数据与读取数据:均不需要.value

18、computed和watch

  • 监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)。

  • 监视reactive定义的响应式数据中某个属性时:deep配置有效。

  • watch:既要指明监视的属性,也要指明监视的回调。

  • watchEffect:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。

  • watchEffect有点像computed:

    • 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
    • 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
     //watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
     watchEffect(()=>{
         const x1 = sum.value
         const x2 = person.age
         console.log('watchEffect配置的回调执行了')
     })
    

19.生命周期