经常会被问及的 Vue 面试题(下)

1263cc60717624286f2214d973af3643.png

作者:东起

链接:https://zhuanlan.zhihu.com/p/103763164

上一篇文章我们分享了《经常会被问及的 Vue 面试题(上)


11、怎么在vue中点击别的区域输入框不会失去焦点?

答:阻止事件的默认行为

具体操作:监听你想点击后不会丢失 input 焦点的那个元素的 mousedown 事件,回调里面调用 event.preventDefault(),会阻止使当前焦点丢失这一默认行为。

12、vue中data的属性可以和methods中的方法同名吗?为什么?

答:不可以

因为,Vue会把methods和data的东西,全部代理到Vue生成的对象中,会产生覆盖所以最好不要同名

13、怎么给vue定义全局的方法?

Vue.prototype.方法名称

14、Vue 2.0 不再支持在 v-html 中使用过滤器怎么办?

解决方法:

①全局方法(推荐)

Vue.prototype.msg = function(msg){
  return msg.replace("\n","<br>")
 }
 <div v-html="msg(content)"></div>

②computed方法

computed:{
 content:function(msg){
  return msg.replace("\n","<br>")
 }
}
<div>{{content}}</div>

③$options.filters(推荐)

filters:{
 msg:function(msg){
  return msg.replace(/\n/g,"<br>")
 }
},    
data:{
 content:"XXXX"
}
<div v-html="$options.filters.msg(content)"></div>

15、怎么解决vue打包后静态资源图片失效的问题?

答:将静态资源的存放位置放在src目录下

16、怎么解决vue动态设置img的src不生效的问题?

<img class="logo" :src="logo" alt="公司logo">
data() {
  return {
    logo:require("./../assets/images/logo.png"),
  };
}

因为动态添加src被当做静态资源处理了,没有进行编译,所以要加上require

17、跟keep-alive有关的生命周期是哪些?描述下这些生命周期

activated和deactivated两个生命周期函数

1.activated:当组件激活时,钩子触发的顺序是created->mounted->activated

2.deactivated: 组件停用时会触发deactivated,当再次前进或者后退的时候只触发activated

18、你知道vue中key的原理吗?说说你对它的理解

暂时没弄明白,等会儿写

19、vue中怎么重置data?

答:Object.assign()

Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

var o1 = { a: 1 };
var o2 = { b: 2 };
var o3 = { c: 3 };
var obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1); // { a: 1, b: 2, c: 3 }, 注意目标对象自身也会改变。

注意,具有相同属性的对象,同名属性,后边的会覆盖前边的。

由于Object.assign()有上述特性,所以我们在Vue中可以这样使用:

Vue组件可能会有这样的需求:在某种情况下,需要重置Vue组件的data数据。此时,我们可以通过this.$data获取当前状态下的data,通过this.$options.data()获取该组件初始状态下的data。

然后只要使用**Object.assign(this.options.data())**就可以将当前状态的data重置为初始状态。

20、vue怎么实现强制刷新组件?

答:① v-if ② this.$forceUpdate

v-if

当v-if的值发生变化时,组件都会被重新渲染一遍。因此,利用v-if指令的特性,可以达到强制
<comp v-if="update"></comp>
<button @click="reload()">刷新comp组件</button>
data() {
 return {
   update: true
  }
 },
 methods: {
  reload() {
     // 移除组件
    this.update = false
      // 在组件移除后,重新渲染组件
      // this.$nextTick可实现在DOM 状态更新后,执行传入的方法。
    this.$nextTick(() => {
      this.update = true
    })
  }
 }

this.$forceUpdate

<button @click="reload()">刷新当前组件</button>
methods: {
  reload() {
    this.$forceUpdate()
  }
}

21、vue如何优化首页的加载速度?

① 第三方js库按CDN引入(一、cdn引入 二、去掉第三方库引入的import 三、把第三方库的js文件从打包文件里去掉)

② vue-router路由懒加载

③ 压缩图片资源

④ 静态文件本地缓存

http缓存:推荐网站:https://www.cnblogs.com/chinajava/p/5705169.html

service worker离线缓存:,缺点:需要在HTTPS站点下,推荐:http://lzw.me/a/pwa-service-worker.html

⑤ 服务器端SSR渲染

除了上面的方案以外,另一种方案也不容小视

我们先说说通常项目中是如何加载页面数据:Vue组件生命周期中请求异步接口,在mounted之前应该都可以,据我了解绝大部分同学是在mounted的时候执行异步请求。但是我们可以把页面需要的请求放到Vue-Router的守卫中执行,意思是在路由beforeEnter之前就可以请求待加载页面中所有组件需要的数据,此时待加载页面的Vue组件还没开始渲染,而Vue组件开始渲染的时候我们就可以用Vuex里面的数据了。

以上方法的实现思路:

82c684966f32b61ea0b3b7e130b51a75.png

图意:每个页面(Page)中都会有很多个Vue组件,可以在Vue组件中添加自定义属性fetchData,fetchData里面可以执行异步请求(图中执行Vuex的Action),但是我们怎么获取到所有组件的fetchData方法并执行呢?如图所示,在router.beforeResolve守卫中,我们看看router.beforeResolve的定义,所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用,意思是即使页面中有异步组件,它会等待异步组件解析之后执行,并且解析守卫在beforeEnter之前执行。那我们怎么在解析守卫中获取到待加载页面的所有组件呢?通过router.getMatchedComponents方法。

bb194d89d0ee93829fa1d432355f26b7.png f24be450e22c8b767f245ea5772bdc7a.png

这样我们就可以在解析守卫中获取到所有待加载组件的fetchData方法并执行,这样无疑会在组件开始渲染之后获取到所有数据,提高页面加载速度。以上方法的实现思路:

很多人可能有个疑问,如果异步请求放在beforeCreate和created不是一样吗?答案是否定的,因为这种方式可以将异步请求放到beforeCreate之前!

22、你了解vue的diff算法吗?

推荐网站:https://www.cnblogs.com/wind-lanyan/p/9061684.html

23、vue能监听到数组变化的方法有哪些?为什么这些方法能监听到呢?

Vue.js观察数组变化主要通过以下7个方法(push、pop、shift、unshift、splice、sort、reverse)

大家知道,通过Object.defineProperty()劫持数组为其设置getter和setter后,调用的数组的push、splice、pop等方法改变数组元素时并不会触发数组的setter,继而数组的数据变化并不是响应式的,但是vue实际开发中却是实时响应的,是因为vue重写了数组的push、splice、pop等方法

从源码中可以看出,ob.dep.notify()将当前数组的变更通知给其订阅者,这样当使用重写后方法改变数组后,数组订阅者会将这边变化更新到页面中

24、说说你对proxy的理解?

Proxy用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。

var target = {
   name: 'zhangsan',
   age:20,
   sex:'男'
 }
var logHandler = {
  get(target, key) {
    console.log(`${key}被读取`)
    return target[key]
   },
  set(target, key, value) {
    console.log(`${key}被设置为${value}`)
    target[key] = value
  }
}
var demo = new Proxy(target, logHandler)
demo.name  //name被读取

var proxy = new Proxy(target, handler);

Proxy对象的所有用法,都是上面的这种形式。不同的只是handle参数的写法。其中new Proxy用来生成Proxy实例,target是表示所要拦截的对象,handle是用来定制拦截行为的对象。

我们可以将Proxy对象,设置到object.proxy属性,从而可以在object对象上调用。

var object = { proxy: new Proxy(target, handler) };

Proxy对象也可以作为其它对象的原型对象。

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});
let obj = Object.create(proxy);
obj.time // 35

上面代码中,proxy对象是obj的原型对象,obj本身并没有time属性,所以根据原型链,会在proxy对象上读取属性,从而被拦截。

同一个拦截函数,可以设置多个操作。

var handler = {
  get: function (target, name) {
    if (name === 'prototype') {
       return Object.prototype;
    }
    return 'Hello, ' + name;
  },
 
  apply: function (target, thisBinding, args) {
    return args[0];
  },
 
  construct: function (target, args) {
    return { value: args[1] };
  }
};
 
var fproxy = new Proxy(function (x, y) {
    return x + y;
}, handler);
 
fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true

25、怎么缓存当前的组件?缓存后怎么更新?

<keep-alive>
    <router-view></router-view>
</keep-alive>
<!-- 这里是需要keepalive的 -->
<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 这里不会被keepalive -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
{
  path: '',
  name: '',
  component: ,
  meta: {keepAlive: true} // 这个是需要keepalive的
},
{
  path: '',
  name: '',
  component: ,
  meta: {keepAlive: false} // 这是不会被keepalive的
}

如果缓存的组件想要清空数据或者执行初始化方法,在加载组件的时候调用activated钩子函数,如下:

activated: function () {
    this.data = '';
}

26、axios怎么解决跨域的问题?

使用axios直接进行跨域访问不可行,我们需要配置代理

代理可以解决的原因:

因为客户端请求服务端的数据是存在跨域问题的,而服务器和服务器之间可以相互请求数据,是没有跨域的概念(如果服务器没有设置禁止跨域的权限问题),也就是说,我们可以配置一个代理的服务器可以请求另一个服务器中的数据,然后把请求出来的数据返回到我们的代理服务器中,代理服务器再返回数据给我们的客户端,这样我们就可以实现跨域访问数据

1.配置BaseUrl

import axios from 'axios'
Vue.prototype.$axios = axios
axios.defaults.baseURL = '/api'  //关键代码

2.配置代理

在config文件夹下的index.js文件中的proxyTable字段中,作如下处理:

proxyTable: {
 '/api': {
   target:'http://api.douban.com/v2', // 你请求的第三方接口
   changeOrigin:true,
// 在本地会创建一个虚拟服务端,然后发送请求的数据,并同时接收请求的数据,
//这样服务端和服务端进行数据的交互就不会有跨域问题
   pathRewrite:{  // 路径重写,
    '^/api': ''
// 替换target中的请求地址,也就是说以后你在请求http://api.douban.com/v2/XXXXX
//这个地址的时候直接写成/api即可。
   }
  }
}

1. 在具体使用axios的地方,修改url如下即可

axios.get("/movie/top250").then((res) => {
  res = res.data
  if (res.errno === ERR_OK) {
    this.themeList=res.data;
  }
 }).catch((error) => {
  console.warn(error)
})

原理:

因为我们给url加上了前缀/api,我们访问/movie/top250就当于访问了:localhost:8080/api/movie/top250(其中localhost:8080是默认的IP和端口)。

在index.js中的proxyTable中拦截了/api,并把/api及其前面的所有替换成了target中的内容,因此实际访问Url是http://api.douban.com/v2/movie/top250。

至此,纯前端配置代理解决axios跨域得到解决

27、怎么实现路由懒加载呢?

第一种(最常用):

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

第二种:

const router = new Router({
  routes: [
   {
     path: '/index',
     component: (resolve) => {
        require(['../components/index'], resolve) // 这里是你的模块 不用import去引入了
     }
    }
  ]
})

第三种(官方推荐):

// r就是resolve
const list = r => require.ensure([], () => r(require('../components/list/list')), 'list');
// 路由也是正常的写法  这种是官方推荐的写的 按模块划分懒加载
const router = new Router({
  routes: [
  {
    path: '/list/blog',
    component: list,
    name: 'blog'
  }
 ]
})

28、怎样动态加载路由?

一、思路

① 在vue-router对象中首先初始化公共路由,比如(首页,404,login)等

② 用户登陆成功后,根据用户的角色信息,获取对应权限菜单信息menuList,并将后台返回的menuList转换成我们需要的router数据结构

③ 通过**router.addRouter(routes)**方法,同时我们可以将转后的路由信息保存于vuex,这样我们可以在我们的SideBar组件中获取我们的全部路由信息,并且渲染我们的左侧菜单栏,让动态路由实现。

二、实现

① 初始化公共路由

//只显示主要代码
export const routes= [
 { path: '/login', component: () => import('@/views/login/index'), hidden: true },
 { path: '/404', component: () => import('@/views/404'), hidden: true }
]
export default new Router({
 scrollBehavior: () => ({ y: 0 }),
 routes: routes
})

② 登陆成功后,获取菜单信息 menuList,并转换成router数组的结构

router.beforeEach((to, from, next) => {
 NProgress.start()//进度条包 npm安装
 if (getToken()) {
  /*有 token,已经登录成功*/
  if (to.path === '/login') {
   next({ path: '/' })
   NProgress.done()
  } else {
   if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
    store.dispatch('GetInfo').then(res => { // 拉取user_info
     const roles = res.roles
     store.dispatch("GetMenu").then(data => {
      initMenu(router, data);
     });
     next()
    }).catch((err) => {
     store.dispatch('FedLogOut').then(() => {
      Message.error(err || 'Verification failed, please login again')
      next({ path: '/' })
     })
    })
   } else {
    next()
   }
  }
 } else {
  /* 无 token*/
  if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
   next()
  } else {
   next('/login') // 否则全部重定向到登录页
   NProgress.done()
  }
 }
})
router.afterEach(() => {
 NProgress.done()
})

③ 动态加载路由

import store from '../store'
export const initMenu = (router, menu) => {
 if (menu.length === 0) {
  return
 }
 let menus = formatRoutes(menu);
 
 let unfound = { path: '*', redirect: '/404', hidden: true }
 menus.push(unfound) //404组件最后添加
 router.addRoutes(menus)
 store.commit('ADD_ROUTERS',menus)
}
export const formatRoutes = (aMenu) => {
 const aRouter = []
 aMenu.forEach(oMenu => {
  const {
   path,
   component,
   name,
   icon,
   childrens
  } = oMenu
  if (!validatenull(component)) {
   let filePath;
   const oRouter = {
    path: path,
    component(resolve) {
     let componentPath = ''
     if (component === 'Layout') {
      require(['../views/layout/Layout'], resolve)
      return
     } else {
      componentPath = component
     }
     require([`../${componentPath}.vue`], resolve)
    },
    name: name,
    icon: icon,
    children: validatenull(childrens) ? [] : formatRoutes(childrens)
   }
   aRouter.push(oRouter)
  }
 })
 return aRouter
}

④ 渲染菜单

<template>
 <el-scrollbar wrapClass="scrollbar-wrapper">
  <el-menu
   mode="vertical"
   :show-timeout="200"
   :default-active="$route.path"
   :collapse="isCollapse"
   background-color="#304156"
   text-color="#bfcbd9"
   active-text-color="#409EFF"
  >
   <sidebar-item v-for="route in permission_routers" :key="route.name" :item="route" :base-path="route.path"></sidebar-item>
  </el-menu>
 </el-scrollbar>
</template>
 
<script>
import { mapGetters } from 'vuex'
import SidebarItem from './SidebarItem'
import { validatenull } from "@/utils/validate";
import { initMenu } from "@/utils/util";
 
export default {
 components: { SidebarItem },
 created() {
 },
 computed: {
  ...mapGetters([
   'permission_routers',
   'sidebar',
   'addRouters'
  ]),
  isCollapse() {
   return !this.sidebar.opened
  }
 }
}
</script>

就这样我们动态加载路由就是实现了,关键点就是router.addRoute方法

⑤ 防坑

点击刷新的时候页面空白 控制台也不报错?

点击刷新,vue-router会重新初始化,那么我们之前的动态addRoute就不存在了,此时访问一个不存在的页面,所以我们的sidebar组件也就不会被访问,那么也无法获取菜单信息,就导致页面空白。所以我们需要把加载菜单信息这一步放在router的全局守卫beforeEach中就可以了。

export const initMenu = (router, menu) => {
 if (menu.length === 0) {
  return
 }
 let menus = formatRoutes(menu);
 // 最后添加
 let unfound = { path: '*', redirect: '/404', hidden: true }
 menus.push(unfound)
 router.addRoutes(menus)
 store.commit('ADD_ROUTERS',menus)
}
//404组件一定要放在动态路由组件的最后,不然你刷新动态加载的页面,会跳转到404页面的

29、切换到新路由时,页面要滚动到顶部或保持原先的滚动位置怎么做呢?

当创建一个 Router 实例,可以提供一个 scrollBehavior 方法:

注意: 这个功能只在 HTML5 history 模式下可用。
const router = new VueRouter({
 routes: [...],
 scrollBehavior (to, from, savedPosition) {
  // return 期望滚动到哪个的位置
 }
})

scrollBehavior 方法接收 to 和 from 路由对象。第三个参数 savedPosition 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用。

scrollBehavior (to, from, savedPosition) {
 return { x: 0, y: 0 }
}
对于所有路由导航,简单地让页面滚动到顶部。
返回 savedPosition,在按下 后退/前进 按钮时,在滚动条位置,就会像浏览器的原生表现那样:
scrollBehavior (to, from, savedPosition) {
 if (savedPosition) {
  return savedPosition
 } else {
  return { x: 0, y: 0 }
 }
}
模拟『滚动到锚点』的行为
scrollBehavior (to, from, savedPosition) {
 if (to.hash) {
  return {
   selector: to.hash
  }
 }
}

还可以利用路由元信息更细颗粒度地控制滚动。

routes: [
 { path: '/', component: Home, meta: { scrollToTop: true }},
 { path: '/foo', component: Foo },
 { path: '/bar', component: Bar, meta: { scrollToTop: true }}
]
const scrollBehavior = (to, from, savedPosition) => {
 if (savedPosition) {
  return savedPosition
 } else {
  const position = {}
  if (to.hash) {
   position.selector = to.hash
  }
   if (to.matched.some(m => m.meta.scrollToTop)) {
   position.x = 0
   position.y = 0
  }
  return position
 }
}

还可以在main.js入口文件配合vue-router写这个

router.afterEach((to,from,next) => {
  window.scrollTo(0,0);
});

30、vue-router如何响应路由参数的变化?

当使用路由参数时,比如:

{path:’/list/:id’component:Foo}

从 /list/aside导航到 /list/foo,原来的组件实例会被复用。

因为两个路由都渲染同个组件Foo,比起销毁再创建,复用则更加高效。

不过,这也意味着组件的生命周期钩子不会再被调用

如果跳转到相同的路由还会报以下错误

298f8c62766e3f0afd8d91b97c2729d5.png

这个时候我们需要重写push方法,在src/router/index.js 里面import VueRouter from 'vue-router'下面写入下面方法即可

const routerPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
  return routerPush.call(this, location).catch(error=> error)
}

如何响应不同的数据呢?

① 复用组件时,想对路由参数的变化作出响应的话,你可以简单地 watch (监测变化) $route 对象

const User = {
  template: '...',
  watch: {
    '$route' (to, from) {
      // 对路由变化作出响应...
    }
  }
}

② 使用beforeRouteUpdate

const User = {
  template: '...',
  beforeRouteUpdate (to, from, next) {
    // react to route changes...
    // don't forget to call next()
  }
}

注意:

(1)从同一个组件跳转到同一个组件。

(2)生命周期钩子created和mounted都不会调用。

31、vue模板中为什么以_、$开始的变量无法渲染?

名字以 _ 或 data._property 访问它们。

32、vue中,如何监听一个对象内部的变化?

方法①:对整个obj深层监听

watch:{
 obj:{
  handler(newValue,oldValue){
   console.log('obj changed')
  },
  deep: true,//深度遍历
  immediate: true
//默认第一次绑定的时候不会触发watch监听,值为true时可以在最初绑定的时候执行
 }
}

方法② :指定key

watch: {
    "dataobj.name": {
      handler(newValue, oldValue) {
        console.log("obj changed");
      }
    }
  }

方法③:computed

computed(){
 ar(){
  return this.obj.name
 }
}

33、v-for循环时为什么要加key?

key的作用主要是为了高效的更新虚拟DOM,是因为Virtual DOM 使用Diff算法实现的原因。

当某一层有很多相同的节点时,也就是列表节点时,Diff算法的更新过程默认情况下也是遵循以上原则。

比如一下这个情况

dc1b87b3dfb1d2de4e60e6249508f504.png

我们希望可以在B和C之间加一个F,Diff算法默认执行起来是这样的:

d0c99b89a79ff0d3754ecadcc098d7e9.png

即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率?

所以我们需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。

e868f2152ffd0e6a762d8e8ae4e99d2f.png

34、$nextTick用过吗,有什么作用?

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

解决的问题:有些时候在改变数据后立即要对dom进行操作,此时获取到的dom仍是获取到的是数据刷新前的dom,无法满足需要,这个时候就用到了$nextTick。

35、vue和react的区别是什么?

① React严格上只针对MVC的view层,Vue则是MVVM模式

② virtual DOM不一样,vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树.而对于React而言,每当应用的状态被改变时,全部组件都会重新渲染,所以react中会需要shouldComponentUpdate这个生命周期函数方法来进行控制

③ 组件写法不一样, React推荐的做法是 JSX + inline style, 也就是把HTML和CSS全都写进JavaScript了,即'all in js'; Vue推荐的做法是webpack+vue-loader的单文件组件格式,即html,css,jd写在同一个文件;

④ 数据绑定: vue实现了数据的双向绑定,react数据流动是单向的

⑤ state对象在react应用中不可变的,需要使用setState方法更新状态;在vue中,state对象不是必须的,数据由data属性在vue对象中管理

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值