60

1. v-if和v-show指令有什么区别?

v-if 是条件渲染指令,控制的是组件是否创建(渲染),值为true则渲染该组件,值为false则不渲染该组件,对应Html元素则不会存在于浏览器的html文档中,即打开浏览器调试工具找不到该组件对应的渲染结果
v-show控制的是组件是否可见,并且是通过css样式的display属性来控制组件的显示与隐藏,所以其值无论是true还是false,对应Html元素都会存在于浏览器的html文档中,即打开浏览器调试工具都能够找到该组件对应的渲染结果,只不过值为false的时候,会在该组件上添加style="display: none;"
需要注意的是,在vue组件开发的时候,即在.vue中使用v-show的时候当给<style>标签添加上scoped会导致其中的css优先级变高,此时如果添加了v-show的元素上同时使用了display样式,那么将会导致v-show为false的时候添加的dispaly:none失效。

2. v-for 与 v-if 的优先级?

可参考v-for与v-if的优先级

v-for比v-if优先级更高,所以不建议v-for和v-if一起使用,如果v-for和v-if同时使用,那么数据发生变化时,v-for首先会进行遍历,然后通过v-if进行判断,这样v-for和v-if都会同时执行一遍,对性能和展现不友好。所以vue建议用计算属性进行替代,返回过滤后的列表再进行遍历

3. v-for循环中key有什么作用?

key的作用主要就是为了性能优化,key让组件具有了唯一性,能让diff算法更快的找到需要更新的组件dom,在绑定key的时候,最好是一个唯一的值如item.id 而不能是简单的index,如果不使用唯一key,那么在有状态组件中会出现渲染错误。因为它默认用就地复用策略,如果数据项的顺序被改变,那么vue将不是移动DOM元素来匹配数据项的改变,而是简单复用此处每个元素,不会重新排列元素的位置。如果是使用 key,它会基于key重新排列元素顺序,并且会移除 key 不存在的元素。
简单说就是,不使用key就会原地复用,使用key就会对元素位置进行重新排列,能够关联数据状态
<template>
  <div class="home">
    <input type="text" v-model="name">
    <button @click="add">添加</button>
    <ul>
      <li v-for="(item, index) in list" :key="item.id"><!--注意,这里不能使用:key="index"-->
        <input type="checkbox">{{item.name}}
      </li>
    </ul>
  </div>
</template>
<script>
export default {
    data: () => {
        return {
            list: [
              { id: 0, name: 'A' },
              { id: 1, name: 'B' },
              { id: 2, name: 'C' }
            ]
        }
    },
     methods: {
        add() {
          this.list.unshift({ id:3, name:'D'}) // 将D添加到A的前面
        }
  },
}
</script>                                                     
如果v-for上不加key,那么当勾选A前面的复选框后,再点击添加按钮,D会添加到A的前面,由于原地复用原则,不会进行位置的移动,所以第一个位置的复选框是勾选状态会被继承到D上,即D会变成勾选状态而A将失去勾选状态,这个显然与原来状态不符;如果v-for上加上:key="item.id",那么D添加到A前面之后,A、B、C都会向后移动,然后再将D插入到A的前面,所以插入D后,A仍然保持勾选状态。

图片描述
图片描述

4. vue路由传参数的三种方式?

query: 直接输入url地址,url地址上带上查询参数,如: http://localhost:8080/home?foo=1 或者 通过路由对象$router调用push()方法进行传参this.$router.push({path:"/home",query:{"foo":"1"}});然后通过this.$route.query.foo进行获取传递过来的参数

params: 通过路由对象$router调用push()方法进行传参this.$router.push({name:"home", params:{foo: 1}});然后通过this.$route.params.foo获取传递过来的参数

动态路由传参: 路由path位置为/home/:foo,然后输入url,如: http://localhost:8080/home/1 然后通过this.$route.params.foo获取传递过来的参数

5. v-on 常用修饰符?

.stop 该修饰符将阻止事件向上冒泡。同理于调用 event.stopPropagation() 方法,即如果当前元素添加了.stop修饰符,那么当点击该元素时候,click事件不会冒泡到其父元素上,即父元素不会触发click事件
.prevent 该修饰符会阻止当前事件的默认行为。同理于调用 event.preventDefault() 方法,即如果<a href="http://www.baidu.com" @click.prevent="show">连接,点击后默认会跳转到百度,但是添加上.prevent修饰符之后,就不会跳转到百度了而是执行show()方法了
.self 该指令只有当事件是从事件绑定的元素本身触发时才触发回调,即冒泡事件到达该元素上时并不会触发事件,但是其不影响事件继续向上冒泡其父元素仍然会触发冒泡事件
.native 就是给自定义组件的根元素添加一个原生事件,所以其通常用在自定义组件上,如果给普通的HTML元素添加.native修饰符那么该HTML元素将无法监听到该事件了
.capture 就是让事件监听变成捕获,默认为冒泡,通常用于修饰父元素,如果给父元素添加@click.capture修饰符,那么当点击子元素的时候,父元素的click事件将先触发,然后才是子元素的click事件。
.once 该修饰符表示绑定的事件只会被触发一次

6. 什么是动态组件?

简单的说,动态组件就是将几个组件放在一个挂载点下,这个挂载点就是<component>标签,其需要绑定is属性,属性值为父组件中的变量,变量对应的值为要挂载的组件的组件名,然后根据父组件里某个变量来动态显示哪个,也可以都不显示,如:
<template>
  <div class="home">
      <component :is="currentComponent"></component>
  </div>
</template>
<script>
import Tab0 from "@/components/Tab0.vue";
import Tab1 from "@/components/Tab1.vue";
export default {
    data: () => {
        return {
            currentIndex: 0 // 通过改变currentIndex改变要挂载的组件名
        }
    },
    components: {
        "tab-0": Tab0,
        "tab-1": Tab1
    }
    currentComponent() { // 动态计算要挂载的组件的组件名
      return `tab-${this.currentIndex}`; // "tab-0" 、"tab-1"
    }
}
</script>
可以将动态组件放到<keep-alive>组件内对动态组件进行缓存,这样动态组件进行切换的时候,就不会每次都重新创建了。
<template>
  <div class="home">
     <keep-alive>
          <component :is="currentComponent"></component>
     </keep-alive>
  </div>
</template>

7. 如何让CSS只在当前组件中起作用、原理以及如何穿透当前组件影响子组件?

将当前组件的<style>修改为<style **scoped**>

//foo.vue

<style scoped>
<!--其中的css样式只在foo.vue组件起作用-->
</style>

scoped的作用原理是,给当前组件内的所有标签都添加上一个data-v-唯一的属性id,同时给原来的样式末尾添加上属性选择器进行匹配,即在原来的样式基础上匹配的元素上还必须带有data-v-唯一的属性id属性,由于data-v-唯一的属性id这个属性只有在当前组件内的标签才有,所以样式只在当前组件内生效。

<template> 
    <button class="button">text</button> 
</template> 
<style scoped> 
    .button  {  border-raduis: 1px;  } 
</style>

编译后的代码将变成,如下:

<button class="button" data-v-7ba5bd90> text</button> 

.button[data-v-7ba5bd90]{ 
    border-raduis: 1px; 
}

意思就是不但要匹配带有class值为button的元素,同时这个元素还必须要有data-v-7ba5bd90属性

使用scoped的时候,要注意父组件内尽量不要使用标签选择器,或者说,子组件根元素上不要使用和父组件相同的id或class名称,因为编译后,子组件根元素上会带上父组件的data-v-唯一的属性id属性,那么子组件的根元素也会受到影响。如:

// 父组件内有一个ul标签
<ul class="container" data-v-7ba5bd90>
    父组件
</ul>
// 子组件渲染而来的ul标签,同时子组件根元素也使用了container相同的类名
<ul class="container" data-v-264bddce data-v-7ba5bd90>
     子组件
</ul>
// 父组件中使用了标签选择器ul,最终样式会变成如下 
ul[data-v-7ba5bd90] {...}
.container[data-v-7ba5bd90] {...}

根据父组件最终样式,我们可以看到,因为子组件根元素也会带上父组件的属性标识,所以子组件的根元素ul也会被匹配到,从而被父组件样式影响。
如果在加了scoped的情况下,如何穿透当前组件作用域影响其子组件样式呢?
我们可以通过 >>>穿透符来对scoped进行穿透,使用了穿透符后,其实就是把data-v-唯一的属性id标识符添加到了最前面,从而可以影响子元素,如:

// 父组件内的样式不使用穿透符后编译结果为 ul li {} 
ul li[data-v-7ba5bd90] {}

// 父组件内的样式使用穿透符后编译结果为 ul >>> li {} 
ul[data-v-7ba5bd90] li {}

可以看到,唯一标识被提前到ul上,所以子元素的根元素被匹配到,其中的li标签就会受到影响了。

由于scoped存在比较多坑,建议不要使用scoped,而是在组件根元素上添加一个唯一的class名来区分不同的组件,每个组件内的样式带上这个唯一的class名即可做到scoped的作用。

8. active-class是哪个组件的属性?

vue-router模块的router-link组件

9. Vue子组件调用父组件的方法?

子组件需要调用父组件的方法,那么可以通过this.$parent获取到父组件实例,然后就可以调用父组件上的方法了。还有一种方法就是,子组件向父组件emit一个事件父组件在子组件上监听到该事件后,就可以调用父组件上的方法了。

10. vue中的 ref 是什么?

ref 被用来给元素或子组件注册引用信息,引用信息将会注册在父组件的 $refs 对象上,即类似于给组件或者DOM元素上添加一个标识id,然后通过这个标识id拿到对应的DOM元素或组件实例,如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。
因为ref相当于是操作DOM,所以必须等待DOM加载好之后才能访问。虽然mounted钩子表示DOM已经加载完成,但是DOM的更新是一个异步的过程,为了以防万一,我们可以使用$nextTick函数,如:
mounted() {
    this.$nextTick(() => {
        console.log("可以在这里操作DOM了。");
    });
}

11. router和route的区别?

$route是"当前路由信息对象",包括path,params,hash,query,fullPath,matched,name等路由信息参数。
$router是通过Router构造函数创建的路由实例对象,包括了路由的跳转方法,全局路由钩子函数等。

12. 怎么定义组件?

全局定义:调用Vue的component()方法创建,Vue.component(组件名, {template: 模板字符串})
局部定义:在创建Vue实例时传递的options对象中的components对象中进行定义,components:{组件名: {template: 模板字符串}}
单文件组件:在.vue文件中定义,包含template,script,style三部分。

13. Vue-cli的src文件夹中有哪些文件?

assets文件夹是放静态资源
components是放组件
router是定义路由相关的配置;
view视图
app.vue是一个应用主组件
main.js是入口文件

14. 对于MVVM的理解?

MVVM 是 Model-View-ViewModel 的缩写。
Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。
View 代表UI 组件,它负责将数据模型转化成UI 展现出来。
ViewModel 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

15. 谈谈你对SPA单页面应用的理解及其优缺点?

SPA(Single-page application)仅在web页面初始化时加载相应的HTML、JS、CSS,一旦加载完成,SPA不会因为用户的操作而进行页面的重新加载,而是通过前端路由机制实现页面内容的切换

优点:

  • 用户体验性好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染问题;
  • 基于上面一点,SPA可以相对减轻服务器压力;
  • 前后端职责分离,架构清晰,前端进行页面渲染、交互逻辑处理,后端负责数据处理。

缺点:

  • 初次加载需要加载的内容多,导致初次加载耗时较多,容易出现白屏现象;
  • 浏览器前进后退功能失效,需要自行实现前进后退功能;
  • 不利于SEO优化,因为所有的内容都在一个页面上动态渲染出来的,Vue的单页面应用首页只有一个<div id="app"></div>,不过可以通过服务端渲染进行优化;
  • 不支持低版本的浏览器,最低只支持到IE9;

16. 路由的跳转方式?

① <router-link to='/home'> router-link标签会渲染为<a>标签,点击该a标签即可跳转到/home路由;
② 另一种是通过js跳转,即通过路由对象this.$router的push()方法进行跳转, 比如 router.push('/home')

17. 计算属性(computed)、方法(methods)和侦听属性(watch)的区别与使用场景?

methods VS 计算属性
我们可以将同一函数定义为一个 method 而不是一个计算属性。对于最终的结果,两种方式确实是相同的。
然而,不同的是计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。
相比而言,只要发生重新渲染,method 调用总会执行该函数。总之,重新计算开销很大的话请选计算属性,不希望有缓存的请选methods。
watch VS 计算属性
当你在模板内使用了复杂逻辑的表达式时,你应当使用计算属性。
侦听属性是一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。
当你有一些数据需要随着其它数据变动而变动时,或者当需要在数据变化时执行异步或开销较大的操作时,你可以使用 watch。

18. axios是什么?怎么使用?

axios是基于Promise的,用于浏览器和nodeJS的http客户端,主要作用就是向后台发送请求。其存在许多优点:
  • 支持Promise
  • 支持并发请求
  • 提供拦截器
  • 浏览器支持防止csrf(跨站请求伪造)

19. axios、fetch、ajax(jquery)的区别?

axios和fetch是基于Promise的,ajax是基于callback的形式。fetch脱离了xhr,是新的语法,默认是不传cookie的,监听不到请求进度

20. vuex是什么?哪种功能场景使用它?

vuex是一个专门为vue构建的状态机管理机制,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化,主要解决组件间数据共享的问题,其实就是采用类似全局对象的形式来管理所有组件的公共数据,其强调的是集中式管理,主要是为了便于维护、组件解耦,适合大型项目,有多个视图组件共同依赖一个状态的情况下使用,比如商城系统、外卖系统。
vuex的核心: stategettersmutationsactionsmodules

21. 说出4个vue当中常用的指令及其用法?

  • v-if: 这是一个条件渲染指令,代表存在和销毁,用于控制组件的创建与否;
  • v-bind: 这是一个绑定指令,用于绑定属性,可简写为冒号;
  • v-on: 这是一个监听指令,用于监听事件,可简写为@;
  • v-for: 这是一个循环指令,用于遍历数组;

22. 导航钩子有哪些?它们有哪些参数?

导航主要有三类钩子: 全局级路由钩子路由级路由钩子组件级路由钩子。主要参数有to(目标路由对象)、from(当前路由对象)、next(是一个函数,用于控制是否放行,即是否能通过当前守卫)。
  • 全局级路由钩子: beforeEach和afterEach,每次路由跳转全局路由钩子都会执行,beforeEach(to, from, next)钩子有三个参数,但是afterEach是已经跳转结束了,所以其没有next参数,afterEach(to, from),全局路由钩子由router对象调用
  • 路由级路由钩子: 路由级钩子只有一个即beforeEnter,其是在配置路由表的时候配置,也是有to、from、next三个参数,只有进入该路由的时候才会执行,如果是动态路由之间的切换,那么则不会触发beforeEnter钩子,因为是同一个路由,只是参数不一样
  • 组件级路由钩子: beforeRouteEnter(路由进入该组件的时候执行)、beforeRouteUpdate(动态路由切换时候执行)、beforeRouteLeave(路由离开当前组件的时候执行),需要注意的是,路由钩子是优先于组件的生命周期的,也就是说路由钩子全部执行完毕之后才会开始组件的生命周期,所以其钩子执行顺序: beforeRouteLeave --> beforeEach --> beforeEnter --> beforeRouteEnter --> afterEach --> beforeCreate --> created --> mountedbeforeRouteUpdate只有动态路由切换的时候才会执行,即/user/1切换到/user/2才会执行

23. v-model是什么?

v-model主要用于数据的双向绑定,其内部主要完成了两个操作: 通过v-bind绑定value属性值监听input事件,并更新数据,如:
<template>
    <!-- <input v-model="msg"/>{{msg}} -->
    <input v-bind:value="msg" @input="msg=$event.target.value"/>{{msg}} <!--二者是等价的-->
</template>
<script>
    export default {
        data: () => {
            return {
                msg: "hello"
            }
        }
    }
</script>

24. 什么是路由懒加载?其原理是什么?

所谓路由懒加载,即在项目打包的时候,项目中通常会有多个路由,如果将所有路由对应的组件都打包到一个文件中,那么最终打包的文件就会变得非常大,会影响页面的加载性能,如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才异步加载出对应组件,这样就会变得更加高效。
所以其原理就是利用了webpack的代码分割(按需加载)机制和vue的异步组件功能,代码被分割后就会变成一个单独的文件,所以路由被访问的时候需要向服务器发起请求加载该组件,这是一个异步的过程,所以需要使用到vue的异步组件机制。

异步组件?

异步组件,就是在注册组件的时候,传入一个函数,然后这个函数返回一个Promise对象,resolve的值为这个组件对象,如:
export default new Router({
    routes: [
        {
          path: '/about',
          name: 'about',
          component: () => { // 注册为一个异步组件
              const About = require("./views/About.vue");
              return Promise.resolve(About);
          }
        }
    ]
});

或者在注册异步组件的时候传入resolve和reject,如:

export default new Router({
    routes: [
        {
          path: '/about',
          name: 'about',
          component: (resolve, reject) => { // 注册为一个异步组件
              const About = require("./views/About.vue");
              resolve(About);
          }
        }
    ]
});

webpack提供的import()函数会返回一个Promise对象,并且会对引入的组件进行代码分割,所以可以通过import()同时实现代码分割和组件异步加载,即路由懒加载,如:

export default new Router({
    routes: [
        {
          path: '/about',
          name: 'about',
          component: () => import('./views/About.vue') // 等价于注册异步组件并返回一个Promise对象,分割代码的同时进行异步加载
        }
    ]
});

25. 用过插槽吗?用过哪些类型的插槽?

插槽其实就是组件中提供的占位符,所以插槽占位符在组件中使用,插槽有三种: 匿名插槽具名插槽作用域插槽
  • 匿名插槽:即没有名字的插槽,即<slot></slot>,使用组件的时候会将组件中的innerHTML插入到<slot></slot>位置上,相当于动态向组件内部传递数据。
// About.vue组件
<template>
  <div class="hello">
    <slot></slot>
  </div>
</template>

// 使用About组件
<About>
    <h1>hello world</h1><!--该内容会插入到上面<slot></slot>位置上,即替换掉<slot></slot>-->
</About>
  • 具名插槽: 即有名字的插槽,需要在<slot>标签上添加一个name属性,指定<slot>的名称,即<slot name="header"></slot>,同时使用组件的时候需要给其中的innerHTML添加slot属性,属性值为<slot>的name属性值,如:
// About.vue组件
<template>
  <div class="hello">
    <slot name="header"></slot> <!--定义slot的名称为header-->
  </div>
</template>

// 使用About组件
<About>
    <h1 slot="header">header</h1> <!--该内容会被插入到名称为header的slot上-->
</About>
  • 作用域插槽: 作用域插槽和普通插槽的区别就在于插槽的渲染位置不同

对于普通插槽,父组件渲染的时候,会先将组件的子节点,即其中的插槽渲染成虚拟节点并保存起来,当组件开始初始化的时候,就会取出之前保存起来的虚拟节点并进行替换,所以普通插槽是在父组件内渲染完成的
对于作用域插槽,父组件渲染的时候,会先将组件的子节点,即其中的插槽解析成函数不在父组件中渲染,等到子组件渲染的时候,会调用之前解析的函数在子组件中渲染,所以作用域插槽是在子组件内渲染完成。
所以作用域插槽也可以理解为组件向外输出数据,即插槽向父组件传递数据,而插槽传递的数据是子组件内的数据,因为其最终是在子组件内渲染的。所以又可以说是子组件向父组件传递数据,我们可以在组件的<slot>标签上添加上一些属性,然后其中的属性值可以传到组件外使用,会将slot标签上的所以属性合并到一个对象对外输出,组件外通过slot-scope指定一个变量名来接收这个对象,如:

// About.vue组件
<template>
  <div class="hello">
    <slot name="footer" msg="haha" foo="foo"></slot><!--会将其中的属性合并成一个对象对外输出-->
  </div>
</template>

// 使用About组件
<About>
    <h1 slot="footer" slot-scope="innerData">{{innerData.msg}} {{innerData.foo}}</h1><!--指定innerData变量接收组件slot标签中属性集对象(即接收组件内部的数据)-->
    <!--这里可以通过结构的方式比较方便-->
    <h1 slot="footer" slot-scope="{msg, foo}">{{msg}} {{foo}}</h1>
</About>

作用域插槽其实就是某个组件中的数据需要交给通过<slot>插槽传递进来的模板渲染,但是又不能直接在组件内把数据放到<slot>标签内渲染,因为slot代表的具体模板内容未知模板内容在外面,所以必须把数据通过插槽传递出去,外面的模板接收到数据后放到模板中即可。

26. 什么是vue-loader?

vue-loader就是.vue组件的加载器可以将.vue组件转换为javascript模块,及动态渲染一些数据,同时vue-loader还对.vue组件中的三个标签都进行了相应的优化。<template>标签中可以使用src属性引入一个组件,引入的组件可以直接使用当前组件中的数据,<script>标签中可以直接使用ES6语法<style>标签可以默认使用sass并且支持scoped作用域选择。如:

// foo.vue

<template>
    <h1>{{msg}}</h1><!--可以直接使用hello.vue中的数据-->
</template>

// hello.vue

<template src="./foo.vue"> <!--直接通过src引入foo.vue-->
  
</template>

<script>
export default {
  name: 'HelloWorld',
  data: () => {
    return {
      msg: "to foo.vue"
    }
  }
}
</script>
<style scoped>

</style>

27. Vue中keep-alive 的作用以及用法?

keep-alive是vue中一个内置的组件,主要用于缓存组件,其会在组件created的时候,将需要缓存的组件放到缓存中,然后再render的时候再根据name进行取出。<keep-alive>主要配合路由进行使用,在配置路由的时候添加上meta元数据对象,里面添加上keepAlive属性,表示是否缓存该组件,然后将<router-view>放到<keep-alive>中,router-view通过v-if指令,从路由配置上的meta对象中取出keepAlive的值进行判断是否需要缓存,如:
<template>
  <div id="app">
      <keep-alive>
        <router-view v-if="$route.meta.keepAlive"/> <!--这个是需要缓存的-->
      </keep-alive>
        <router-view v-if="!$route.meta.keepAlive"></router-view><!--这个是不需要缓存的-->
  </div>
</template>

// 路由配置

export default new Router({
    routes: [
        {
          path: '/',
          name: 'home',
          component: Home,
          meta: {
            keepAlive: true
          }
        },
        {
          path: '/about',
          name: 'about',
          component: About,
          meta: {
            keepAlive: false
          }
        }
    ]
});
组件缓存后就不会执行组件的beforeCreate、created和beforeMount、mounted钩子了,所以其提供了actived和deactived钩子,actived钩子主要用于承担原来created钩子中获取数据的任务

28. Vue 组件中 data 为什么必须是函数?

因为组件是可以多次复用的,也就是说会有多个组件实例同时存在,同时由于对象是引用数据类型,如果所有组件实例都共用一个data数据对象,那么一个组件对data数据对象进行修改,那么其他组件实例也会受到影响,所以需要使用函数返回data对象的独立拷贝,使得每个组件实例都会拥有自己的data数据对象,相互之间独立,不会互相受影响,便于组件维护。

29. Vue组件复用时,vue-router如何响应路由参数的变化?

当使用路由参数时,例如从 /user/lucy 导航到 /user/lily,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用,复用组件时,想对路由参数的变化作出响应的话,有两种方式:
  • 监听$route对象数据变化
export default {
  watch: {
    '$route': (to, from) =>{
      console.log("route change");// 对路由变化作出响应...

    }
  }
}
  • 通过beforeRouteUpdate路由钩子
export default {
  beforeRouteUpdate(to, from ,next) {
    console.log("beforeRouteUpdate hook run.");
    next();
  }
}

30. 简述Vue的响应式原理?

当一个Vue实例创建时,vue会遍历data选项的属性,用 Object.defineProperty 将它们转为 getter/setter并且在内部追踪相关依赖,在属性被访问和修改时通知变化。
每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

31. 怎么理解Vue的单向数据流?

Vue中父子组件之间可以通过prop进行传值,当父级组件数据更新会向下流动到子组件内,但反过来则不行,这样可以防止子组件意外改变父级组件状态,导致应用中的数据流向难于理解。子组件如果想要修改父组件中的数据,那么可以通过$emit派发一个自定义事件到父组件上,父组件监听到改变其数据,然后自动流到子组件内,子组件数据更新。

有两种常见的试图改变父组件数据的情形:

  • 子组件希望将父组件传递过来的数据作为子组件的初始数据进行使用,这种情况下,最好在子组件内定义一个data属性,将父组件传递过来的数据进行拷贝,然后对data中的数据进行操作,如:
props: ["initCounter"],
data() {
    return {
        counter: this.initCounter // 对父组件传递的数据进行拷贝
    }
}
  • 子组件希望对父组件传递过来的数据进行转换,这种情况下,最好在子组件中定义一个计算属性,用父组件的数据派生出一个新的状态数据,如:
props: ["size"],
computed: {
    normalizedSize() { // 用父组件数据派生出子组件的新状态数据
        return this.size.trim().toLowerCase();
    }
}

32. 对数组数据进行修改时,Vue存在什么缺陷?

Vue不能检测到以下数组的变动:

  • 当通过索引的方式直接修改某个数组项时,如: vm.items[index] = newValue;

对于这个问题,我们可以通过以下三种方式解决:

// ① 通过数组提供的方法进行修改数组数据
vm.items.splice(index, 1, newValue);

// ② 通过Vue实例提供的$set()方法进行修改
vm.$set(vm.items, index, newValue);

// ③ 通过Vue提供的set()方法进行修改
Vue.set(vm.items, index, newValue);
  • 当修改数组的长度的时候,如:vm.items.length = newLength;

对于这个问题,我们可以通过以下方式解决:

// 通过数组提供的splice()方法进行修改数组长度
vm.items.splice(newLength);

33. 在哪个生命周期内发起异步请求最好?

通常可以在createdbeforeMountmounted这三个钩子函数中发起异步请求,因为在这三个钩子中,data已经创建完成,我们可以将服务器返回的数据赋值保存到data中,但是建议最好在created钩子函数中发起异步请求,因为以下两点原因:

  • created在beforeMount和mounted这两个钩子之前执行,所以能更快获取到服务器端的数据,减少页面loading的时间;
  • ssr不支持beforeMount和mounted钩子函数,所以放到created中有利于保持一致性,切换到ssr的时候不需要修改了。

34. Vue的生命周期、触发时机以及每个钩子的用途?

Vue的生命周期是指Vue实例的一个完整生命周期,即从开始创建、初始化数据、编译模板、挂载渲染、更新、销毁的一系列过程,一个组件可以看做是Vue的实例。主要有以下几种:

  • beforeCreate: 此时组件实例对象已经创建出来了,但是数据还未初始化,即组件属性生效之前,此时可以获取到this对象为一个Vue实例对象,但是$data、$el、$methods都为undefined,所以此钩子通常用来设置一些页面渲染前的内容,比如loading加载

beforeCreate还可以用于vuex数据的持久化,因为页面刷新后vuex中的数据就会被丢失,所以我们需要从cookie或者localStorage中提取出来再提交到vuex中。

  • created: 此时组件实例对象数据初始化完毕,即$data、$methods等都已经有值了,此时$el因为真实DOM还未生成,故也无值,所以此钩子通常用来发送异步请求,异步获取的数据已经可以保存到data中了。
  • beforeMount: 即组件实例挂载到页面前,不能操作DOM,开发中比较少用;
  • mounted: 即组件实例挂载到页面后,可以操作DOM,动态修改数据了,所以此钩子通常用来操作DOM
  • beforeUpdateupdated: 这两个钩子触发的条件是,组件对应的页面中使用到的数据发生变化后,页面需要更新的时候才会触发,如果组件对应页面中并没有使用到组件实例中的数据,那么即使组件实例中的数据发生变化也不会触发这两个钩子。需要注意的是,这两个钩子中去获取数据的时候,获取的都是变化后的数据,只不过,beforeUpdate中数据还未更新到页面updated的时候数据已经更新到了页面中,所以beforeUpdate的时候还可以继续修改数据,而不会触发页面的重新渲染,但是在updated中继续修改数据的话,就会触发页面的重新渲染,即再次触发beforeUpdate和updated钩子
  • beforeDestroy: 其触发条件通常为,页面跳转的时候,即组件切换的时候,比如切换路由显示新的组件的时候,旧的组件就会被销毁,新的组件就会被创建,所以不用<keep-alive>的话,每次切换路由,对应组件都会被销毁。这个钩子通常用于清除一些定时器之类的。
  • destoryed: 组件销毁后

35. 父子组件生命周期钩子函数的执行顺序?

  • 父子组件加载渲染过程中
    父beforeCreate --> 父created --> 父beforeMount --> 子beforeCreate --> 子created --> 子beforeMount --> 子mounted --> 父mounted
  • 子组件更新(子组件页面中使用到了父组件中的数据)
    父beforeUpdate --> 子beforeUpdate --> 子updated --> 父updated
  • 父组件更新(子组件页面中未使用父组件中的数据)
    父beforeUpdate --> 父updated
  • 子组件更新(子组件页面中自己的数据更新)
    子beforeUpdate --> 子updated
  • 父子组件销毁过程
    父beforeDestory --> 子beforeDestory --> 子destoryed --> 父destoryed。

36. 父组件如何监听子组件生命周期?

比如父组件想在监听到子组件mounted之后做一些逻辑处理,我们可以有两种方式:

  • 在子组件mounted钩子中发射一个自定义事件,然后父组件通过子组件监听即可,如:
// Parent.vue
<Child @mounted="doChildMounted"/>

// Child.vue
mounted() {
    this.$emit("mounted");
}
  • 通过@hook进行钩子监听,如:
// Parent.vue
<Child @hook:mounted="doChildMounted"/>

37. 如何在mutation里面触发action?

mutation里面是用于同步修改state的值的,假如mutation里面修改好state后,需要发起一个异步的action,由于mutation里面拿不到dispatch,所以无法发起action,我们可以给实例化的vuex用一个变量保存起来,然后通过这个变量去发起action,如:

const store = new Vuex.Store({ // 将Store实例保存起来
    mutations: {
        syncSetAge(state, val) {
            state.age = val;
            store.dispatch("getName");
        }
    }
});

38. 如何实现路由的懒加载?

路由懒加载也叫延迟加载,即在需要的时候进行加载,Vue是单页面应用,如果没有应用懒加载,webpack打包后的文件将会异常的大,造成进入首页时,需要加载的内容过多,时间过长,会出啊先长时间的白屏,即使做了loading也是不利于用户体验,而懒加载则可以在切换路由的时候,再加载对应的组件
路由懒加载有两种方式,如:

  • 通过import语法,给component属性设置为一个函数,函数返回通过import()加载的组件,如:
// import About from "./components/About" // 必须移除该行代码,否则懒加载失败
{
    path: "/about",
    component: () => import("@/components/About")
}
  • 通过require语法,给component属性设置为一个函数,函数返回通过require加载的组件,如:
// import List from "./components/List" // 必须移除该行代码,否则懒加载失败
{
    path: "/list",
    component: resolve => require(["@/components/List"], resolve)
}

需要注意的时候,使用路由懒加载后必须移除原来通过import ... from 加载的组件,否则懒加载会失败。
不管哪种方式,component的属性值都是一个函数
使用路由懒加载的时候,会给每个懒加载的组件分割出一个js文件,文件名是数字从0开始进行标识,比如0.js、1.js,并且分割出来的js文件会通过<link rel="prefetch">标签的预加载功能在浏览器空闲的时候加载,但是在切换路由的时候才会被引入

39. 如何配置404页面

在vue中,如果访问到了不存在的路径,那么会显示一个空白页面,可以通过全局的beforeEach路由钩子进行拦截,然后根据目标路由的matched属性进行判断,matched属性值为一个数组,如果没有匹配到路由,那么其length就是0,我们可以直接将其跳转到404页面即可,如:

router.beforeEach((to, from, next) => {
    if (to.matched.length === 0) { // 如果目标路由未匹配到
      from.name ? next({ name:from.name }) : next('/404.html'); // 跳转到原页面或者首页
    } else {
      next();
    }
})

40. 什么是nextTick?什么时候用?

nextTick(),是将回调函数延迟在下一次dom更新数据后调用,简单的理解是:当数据更新了,在dom中渲染完成后,自动执行该回调函数。实际上是其内部维护了一个回调函数队列,当调用nextTick的时候会将传入的回调函数追加到nextTick队列中,然后开启一个timer定时器等同步代码执行完成后,开始清空队列,依次执行队列中的回调函数
使用场景如下:

  • 当你想在created()钩子函数进行的DOM操作,一定要放在Vue.nextTick()的回调函数中。 在created函数中调用Vue.nextTick之后会将回调函数追加到队列中,等到渲染等同步代码执行完成后,开始清空队列中的回调函数,所以能保证拿到的DOM是更新后的DOM。
  • 当你想在改变DOM元素的数据后基于新的dom做点什么,对新DOM一系列的js操作都需要放进Vue.nextTick()的回调函数中,当我们修改数据后,会通知渲染Watcher更新,然后将渲染Watcher放到Watcher的更新队列中,同时内部再调用nextTick函数将清空Watcher队列的函数放到nextTick的队列中,如果修改数据后接着调用Vue.nextTick就会将回调函数添加到nextTick队列中,因为nextTick队列中watcher更新在前面,所以nextTick清空队列的时候,就能保证拿到数据更新后的DOM。
methods : {
    changeData() {
        this.msg = "hello vue"; // 改变vue中的数据
        // 由于vue中数据发生改变,比如页面中有一个地方的数据依赖this.msg,此时如果想通过操作DOM获取到变化后的数据是获取不到的
        this.nextTick(() => {
            // 这里进行DOM操作
        });
    }
}

JS_Even_JS
2.6k 声望3.7k 粉丝

前端工程师