7月面试总结

58 阅读30分钟

vue2 升级到 vue3 的过程中遇到的问题

1. 升级教程

1.1 升级 Vue-cli

你的项目如果是使用 Vue-cli 创建的,则需要升级到最新版本的 Vue-cli(3.0.0 及以上),以便支持 vue3

npm install -g @vue/cli
yarn global add @vue/cli

1.2 安装 vue3

使用 npm 或者 yarn 安装最新版本的 Vue3

npm install vue@next
yarn add vue@next

1.3 更新 Vue 组件

在 vue3 中,一些属性和选项被删除被重命名或者被删除需要更新组件代码

  • v-bind=“listeners”替换为vbind=listeners”在Vue2.x中,可以使用vbind=listeners” 替换为 v-bind=“listeners” 在 Vue 2.x 中,可以使用 v-bind=“listeners” 将所有父组件传递的事件监听器绑定到子组件上,但是在 Vue 3 中,这种语法被弃用了,需要改为 v-bind=“listeners”。
  • v-bind=“attrs”替换为vbind=attrs”在Vue2.x中,可以使用vbind=attrs” 替换为 v-bind=“attrs” 在 Vue 2.x 中,可以使用 v-bind=“attrs” 将所有非 prop 的父组件属性绑定到子组件上,但是在 Vue 3 中,需要改为 v-bind=“attrs”。
  • v-on:click.native 替换为 @click.native 在 Vue 2.x 中,可以使用 v-on:click.native 绑定原生 DOM 事件,但是在 Vue 3 中,需要改为 @click.native。
  • v-on:keyup.enter 替换为 @keyup.enter 在 Vue 2.x 中,可以使用 v-on:keyup.enter 绑定键盘事件,但是在 Vue 3 中,需要改为 @keyup.enter。
  • v-on:keyup.13 替换为 @keyup.13 在 Vue 2.x 中,可以使用 v-on:keyup.13 绑定键盘事件,但是在 Vue 3 中,不再支持这种语法,需要改为 @keyup.13。
  • v-on:keyup 替换为 @keyup 在 Vue 2.x 中,可以使用 v-on:keyup 绑定键盘事件,但是在 Vue 3 中,需要改为 @keyup。

1.4 迁移全局 API

Vue 3 中全局 API 的使用方式也有所改变。例如:

  • Vue.filter 替换为 app.config.globalProperties.filterVue2.x中,可以使用Vue.filter注册全局过滤器,但是在Vue3中,需要使用app.config.globalProperties.filter 在 Vue 2.x 中,可以使用 Vue.filter 注册全局过滤器,但是在 Vue 3 中,需要使用 app.config.globalProperties.filter。
  • Vue.directive 替换为 app.directive 在 Vue 2.x 中,可以使用 Vue.directive 注册全局指令,但是在 Vue 3 中,需要使用 app.directive。
  • Vue.mixin 替换为 app.mixin 在 Vue 2.x 中,可以使用 Vue.mixin 全局混入选项,但是在 Vue 3 中,需要使用 app.mixin。

1.5 迁移路由和状态管理器

如果你的项目中使用了 Vue Router 和 Vuex 等状态管理器,需要将其升级到最新版本,以适配 Vue 3。

  • Vue Router: Vue Router 4.x 支持 Vue 3,需要将 Vue Router 和 Vue 升级到最新版本,然后更新路由的 API 和语法,例如: router-link 替换为 RouterLink v-bind=“r o u t e " 替换为 : t o = " route" 替换为 :to="route"替换为:to="route” $router.push 替换为 router.push
  • Vuex: Vuex 4.x 支持 Vue 3,需要将 Vuex 和 Vue 升级到最新版本,然后更新状态管理器的 API 和语法,例如: store.subscribe 替换为 store.watch mapState 替换为 useStore/mapState

1.6 迁移 TypeScript

如果你的项目使用 TypeScript,需要更新 TypeScript 版本并进行相应的配置和迁移。

  • TypeScript 版本:

Vue 3 需要 TypeScript 3.9 及以上版本。

  • 配置文件: 需要更新 TypeScript 配置文件(tsconfig.json)中的编译选项,例如:
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "jsx": "preserve",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "types": [
      "webpack-env",
      "@types/node",
      "@vue/cli-plugin-babel/types",
      "@vue/cli-plugin-eslint/types",
      "@vue/cli-service"
    ]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
  • 类型定义文件: Vue 3 中的类型定义文件(.d.ts)有所改变,需要将其更新为最新的版本。

1.7 迁移测试代码

如果你的项目中有测试代码,需要更新测试框架和测试代码,以适配 Vue 3。

  • 测试框架: 更新测试框架到最新版本,例如 Jest 27.x、Mocha 9.x 等。
  • 测试代码: 更新测试代码,例如:
 - import { mount } from '@vue/test-utils' 替换为 import { mount } from 'vue-test-utils'
 - Vue.extend 替换为 defineComponent
 - wrapper.vm.$emit 替换为 wrapper.trigger

将 Vue 2.x 项目升级到 Vue 3 需要更新组件代码、全局 API、路由和状态管理器、TypeScript 配置和测试代码等。如果你对迁移过程不确定,可以先在一个新的项目中尝试升级,以便在实际项目中更好地适配 Vue 3。

2 迁移总结

vue2.x 升级到 vue3 做好充分的准备和团队,以及领导沟通清楚,步骤以及面临的风险,以及应急准备,以及知识储备和人员储备。就比如搞了一半发现领导根本对 vue 升级的急迫性没那么强烈,中途给你把资源撤了,半拉子烂摊子毁在你的手里,瞬间挫败感笼上心头。记住资源是团队作战中很重要的事情,提前给团队和领导信心和目标。在升级之前,需要做好充分的准备和规划。有的时候干成一件事,也许技术上根本不是障碍。

下面只是一些技术上的总结和技术事项,都好解决。

2.1. 语法变化

Vue 3 中的语法与 Vue 2.x 有一些重大变化,例如使用 createApp 替代了 new Vue,使用 setup 替代了 data 和 methods 等等。因此,需要花费一些时间来学习新语法和调整现有代码。

2.2. 插件和库的兼容性

一些 Vue 2.x 插件和库可能不兼容 Vue 3,需要更新或替换。在升级之前,需要检查你的依赖项是否与 Vue 3 兼容,并相应地做出调整。

2.3. TypeScript 支持

Vue 3 对 TypeScript 的支持更加完善,因此如果你的项目中使用了 TypeScript,升级到 Vue 3 可能需要一些额外的注意事项,例如更改类型定义和配置文件等等。

2.4. 升级顺序

如果你的项目依赖于其他库或框架,例如 Vuex、Vue Router 等等,需要在升级 Vue 3 之前先升级这些库或框架,以确保整个项目能够顺利升级。

2.5. 测试和调试

在升级之后,需要进行一些测试和调试来确保项目的稳定性和正确性。这可能需要一些额外的时间和精力。

2. VueX 详解

vuex 中一共有五个状态 State Getter Mutation Action Module 下面进行详细讲解

2.1 state

提供唯一的公共数据源,所有共享的数据统一放到 store 的 state 进行储存,相似与 data

// 在vuex中state中定义数据,可以在任何组件中进行调用
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  //数据,相当于data
  state: {
    name: "张三",
    age: 12,
    count: 0,
  },
});

调用:

//方法一:
//在标签中直接使用
<p>{{$store.state.name}}</p>
<p>{{$store.state.age}}</p>

// 方法二:
this.$store.state.全局数据名称

注意:当前组件需要的全局数据,映射为当前组件 computed 属性

computed: {
    ...mapState(["name","age",'sex'])
}

2.2 Mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

在 vuex 中定义: 其中参数 state 参数是必须的,也可以自己传递一个参数,如下代码,进行计数器的加减操作,加法操作时可以根据所传递参数大小进行相加,减法操作没有传参每次减一

state:{
    name:'张三'age:12,
    count:0
}
getters:{

}
// 里面定义方法,操作state方法
mutations:{
    addcount(state,num){
        state.count =+ state.count+num
    }
    reduce(state){
        state.count++
    }
}

在组件中使用:

注意:使用 commit 触发 Mutation 操作

// 方法一
methods:{
//加法
btn(){
this.$store.commit("addcount",10)     //每次加十
}
//减法
btn1(){
this.$store.commit("reduce")
}
}

//方法二 使用复制函数进行操作
methods:{
    ...mapMutations(["addcount","reduce"])
    btn(){
        this.addcount(10)
    }
    btn1(){
        this.reduce()
    }
}

2.3 Action---进行异步操作

Action 和 Mutation 相似,一般不用 Mutation 异步操作,若要进行异步操作,使用 Action

原因:为了方便 devtools 打个快照存下来,方便管理维护。所以说这个只是规范,而不是逻辑的不允许,只是为了让这个工具能够追踪数据变化而已

在 vuex 中定义:

将上面的减法操作改为异步操作

// 操作一部操作mutation
actions:{
    asyncAdd(context){
        // 异步
        setTimeout(() => {
            context.commit('reduce')
        },100)
    }
}

在组件中使用

// 方法一  ---直接使用dispatch触发Action函数
this.$store.dispatch('asynAdd')

// 方法二  --- 使用辅助函数
...mapActions["asyncAdd"]
btn2(){
    this.asyncAdd()
}

2.4 Getter

类似于 vue 中的 computed,进行缓存,对于 Store 中的数据进行加工处理形成新的数据

2.5 Modules

当遇见大型项目时,数据量大,store 就会显得很臃肿

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

modules:{
    cityModules:{
        namespaced:true,
        state:{
            cityName:'中国',
        },
        mutations:{
            cityFuntion(state){
                state.cityName = '美国'
            }
        }
    },
    userInfo:{
        state:{
            userName:'张三'
        }
    }

}

默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 action 或 mutation 作出响应。

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

btn3(){
    console.log(this.$store.state.cityModules.cityName)
}

3. 路由钩子(路由守卫)函数

3.1 什么是路由守卫

路由守卫也叫路由钩子函数, 它是在路由跳转过程中调用的函数, 通过路由守卫可获取路由信息和阻止路由跳转

3.2 有哪些路由守卫

路由守卫有以下三种(6 个): 组件内的路由守卫: beforeRouteEnter() beforeRouterUpdate() beforeRouteLeave() 全局路由守卫: beforeEach() afterEach() 单个路由守卫: beforeEnter()

3.3 路由守卫有哪些参数, 作用是什么?

路由守卫的参数有三个: to, from , next to: 表示路由跳转的目标信息对象 (到哪儿去) from: 表示路由跳转的来源信息对象(从哪儿来) next: 回调函数, 表示是否允许路由跳转, 调用允许, 不调用则禁止

3.4 什么时候用路由守卫?

  • beforeRouteEnter, beforeEnter, beforeEach, 这三个都是路由进入之前的守卫, 这些路由守卫常用于做登录认证, 在这里判断用户是否已经登录, 如果已经登录,调用 next()允许进入, 如果没有登录, 不调用 next(),禁止进入 主要针对一些需要登录状态才能访问的页面, 如: 个人信息页
  • beforeRouteUpdate 是路由更新时调用的钩子函数, 当路由路径来源和目标相同(当前页跳转当前页),并且参数不同时, 会调用这个钩子, 常用于搜索页, 当搜索关键字改变时, 在这里更新数据

3.5 使用路由守卫需要注意哪些问题?

  • 由于在任何组件中都能使用 this.$router, 所以全局路由守卫可以在任意组件中调用
  • beforeRouteEnter 路由进入之前,组件还未创建,打印 this 是 undefined, 如果想用组件对象,可以在 next()的回调中拿到组件对象 next( vm=>{ console.log(vm) })
  • this.$router 是/src/router/index.js 中创建的路由对象, 全局唯一, 用于编程式导航跳转和全局路由守卫
  • this.route是路由跳转的信息对象,每一个路由对应一个route 是路由跳转的信息对象, 每一个路由对应一个route 对象, 用于路由传值和路由监听
  • 路由监听和路由守卫的区别:
    1. 路由监听: 在组件中 watch 字段中监听$route, 实现路由监听, 只能监听路由变化, 不能修改路由和限制路由跳转
    2. 路由守卫: 使用路由钩子函数实现守卫, 路由不止可以监听路由, 还能控制路由跳转
  • 当路由跳转时, 跳转的路径和参数与当前完全一致, 没有变化, 不会调用路由钩子函数, 会报错 NavigationDuplicated: Avoided redundant navigation to current location: “/about?page=5”.

3.6 详解

3.6.1 组件内-beforeRouterEnter/beforeRouteUpdate/beforeRouteLeave

在组件内调用

// beforeRouterEnter-路由守卫:判断路由能否跳转,可控制路由跳转(多做登录认证)
beforeRouteEnter (to, from, next) {
  // 路由守卫有三个参数,to表示即将跳转的路由信息对象(到哪去,跳到当前页的路由对象),from是路跳转的来源信息对象(从哪来,从哪里调到此页面的路由对象),next函数,调用时允许路由进入
  console.log("路由进入之前", to, from, next);
  // 此时,to的路径必定是/about,如果from的路由路径也是about,会报错,此时需要禁止路由跳转,止报错
  // next(); // 是否允许被跳转
  // 使用场景:这个路由守卫,常用于做登录认证,在这里判断用户是否已经登录,如果已经登录,调next()允许进入,如果没有登录,不调用next(),禁止进入。主要针对些需要登录状态才能访问的页面如:个人信息页
  console.log(this);
  // 路由进入之前,组件还未创建,打印this是undefined,this是不能调的,那么如何在这个路由守卫获取组件对象this?
  next((vm)=>{
    // next()的参数是一个回调函数,当组件创建完成后调用,回调的函数就是组件对象this
    console.log(vm);
  })
},

// beforeRouteUpdate-路由守卫:当路由路径来源和目标相同时(当前页跳转当前页),参数不同时,执行这个路由守卫,不会报错(如果参数相同,会直接报错,不会进入路由守卫)
beforeRouteUpdate (to, from, next){
  console.log("路由更新之前", to, from, this.$route);
  // 此时这里的this.$route和from里的page一直是不变的是旧的信息,to对象里的数据才是最新值,所用to.query.page拿到最新的数据
  this.$axios.get("/douyu/api/room/list", {
    params: { page: to.query.page }
  }).then(res => {
    console.log(res.data.data.list);
    this.list = res.data.data.list
  })
  next();
  // 使用场景:这个路由守卫,常用于写搜索页,当搜索关键词更新时,组件并没有重置也没有显示隐藏所以普通的生命周期钩子并不会触发,只能在这里更新数据
},

// beforeRouteLeave-路由守卫:路由离开前,从当前页跳转到其它页之前调用
beforeRouteLeave (to, from, next) {
  console.log("路由离开之前", to.path, from.path, next);
  next()
  // 使用场景,比如我们网站中遇到的关不掉的广告页面
  // 比如我们在这个函数中能够不掉用next函数,当前页面就不能跳转到其它页面了
}

3.6.2 全局路由守卫 beforEach/afterEach

由于在任何组件中都能使用 this.$router,所以全局路由守卫可以在任意组件中调用(可以放在任意组件中,放 main.js 中也可以)

// beforEach-如果想限制所有路由的跳转,可以使用router对象调用全局路由守卫
router.beforeEach(function (to, from, next) {
  // 任何一个路由跳转都会经过这个路由守卫
  console.log("全局路由进入之前", to.fullPath, from.fullPath);
  next();
});

// afterEach-路由跳转之后的守卫,next不调用也无所谓
router.afterEach(function (to, from, next) {
  // 任何一个路由跳转都会经过这个路由守卫
  console.log("全局路由进入之后", to.fullPath, from.fullPath);
});

3.6.3 单个路由守卫 beforeEnter

定义在路由文件 index.js 的路由对象中

{
    path: '/',
    name: 'Home',
    component: Home,
    // 路由配置对象中可添加路由守卫函数,只监听单个路由
    beforeEach: (to, from, next) => {
      console.log("单个路由守卫进入之前", to.fullPath, from.fullPath);
      next()
    }
}

4. 算法

4.1 两组数组对象进行查询相同项并返回

// 方法一
let newArr = list.filter((v) => list2.some((val) => val.id == v.id));
console.log(newArr, "newArr");
// 方法二
let newArr = list.map((i) => i.id);
let newArr2 = list2.filter((i) => newArr.includes(i.id));
console.log("newArr", newArr);
console.log("newArr", newArr2);
// 方法三
let newArr = list.map((i) => i.id);
let newArr2 = list2.filter((i) => newArr.indexOf(i.id) > -1);
console.log(newArr, "newArr");
console.log(newArr2, "newArr2");
// 方法四
let newArr = [];
list.filter(i > newArr.push(i.id));
let newArr2 = list2.filter((i) => newArr.indexOf(i.id) !== -1);
console.log(newArr);
console.log(newArr2);
// 方法五
let newArr = [];
list.forEach((i) => {
  list2.forEach((j) => {
    if (i.id == j.id) newArr.push(i);
  });
});
console.log("newArr", newArr);
// 方法六
let newArr = [];

for (let i = 0; i < list2.length; i++) {
  let obj = list2[i];
  for (let j = 0; j < list.length; j++) {
    let boj2 = list[j];
    if (obj.id === boj2.id) {
      newArr.push(boj2);
      break;
    }
  }
}

console.log("newArr", newArr);

4.2 table 数据进行排序不管是字符串还是 number

// 方法一 --- sort-method:属性  和Array.sort()表现一致
// 值得注意的一点是sort-method是属性不是方法,要写成”:sort-method=’sortfunc'”而不能写成”@sort-method=’sortfunc'”,另外两个排序方法都需要将sortable设为true才能生效
// 如果升序排序则
Array.sort(function (a, b) {
  return a - b;
});
// 如果降序排序则
Array.sort(function (a, b) {
  return b - a;
});

// 如果是对象组成的数组,需要按照对象的某个key的值进行排序,则可以按照下面的方式来进行
Array.sort(function (obj1, obj2) {
  let val1 = obj1.key;
  let val2 = obj2.key;
  return val1 - val2;
});

// 方法二--- sortable
// 方法三--- sort-by
// 这个属性是指定数据按照哪个属性进行排序,比如上面的例子中如果要按照时间戳来排序可以直接把:sort-method=”sortByDate”换成sort-by=”deadline”就能达到同样的效果了,如果使用了sort-by就不能使用sort-method了,否则不会生效。

// 数组排序方法
// 方法四 --- sort
// 数字
var sum = 0;
var numbers = [4, 2, 5, 1, 3];
numbers.sort(function (a, b) {
  sum++;
  return a - b;
});

console.log(numbers); // 1 2 3 4 5
console.log(sum); // 7
// 字符串
const months = ["March", "Jan", "Feb", "Dec"];
months.sort();
// 数组对象
var student = [
  { name: "jack", age: 18 },
  { name: "apple", age: 16 },
  { name: "tony", age: 30 },
  { name: "marry", age: 8 },
];
// 按年龄
student.sort(function (a, b) {
  return a.age - b.age;
});

// 按姓名
student.sort(function (a, b) {
  var nameA = a.name.toUpperCase();
  var nameB = b.name.toUpperCase();
  if (nameA < nameB) {
    return -1;
  }
  if (nameA > nameB) {
    return 1;
  }
});
// 方法五-冒泡排序
// 相邻两个数逐个比较,如果前一个数比后一个数小则交换位置。
//重点:交换过程需要变量存储较小值/较大值

var numbers = [4, 2, 5, 1, 3];

var sum = 0;
function bubbleSort(arr) {
  let temp = "";
  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        temp = arr[j + 1];
        arr[j + 1] = arr[j];
        arr[j] = temp;
      }
      sum++;
    }
  }
  return arr;
}

console.log(bubbleSort(numbers));
console.log(sum); // 20
// 方法六---快速排序
//冒泡排序的改进算法。通过多次的比较和交换来实现排序。
//重点:需设定分界值,根据分界值将数组分为左右两部分。然后在左右两边不断重复取分界值和分左右部分的操作。
var numbers = [4, 2, 5, 1, 3];

var sum = 0;
function quickSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  var medianIndex = Math.floor(arr.length / 2); // 分解值索引
  var medianValue = arr.splice(medianIndex, 1); // 分界值
  var left = [];
  var right = [];
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] < medianValue) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
    sum++;
  }
  console.log(medianIndex, medianValue, left, right);
  return quickSort(left).concat(medianValue, quickSort(right));
}

console.log(quickSort(numbers));
console.log(sum); // 10

// 方法七---插入排序
//假设前面 n-1 的元素已经排好序,将第n个元素插入到前面已经排好的序列中。

//重点:需定义有序序列中最后一个位置,从最后一位开始不断和序列前元素进行比较,直到找到插入位置。
var numbers = [4, 2, 5, 1, 3];

var sum = 0;
function insertSort(arr) {
  // 假设第一个元素已经排好序
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < arr[i - 1]) {
      // 取出无序序列中需要插入的第i个元素
      var temp = arr[i];
      // 定义有序中的最后一个位置
      var j = i - 1;
      arr[i] = arr[j];
      // 根据序列最后一位,不断循环比较,找到插入的位置
      while (j >= 0 && temp < arr[j]) {
        arr[j + 1] = arr[j];
        j--;
        sum++;
      }
      //插入
      arr[j + 1] = temp;
    }
  }
}

console.log(insertSort(numbers));
console.log(sum); // 6

// 方法八 希尔排序
//希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序。

//希尔排序是插入排序算法的一种更高效的改进版本。
var numbers = [4, 2, 5, 1, 3];
var sum = 0;

function shellSort(arr) {
  var len = arr.length;
  // 定义间隔区间
  var fraction = Math.floor(len / 2);
  // fraction = Math.floor(fraction / 2) => 循环中不断切割区间
  for (fraction; fraction > 0; fraction = Math.floor(fraction / 2)) {
    // 以间隔值开始遍历
    for (var i = fraction; i < len; i++) {
      // 如果前面一个大于后面一个
      for (
        var j = i - fraction;
        j >= 0 && arr[j] > arr[fraction + j];
        j -= fraction
      ) {
        var temp = arr[j];
        arr[j] = arr[fraction + j]; // 后移
        arr[fraction + j] = temp; // 填补
        sum++;
      }
    }
  }
}

console.log(shellSort(numbers));
console.log(sum); // 6

// 方法九---选择排序
// 从待排序的数据元素中选出最小/最大的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小/最大元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。

var numbers = [4, 2, 5, 1, 3];

var sum = 0;
function selectionSort(arr) {
  if (arr == null || arr.length < 2) {
    return arr;
  }
  for (var i = 0; i < arr.length - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < arr.length; j++) {
      minIndex = arr[j] < arr[minIndex] ? j : minIndex;
      sum++;
    }
    let temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
  }
  return arr;
}

console.log(selectionSort(numbers));
console.log(sum); // 10

以同一个数组[4, 2, 5, 1, 3]不同的方法比较计算次数

方法计算次数稳定性
sort()7
冒泡排序20稳定
快速排序10不稳定
插入排序6稳定
希尔排序6不稳定
选择排序10不稳定

5. 事件循环机制(Event Loop)

5.1 什么是事件循环机制

在介绍事件循环机制之前,我们要首先了解以下什么是事件循环机制,我们首先看下下面这段代码的执行顺序,正确的执行顺序应该是序号 1>3>2

代码片段转存失败,建议直接上传图片文件

原因是 JS 引擎指向代码是从上往下执行的,所以首先会执行序号 1 这个语句,JS 引擎会将这个语句放在调用栈当中,然后执行代码,将序号 1 打印在控制台当中,当这段代码执行完毕之后,便将这段代码从调用栈中移出去。然后开始执行后续的代码,此时 setTimeout 这段代码进入调用栈,这段代码,会调用 Web API,2 秒之后进入 callback 队列,此时 JS 引擎将 setTimeout 移出调用栈,继续执行后面的代码,所以屏幕上会先打印出序号 1,3,此时 eventLoop 登场了,它会不断循环的访问 callbackqueue,等 2s 之后 Web API 会将要执行的打印序号 2 这句话放入 callbackqueue,eventLoop 将 callbackQueue 中的内容放入调用栈,开始执行,然后屏幕上打印出序号 2,这就是 eventLoop 的基本流程

执行顺序图解

执行顺序转存失败,建议直接上传图片文件

引出事件循环是什么

JS 的运行机制就是事件循环!

5.2js 的执行顺序是什么

  1. JS 是从上到下一行一行执行。
  2. 如果某一行执行报错,则停止执行下面的代码。
  3. 先执行同步代码,再执行异步代码

5.3 事件循环的执行过程

  • 同步代码,调用栈执行后直接出栈
  • 异步代码,放到 Web API 中,等待时机,等合适的时候放入回调队列(callbackQueue),等到调用栈空时 eventLoop 开始工作,轮询
  • 微任务执行时机比宏任务要早 执行顺序转存失败,建议直接上传图片文件
  • 微任务在 DOM 渲染前触发,宏任务在 DOM 渲染后触发,

5.4 微任务和宏任务的根本区别

  • 宏任务:setTimeout,setInterval,Ajax,Dom 事件,script 标签,postMessage,requestAnimationFrame,setImmediate 与 I/O
  • 微任务: Promise,Promise.then().catch, async/await,MutaionObserver,proxy,process.nextTick(Node.js)
  • 微任务执行时机比宏任务要早
  • 微任务是由 ES6 语法规定的
  • 宏任务是由浏览器规定的

5.5 事件循环的整体流程

  1. 先清空 call stack 中的同步代码
  2. 执行微任务队列中的微任务
  3. 尝试 DOM 渲染
  4. 触发 Event Loop 反复询问 callbackQueue 中是否有要执行的语句,有则放入 call back 继续执行 执行顺序转存失败,建议直接上传图片文件

6 vue3 比起 vue2 的优势是什么

  • 性能提升:‌Vue3 通过优化 Virtual DOM 和模板编译,‌ 使得页面渲染速度更快,‌ 性能更高。‌ 特别是在处理大量数据和复杂组件时,‌Vue3 的优势更加明显。‌ 此外,‌Vue3 的编译器生成的代码更为紧凑和高效,‌ 加速了首次渲染和更新速度。‌
  • 响应式系统改进:‌Vue3 使用 Proxy 代理对象替代 Object.defineProperty,‌ 使得响应式系统更加高效、‌ 灵活,‌ 数据变更更加可预测和透明。‌
  • 组件开发方式:‌Vue3 引入了 Composition API,‌ 让组件代码更加简洁和可复用,‌ 开发者在编写组件时更加灵活和自由。‌ 相比 Vue 2 的 Options API,‌Composition API 更灵活,‌ 可读性更好,‌ 并且更容易共享和复用逻辑代码。‌
  • TypeScript 支持:‌Vue3 对 TypeScript 的支持更加严格和完整,‌ 提供了更加准确的类型检查和错误提示,‌ 帮助开发者写出更加安全和可维护的代码。‌
  • 可维护性和拓展性:‌Vue3 通过组件化和模块化的方式,‌ 极大地增加了代码的可维护性和拓展性,‌ 让开发者在项目开发过程中更加容易进行代码管理和扩展。‌
  • 自定义渲染:‌Vue3 引入了一个新的自定义渲染 API,‌ 让开发者可以更加灵活地控制组件的渲染方式,‌ 支持自定义渲染器和自定义渲染节点等。‌
  • 懒加载支持:‌Vue3 默认开启了懒加载机制,‌ 可以极大地提升页面加载速度和性能。‌
  • 体积优化:‌Vue3 中的模板编译器和运行时都经过了重构和优化,‌ 使得整个库的体积变得更小,‌ 更适合现代前端项目的发展需求。‌

7 vue3CompositionAPI 与 vue2 组合式 api 的好处在哪

‌Vue3 引入了 Composition API,‌ 让组件代码更加简洁和可复用,‌ 开发者在编写组件时更加灵活和自由。‌ 相比 Vue 2 的 Options API,‌Composition API 更灵活,‌ 可读性更好,‌ 并且更容易共享和复用逻辑代码。‌

8 vue3 父子组件传值、父子组件方法调用

使用 vue3 官方提供的 setup 语法糖中给出的 defineEmits、defineProps、defineExpose 来定义父子间的传参值和关联方法

暴露方法:defineExpose

引入方法:defineEmits

获取属性值:defineProps

9 前端在项目开发中起到的作用

  1. 用户交互体验:前端主要负责设计和开发用户界面(UI)和用户体验(UX)。他们根据用户需求和行为,创建并优化网站、应用程序或其它用户交互的界面。这包括对页面布局、色彩搭配、动画效果、交互设计的考虑。良好的用户交互体验能够提高用户满意度和使用效率。
  2. 前后端桥梁:前端是后端工程师和设计师之间的桥梁。他们需要理解后端架构和功能,同时也要理解设计师的意图和用户需求。他们负责将后端的 API 接口和前端的界面进行整合,使得整个系统能够协调工作。
  3. 数据可视化和处理:前端通常负责数据的可视化和处理。他们可以通过使用各种图表、数据展示组件等方式,帮助用户更好地理解数据。同时,他们也可能使用数据 API 或后端数据存储来获取和操作数据,以满足用户需求。
  4. 响应式设计:随着多设备、多屏幕尺寸的普及,前端需要设计和开发响应式布局。他们需要根据不同设备的特性,设计和开发能够自适应不同屏幕尺寸的界面,确保用户在任何设备上都能获得良好的用户体验。
  5. 性能优化:前端也需要考虑性能优化问题。他们可以通过优化图片大小、减少 HTTP 请求次数、使用 CDN 等方式,提高网站或应用的加载速度和响应速度,从而提高用户体验。
  6. 跨部门合作:前端需要与设计师、产品经理、后端工程师等多个部门进行合作,确保项目的顺利进行。他们需要理解各个部门的需求和目标,与他们进行有效的沟通和协作,共同完成项目

10 递归函数及数组和树型结构互转

listr 转树

// 第一步:
// 递归: 封装一个函数:将列表list数据转换成数据树形数据=>递归
// 遍历树形 有一个重点 要先找一个头
function transListDataToTreeData(list, root) {
  const arr = [];
  // 1.遍历
  list.forEach((item) => {
    // 2.首次传入空字符串  判断list的pid是否为空 如果为空就是一级节点
    if (item.pid === root) {
      // 找到之后就要去找item下面有没有子节点  以 item.id 作为 父 id, 接着往下找
      const children = transListDataToTreeData(list, item.id);
      if (children.length > 0) {
        // 如果children的长度大于0,说明找到了子节点
        item.children = children;
      }
      // 将item项, 追加到arr数组中
      arr.push(item);
    }
  });
  return arr;
}

//第二步:
// 调用转化方法,转化树形结构 赋值给data中定义的数组
this.departs = transListDataToTreeData(data.depts, "");

// 第二种
// 根据平铺结构生成树结构
const genTreeData = (originData: any) => {
  const rootNode = originData.filter((n: any) => n.parId === 0);
  const genChildren = (parents: any) => {
    const tempParents = parents.map((parent: any) => {
      const children = originData.filter((m: any) => m.parId === parent.id);
      const tempParent = {
        ...parent,
        key: parent.id,
        title: parent.menuName,
      };
      if (children.length > 0) {
        tempParent.children = genChildren(children);
      }
      return tempParent;
    });
    return tempParents;
  };
  const tempTreeData = genChildren(rootNode);
  return tempTreeData;
};

树形转数组

// tree转数组
function toArray(data) {
  let newArr = [];
  const list = (data) => {
    data.forEach((item) => {
      newArr.push(item);
      if (!!item.children) list(item.children);
      delete item.children;
    });
  };
  list(data);
  return newArr;
}

toArray(data);

// tree转数组 获取平铺结构
const tile = (data: any) => {
  let arr: any = [];
  for (let i = 0, l = data.length; i < l; i++) {
    arr.push(data[i]);
    if (data[i].childMenuList && data[i].childMenuList.length > 0) {
      arr = arr.concat(tile(data[i].childMenuList));
    }
  }
  return arr;
};

11 diff 算法详解-vue 中 key 的作用

使用 index 作为 key 可能会应发的问题? 1、效率问题,页面不会出现展示问题 如果对数据进行逆序添加、删除等破坏顺序操作,会产生没有必要的真实 DOM 更新,页面效果没有问题,但是效率低下。 我们希望可以在 B 和 C 之间加一个 F,Diff 算法默认执行起来是这样的: 全局生命周期显示转存失败,建议直接上传图片文件

即把 C 更新成 F,D 更新成 C,E 更新成 D,最后再插入 E,这样效率不高,且性能不够好。 全局生命周期显示转存失败,建议直接上传图片文件

2、存在输入类 DOM,产生错乱数据展示问题 如果 DOM 结构中还包括输入类(input、textarea)DOM ,会产生错误的 DOM 更新,界面明显看到问题。 如下图所示,当我们在列表中,在每个输入框中添加对应(数据)内容,逆向添加一个数据,在红色框中的位置,可以看到输入框中的数据产生错乱。 全局生命周期显示转存失败,建议直接上传图片文件

下面我们来分析一下原因 1)、初始化数据,根据数据生成虚拟 DOM,将虚拟 DOM 转化为展示 DOM; 2)、更新数据,向初始数据逆序添加一个数据(老刘-30),生成新数据; 3)、数据变化,生成新的虚拟 DOM; 4)、新旧虚拟 DOM 比较:查找相同 key 值,如果查询到,存在差异,则以新虚拟 DOM 替换旧虚拟 DOM,反之,则使用久虚拟 DOM; 5)、如下图,key=0 时,老刘-30 与张三-18 不等,则替换;input 则完全一样,则使用旧虚拟 DOM,但是发现没有,key=1,2...每一步都需要替换,(张三、李四、王五都需要新旧替换)效率低下问题出现; 6)、当 key=3 时,在旧的虚拟 DOM 中未找到,则创建新的虚拟 DOM; 7)、将新的虚拟 DOM 转化为真实 DOM。 全局生命周期显示转存失败,建议直接上传图片文件

解决 index 作为 key 可能引发的问题 通过唯一标识作为 key,则简洁很多 1)、前四步都一样,从第五步开始节省,查找 key=004,发现在旧虚拟 DOM 并未找到,直接创建新的虚拟 DOM; 2)、后面 key=001,002,003...都在旧的虚拟 DOM 中查询到,则直接使用旧虚拟 DOM 3)、将新的虚拟 DOM 转化为真实的 DOM。 全局生命周期显示转存失败,建议直接上传图片文件

总结:key 有什么作用(内部原理)

  1. 虚拟 DOM 中的 key 作用 key 是虚拟 DOM 对象的标识,当数据发生变化时,Vue 会根据新数据生成新的虚拟 DOM,随后 Vue 进行新虚拟 DOM 和旧虚拟> DOM 的差异比较;比较规则如:

  • 旧虚拟 DOM 中找到了与新虚拟 DOM 相同地 key:如果虚拟 DOM 内容没有变化,则直接使用之前的真实 DOM;如果虚拟> DOM 中内容发生改变,则生成新的真实 DOM,随后替换掉页面之前的真实 DOM;

  • 旧虚拟 DOM 中未找到与新虚拟 DOM 相同地 key,则创建新的真实 DOM,随后渲染到页面

  1. 开发中如何选择 key 最好使用每条数据的唯一标识作为 key,比如 id、身份账号、手机号等等; 如果不存在对数据逆序添加、删除等破坏顺序的操作,仅用于列表展示,使用 index 作为 key 是没有问题的。

12 SSR 服务端渲染详解

  • SSR 的基本原理

    SSR 是一种将网站或 Web 应用的页面在服务器端动态生成的技术,而不是在客户端通过 JavaScript 来渲染页面。这意味着用户在浏览器中请求页面时,会直接收到服务器生成的 HTML,而不是一个空白的页面,然后再通过 JavaScript 填充内容。

  • 与 CSR 的对比

    与客户端渲染(CSR)相比,SSR 的主要区别在于页面的首次加载。CSR 通常会加载一个空白的 HTML 页面,然后使用 JavaScript 异步请求数据并渲染页面,这可能导致首次加载时的白屏延迟。而 SSR 则在服务器端生成完整的 HTML 页面,减少了客户端的渲染工作

  • 为什么选择服务端渲染(SSR)

    • 提升性能

      SSR 可以显著减少首次加载的时间,因为浏览器直接接收到完整的 HTML 页面,而不需要等待 JavaScript 的下载和执行。

    • 改善搜索引擎优化(SEO)

      搜索引擎可以更轻松地索引 SSR 生成的页面,因为页面内容在 HTML 中已经存在,而不是通过 JavaScript 生成。

    • 提高用户体验

      更快的加载时间和更好的 SEO 可以改善用户体验,减少用户的等待时间和提高网站的可访问性

  • 如何实现服务端渲染(SSR)

    • 使用服务器端框架

      一些流行的服务器端框架,如 Next.js(React)、Nuxt.js(Vue.js)、Angular Universal(Angular)等,提供了 SSR 的支持和实现。

    • 渲染引擎

      使用服务器端渲染引擎,如 Node.js 的 Express、Koa 等,将页面的请求路由到相应的处理器并生成 HTML。

    • 数据预取

      在 SSR 中,通常需要提前加载数据并将其注入到 HTML 中,以确保页面在客户端渲染时具备所需的数据。

  • 适用场景

    • 内容密集型页面

      对于需要大量内容渲染的页面,如新闻站点或博客,SSR 特别有用,因为它可以加速内容的加载。

    • SEO 敏感性

      如果网站对 SEO 非常敏感,例如电子商务网站,采用 SSR 可以提高搜索引擎的索引效率。

    • 首屏渲染速度要求高

      对于那些要求页面快速加载并具备良好用户体验的应用,SSR 可以降低首屏渲染的时间。

  • 开始使用服务端渲染(SSR)

    • 选择适当的技术栈

      根据您的应用需求,选择适合的服务器端框架或渲染引擎,并了解它们的使用方式。

    • 数据管理

      确保您的应用能够预取和管理数据,以便在 SSR 期间注入到页面中。

    • 部署和维护

      部署 SSR 应用可能需要不同的配置,确保服务器能够正确地处理 SSR 请求。

结语

服务端渲染(SSR)是提升 Web 应用性能、SEO 和用户体验的关键技术之一。通过在服务器端生成页面内容,SSR 可以显著减少首次加载时间,改善搜索引擎优化,并提供更好的用户体验。无论您是开发者还是网站管理员,了解 SSR 的原理、优势和实现方式,都将有助于您更好地利用这一技术来构建现代化的 Web 应用

13 小程序的生命周期钩子函数

首先执行 App.onLaunch -> App.onShow 其次执行 Component.created -> Component.attached 再执行 Page.onLoad -> Page.onShow 最后 执行 Component.ready -> Page.onReady

全局生命周期显示转存失败,建议直接上传图片文件

14 const 声明的数组值可以改变吗

数组的值可以改变,但是他的引用路径不能改变,也就是说不能对整个数组进行重新赋值,但可以通过修改数组的元素来实现对数组的修改,比如 push(),pop(),shift(),unshift()等添加删除元素

15 displaynone 与 visblity hidden 的区别

  • 空间占用与布局影响:
    • 当一个元素被设置为 visibility: hidden,它虽然在视觉上不可见,但仍然占据文档中的物理空间,这意味着它仍然会影响页面的布局和其他元素的定位。相反,
    • 当元素被设置为 display: none,它不仅在视觉上被隐藏,而且完全从文档流中移除,不占据任何空间,因此不会影响其他元素的布局或定位。
  • 性能考虑:
    • display: none 从文档流中移除元素,可能对性能有积极影响,尤其是在处理大量隐藏元素时。
    • visibility: hidden 虽然隐藏了元素,但由于它仍然占据空间,对性能的影响较小,但可能需要额外的布局计算。‌3
  • 继承与计数器影响:
    • visibility: hidden 具有继承性,如果给父元素设置此属性,子元素也会继承这种隐藏状态。此外,即使元素被设置为 visibility: hidden,页面的计数器仍然会计算该元素,
    • 这与 display: none 完全不同,后者会导致计数器忽略隐藏的元素。

注意:display 和 visibility 都会引起页面重绘。‌ 修改常规文档流中元素的 display 通常会造成文档的重排,但是修改 visibility 属性只会造成本元素的重绘;

16 定位的几种值和作用

  1. relative:相对定位,相对于自己本身在正常文档流中的位置进行定位。
  2. absolute:生成绝对定位,相对于最近一级定位不为 static 的父元素进行定位。
  3. fixed: 生成绝对定位,相对于浏览器窗口或者 frame 进行定位。(老版本 IE 不支持)
  4. static:默认值,没有定位,元素出现在正常的文档流中。(很少用)
  5. sticky:生成粘性定位的元素,容器的位置根据正常文档流计算得出。(很少用)

17 token 往 vuex 存一份的同时还往 localStorage 中存一份的原因

vuex 存储数据的特点是数据统一全局管理。

一旦数据在某组件更新,其他所有组件的数据都会同步更新。也就是说它是响应式的。

但是如果数据只存在 vuex 中,刷新页面,vuex 中的数据会重新初始化,导致数据丢失,恢复到原来的状态。

localstorage 存储数据的特点是永久性存储。但他不是响应式的。当某个组件数据修改的时候,其他组件无法同步更新。 另外 vuex 是存储在内存中的,而 localstorage 是本地存储,是存储到磁盘里的。从内存中读取数据速度是远高于磁盘的。所以我们把数据存在 vuex 中,可以提高获取 token 的速度,从而提高性能。

通常我们在实际项目中都是结合这两者使用,拿到 token 后,把 token 存储到 localstorage 和 vuex 中。vuex 保证数据在各组件间同步更新。如果刷新页面数据丢失,我们可以从 localstorage 获取。通过结合这两者,从而实现数据的持久化。

18 BFC 是什么

BFC (Block formatting context) 直译为 "块级格式化上下文"。它是一个独立的渲染区域,只有 Block-level box 参与,它规定了内部的 Block-level Box 如何布局,并且与这个区域外部毫不相干。

19 如何保证 vue 组件状态不变,但是切换页签时数据刷新

keep-alive:

  • activated: 组件被激活时调用,可以用来更新数据等操作。
  • deactivated: 组件被缓存时调用,可以用来清除数据等操作。

20 foreach 循环和 for 循环区别

在固定长度或者长度不需要计算的时候 for 循环效率高于 foreach,在不确定长度或者计算长度有损性能的时候用 foreach 比较方便

foreach 在循环次数未知或者计算起来较复杂的情况下效率比 for 循环高。

foreach 适用于只是进行集合或数组遍历,for 则在较复杂的循环中效率更高。

如果对集合中的值进行修改,就要用 for 循环了。其实 foreach 的内部原理其实也是 Iterator,但它不能像 Iterator 一样可以人为的控制,而且也不能调用 iterator.remove();更不能使用下标来访问每个元素,所以不能用于增加,删除等复杂的操作。

forEach 相比普通的 for 循环的优势在于对稀疏数组的处理,会跳过数组中的空位