VUE前端面试题总结

简述Vue的响应式原理(Vue的双向数据绑定原理)

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

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
具体实现步骤,感兴趣的可以看看:

  • 当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 都加上 setter和getter 这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
  • compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
  • Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:1、在自身实例化时往属性订阅器(dep)里面添加自己;2、自身必须有一个update()方法;3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
  • MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

https://www.jianshu.com/p/c4dfc5806178

Vue-给对象新增属性

官方定义:

Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (root-level reactive property)。然而它可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上:

Vue.set(vm.obj, ‘e’, 0)
您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:

this.$set(this.obj,’e’,02)

有时你想向已有对象上添加一些属性,例如使用 Object.assign() 或 _.extend() 方法来添加属性。但是,添加到对象上的新属性不会触发更新。在这种情况下可以创建一个新的对象,让它包含原对象的属性和新的属性:

// 代替 Object.assign(this.obj, { a: 1, e: 2 })
this.obj= Object.assign({}, this.obj, { a: 1, e: 2 })

v-model语法糖

1
2
3
<input v-model="something">
等于
<input v-bind:value="something" v-on:input="something = $event.target.value">

v-model=”something”则表示将value值绑定在something上,当值发生改变时触发绑定的oninput事件。oninput事件绑定的函数是将触发oninput事件的目标(该input)的value值赋值给something这个变量。

v-model主要用于数据的双向绑定,其内部主要完成了两个操作: 通过v-bind绑定value属性值和监听input事件,并更新数据

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失效。

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建议用计算属性进行替代,返回过滤后的列表再进行遍历。

v-for循环中key作用

key的作用主要就是为了性能优化,key让组件具有了唯一性,能让diff算法更快的找到需要更新的组件dom,在绑定key的时候,最好是一个唯一的值,如 item.id 而不能是简单的index,如果不使用唯一key,那么在有状态组件中会出现渲染错误。因为它默认用就地复用策略,如果数据项的顺序被改变,那么vue将不是移动DOM元素来匹配数据项的改变,而是简单复用此处每个元素,不会重新排列元素的位置。如果是使用 key,它会基于key重新排列元素顺序,并且会移除 key 不存在的元素。
简单说就是,不使用key就会原地复用,使用key就会对元素位置进行重新排列,能够关联数据状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<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仍然保持勾选状态。

vue路由传参数的三种方式

① query: 直接输入url地址,url地址上带上查询参数,如: 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,如: localhost:8080/home/1 然后通过this.$route.params.foo获取传递过来的参数

v-on 常用修饰符

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

vue中的 ref

ref 被用来给元素或子组件注册引用信息,引用信息将会注册在父组件的 $refs 对象上,即类似于给组件或者DOM元素上添加一个标识id,然后通过这个标识id拿到对应的DOM元素或组件实例,如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。

$route$router的区别

$route是”路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。
$router是”路由实例”对象包括了路由的跳转方法,钩子函数等。

Vue定义组件

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

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

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

vue等单页面应用及其优缺点

优点:Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好。
缺点:不支持低版本的浏览器,最低只支持到IE9;不利于SEO的优化(Search Engine Optimization,搜索引擎优化)(如果要支持SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。

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

  • methods VS computed

    我们可以将同一函数定义为一个 method 而不是一个计算属性。对于最终的结果,两种方式确实是相同的。
    然而,不同的是计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。
    相比而言,只要发生重新渲染,method 调用总会执行该函数。总之,重新计算开销很大的话请选计算属性,不希望有缓存的请选methods。
  • watch VS computed

    当你在模板内使用了复杂逻辑的表达式时,你应当使用计算属性。
    侦听属性是一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。
    当你有一些数据需要随着其它数据变动而变动时,或者当需要在数据变化时执行异步或开销较大的操作时,你可以使用 watch。

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

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

视图通过点击事件,触发mutations中方法,可以更改state中的数据,一旦state数据发生更改,getters把数据反映到视图。

那么actions,可以理解处理异步,而单纯多加的一层。

既然提到了mutions actions这时候 就不得不提commit,dispatch这两个有什么作用呢?

在vue例子中,通过click事件,触发methods中的方法。当存在异步时,而在vuex中需要dispatch来触发actions中的方法,actions中的commit可以触发mutations中的方法。同步,则直接在组件中commit触发vuex中mutations中的方法。

vue当中常用的指令及其用法

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

路由懒加载

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

异步组件,就是在注册组件的时候,传入一个函数,然后这个函数返回一个Promise对象,resolve的值为这个组件对象,如:

1
2
3
4
5
6
7
8
9
10
11
12
export default new Router({
routes: [
{
path: '/about',
name: 'about',
component: () => { // 注册为一个异步组件
const About = require("./views/About.vue");
return Promise.resolve(About);
}
}
]
});

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

1
2
3
4
5
6
7
8
9
10
11
12
export default new Router({
routes: [
{
path: '/about',
name: 'about',
component: (resolve, reject) => { // 注册为一个异步组件
const About = require("./views/About.vue");
resolve(About);
}
}
]
});

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

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

插槽

插槽其实就是组件中提供的占位符,所以插槽占位符在组件中使用,插槽有三种: 匿名插槽、具名插槽、作用域插槽。

  • 匿名插槽:即没有名字的插槽,即<slot></slot>,使用组件的时候会将组件中的innerHTML插入到<slot></slot>位置上,相当于动态向组件内部传递数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 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属性值,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 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指定一个变量名来接收这个对象,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // About.vue组件
    <template>
    <div class="hello">
    <h1 slot="footer" slot-scope="innerData">{{innerData.msg}} {{innerData.foo}}</h1><!--指定innerData变量接收组件slot标签中属性集对象-->
    </div>
    </template>

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

请详细说下你对vue生命周期的理解

vue生命周期总共分为8个阶段:创建前/后,载入前/后,更新前/后,销毁前/后。

  • beforeCreate (创建前)vue实例的挂载元素$el和数据对象data都是undefined, 还未初始化
  • created (创建后) 完成了 data数据初始化, el还未初始化
  • beforeMount (载入前) vue实例的$el和data都初始化了, 相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。
  • mounted (载入后) 在el 被新创建的 vm.$el替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互
  • beforeUpdate (更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
  • updated (更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
  • beforeDestroy (销毁前) 在实例销毁之前调用。实例仍然完全可用。
  • destroyed (销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

+路由生命周期
https://segmentfault.com/a/1190000013956945#item-1-4

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

当使用路由参数时,例如从 /user/lucy 导航到 /user/lily,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用,复用组件时,想对路由参数的变化作出响应的话,有两种方式:

  • 监听$route对象数据变化

    1
    2
    3
    4
    5
    6
    7
    8
    export default {
    watch: {
    '$route': (to, from) =>{
    console.log("route change");// 对路由变化作出响应...

    }
    }
    }
  • 通过beforeRouteUpdate路由钩子

    1
    2
    3
    4
    5
    6
    export default {
    beforeRouteUpdate(to, from ,next) {
    console.log("beforeRouteUpdate hook run.");
    next();
    }
    }

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

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

当data定义为对象后,这就表示所有的组件实例共用了一份data数据,因此,无论在哪个组件实例中修改了data,都会影响到所有的组件实例。组件中的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份data,就会造成一个变了全都会变的结果。

vue-loader

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

vue-loader会解析文件,提取出每个语言块,如果有必要会通过其他loader处理,最后将他们组装成一个commonjs模块;module.exports出一个vue.js组件对象;

  1. < temlate>语言块
    (1)默认语言:html
    (2)每个.vue文件最多包含一个< template>块
    (3)内容将被提取为字符串,将编译用作VUE组件的template选项;
  2. < script>
    (1)默认语言:JS(在监测到babel-loader或者buble-loader配置时,自动支持ES2015)
    (2)每个.vue文件最多包含一个< script>块
    (3)该脚本在类CommonJS环境中执行(就像通过webpack打包的正常JS模块)。所以你可以require()其他依赖。在ES2015支持下,也可以使用import跟export语法
    (4)脚本必须导出Vue.js组件对象,也可以导出由VUE.extend()创建的扩展对象;但是普通对象是更好的选择;
  3. < style>
    (1)默认语言:css
    (2)一个.vue文件可以包含多个< style>标签
    (3)这个标签可以有 scoped 或者 module属性来帮助你讲样式封装到当前组件;具有不同封装模式的多个< style>标签可以在同一个组件中混合使用
    (4)默认情况下,可以使用style-loader提取内容,并且通过< style>标签动态假如文档的< head>中,也可以配置webpack将所有的styles提取到单个CSS文件中;
  4. 自定义块
    可以在.vue文件中添加额外的自定义块来实现项目的特殊需求;例如< docs>块;vue-loader将会使用标签名来查找对应的webpack loaders来应用到对应的模块上;webpack需要在vue-loader的选项loaders中指定;

vue-loader支持使用非默认语言,比如CSS预处理器,预编译的HTML模板语言,通过设置语言块的lang属性

Vue中keep-alive的作用以及用法

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

组件缓存后就不会执行组件的beforeCreate、created和beforeMount、mounted钩子了,所以其提供了actived和deactived钩子,actived钩子主要用于承担原来created钩子中获取数据的任务。

  • mounted钩子 在主页挂载时执行一次,如果没有缓存的话,再次回到主页时,mounted还会执行,从而导致ajax反复获取数据。

  • activated钩子 则不受缓存的影响,每次重新回到主页都会执行。

Vue.nextTick

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

nextTick的由来:
  由于VUE的数据驱动视图更新,是异步的,即修改数据的当下,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
nextTick的触发时机:
  在同一事件循环中的数据变化后,DOM完成更新,立即执行nextTick(callback)内的回调。
应用场景:
  需要在视图更新之后,基于新的视图进行操作。
以上出现了事件循环的概念,其涉及到JS的运行机制,包括主线程的执行栈、异步队列、异步API、事件循环的协作,此处不展开之后再总结。大致理解:主线程完成同步环境执行,查询任务队列,提取队首的任务,放入主线程中执行;执行完毕,再重复该操作,该过程称为事件循环。而主线程的每次读取任务队列操作,是一个事件循环的开始。异步callback不可能处在同一事件循环中。

https://www.cnblogs.com/hity-tt/p/6729118.html

Vue 父子组件通信方式

  1. 通过prop向子组件传递数据,子组件向父组件传递数值$emit
  2. Vuex

Vue 路由的实现 hash模式 和 history模式

hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;
特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如http://www.xxx.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。

history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。
history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如http://www.xxx.com/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

https://www.jianshu.com/p/bfffb4b8c9fa

坚持原创技术分享,您的支持将鼓励我继续创作!