目录
46、vue-router中常用的hash和history路由模式实现原理?
50、Proxy 与 Object.defineProperty 优劣对比
56、全局指令:directive、extent、set、component
59、dependencies和devDependencies的区别
以下为个人面试经验问题总结,欢迎评论补充会持续收集问题更新~
1、请讲述下vue的MVVM的理解?
MVVM是Model-View-ViewModel的缩写,即将数据模型与数据表现层通过数据驱动进行分离,从而只需要关系数据模型的开发而不需要考虑页面的表现,具体来说如下:
Model代表数据模型:主要用于定义数据和操作的业务逻辑
View代表页面展示组件(即dom展现形式):负责将数据模型转化成UI展现出来
ViewModel为model和view之间的桥梁:监听模型数据的改变和控制视图行为、处理用户交互。通过双向绑定把view层和model层连接起来,而view和model之间的同步工作完全是自动的,无需认为干涉。
在MVVM架构下view和model之间并没有直接的练习,而是通过ViewModel进行交互,Model和ViewModel之间的交互是双向的,因此View数据的变化会同步到Model中,而Model数据的变化也会立即反映到View上。
2、Vue的生命周期及理解?
答:8个阶段。具体为:创建前/后,载入前/后,更新前/后,销毁前/后
创建前/后:
- 在beforeCreated阶段:vue实例的挂载元素$el和数据对象data都为undefined,还未初始化
- 在created阶段:vue实例的数据对象data有了,$el还没有
载入前/后:
- 在beforeMounted阶段:vue实例的$el和data都初始化了,但还是挂载之前为虚拟dom节点,data.message还未替换;
- 在mounted阶段:vue实例挂载完成,data.message成功渲染
更新前/后:
- 当data变化时,会触发beforeUpdate和updated方法
销毁前/后:
- 在执行destroy方法之后,对data的改变会不触发周期函数,说明此时vue实例已经解除了时间监听以及和dom的绑定,但是dom结构依然存在。
具体讲解及应用
beforeCreated:在new一个vue实例后,只有一些默认的生命周期钩子和默认事件,其他的东西都还没创建,data和methods中的数据都还没有初始化。不能再这个阶段使用data中的数据和methods中的方法
create:data和methods都已经被初始化好了,如果要调用methods中的方法,或者操作data中的数据,最早可以在这个阶段中操作
beforeMounted:执行到这个钩子的时候,在内存中已经编译好了模板了,但是还没有挂载到页面中,此时,页面还是旧的,不能直接操作页面的dom和获取dom对象
mounted:执行到这个钩子的时候,就表示Vue实例已经初始化完成了。此时组件脱离了创建阶段,进行到了运行阶段。如果我们想要通过插件操作页面上的dom节点,最早可以在这个阶段中进行。
beforeUpdate:当执行这个钩子时,页面中的显示的数据还是旧的,data中的数据是更新后的,页面还没有和最新的数据保持同步
updated:页面显示的数据和data中的数据已经保持同步了,都是最新的。
beforeDestroyed:Vue实例从运行阶段进入到了销毁阶段,这个时候上所有的data和methods、指令、过滤器都处于可用状态,还没有真正被销毁
destroyed:这个时候所有的data和methods、指令、过滤器......都是处于不可用状态。组件已经被销毁了。
3、v-if和v-show的区别?
共同点:都能控制元素的显隐
不同点:实现本质方法不同,v-show本质就是通过控制css中的display设置为none,控制隐藏,只会编译一次;v-if是动态的向dom树内添加或者删除DOM元素,初始值为false,就不会编译了。而且v-if不停的销毁和创建比较消耗性能。
如果要频繁切换某节点,使用v-show(切换开销比较小,初始开销较大)。如果不需要频繁切换某节点使用v-if(初始渲染开销较小,切换开销比较大)。
4、v-if和v-for同时使用在同一个标签上的表现?
当v-if和v-for一起使用时,v-for具有比v-if更高的优先级,这意味着v-if将分别重复运行于每个v-for循环中。所以不推荐v-if和v-for同时使用。如果v-if和v-for一起用的话,vue中会自动提示v-if应该放到外层去。
5、v-for中的key的理解?
需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确识别此节点。主要是为了高效的更新虚拟DOM
6、vue中transition的理解?
1)定义transition时设置对应的name,具体语法为:
<transition name="fade">需要动画的内容或者组件或者页面</transition>
2)过渡动画主要包含6个class,分别为:
- v-enter:定义元素进入过渡的初始状态,在元素插入前生效,插入后一帧删除,
- v-enter-active:在元素插入前生效,在动画完成后删除,
- v-leave:离开过渡的初始状态,在元素离开时生效,下一帧删除
- v-leave-active:在离开过渡时生效,在动画完成后删除
- v-leave-to:离开过渡结束状态,在离开过渡下一帧生效,在动画完成后删除
⚠️:v会转化为对应的transition的name值
3)当然我们也可以自定义这六个class可以直接在transition中设置对应的属性为对应的class名称,属性有:enter-class,enter-active-class,enter-to-class,leave-class,leave-active-class,leave-to-class
4)在同时使用过渡和css动画的时候 可以设置type属性来制定vue内部机制监听transitioned或者animationed时间来完成过渡还是动画的监听
5)如果需要设置对应的过渡时间,可以直接设置属性duration,可以直接接收一个数字(单位为毫秒),也可以接收一个对象{enter:1000,leave:300}
6)也可以设置过渡的钩子函数,具体有:before-enter,enter,after-enter,enter-cancelled,before-leave,leave,after-leave,leave-cancelled
7、vue的自定义指令?
自定义指令分为全局指令和组件指令,其中全局指令需要使用directive来进行定义,组件指令需要使用directives来进行定义,具体定义方法同过滤器filter或者其他生命周期,具体使用方法如下:
全局自定义指令directive(name,{}),其中name表示定义的指令名称(定义指令的时候不需要带v-,但是在调用的时候需要带v-),第二个参数是一个对象,对象中包括五个自定义组件的钩子函数,具体包括:
1.bind函数:只调用一次,指令第一次绑定在元素上调用,即初始化调用一次
2.inserted函数:绑定元素插入父级元素(即new vue中el绑定的元素)时调用(此时父级元素不一定转化为了dom)
3.updated函数:在元素发生更新时就会调用,可以通过比较新旧的值来进行逻辑处理
4.componentUpdated函数:元素更新完成后触发一次
5.unbind函数:在元素所在的模板删除的时候就触发一次
钩子函数对应的参数el,binding,vnode,oldnode,具体参数讲解如下:
a、el指令所绑定的元素 可以直接操组dom元素
b、binding一个对象,具体包括以下属性:
1)name:定义的指令名称 不包括v-
2)value:指令的绑定值,如果绑定的是一个计算式,value为对应计算结果
3)oldvalue:指令绑定元素的前一个值,只对update和componentUpdated钩子函数 有值
4)expression:指令绑定的原始值 不对值进行任何加工
5)arg:传递给指令的参数
6)modifiers:指令修饰符,如:v-focus.show.async 则接收的modifiers为{show:true,async:true}
c、vnode:vue编译生成的虚拟dom
d、oldVnode:上一个vnode,只在update和componentUpdated钩子函数中有效
⚠️:如果不需要其他钩子函数,可以直接简写为:directive(“focus”,function(el,binding){})
8、vue的实现原理?
vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
具体步骤:
第一步:需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter
这样的话,给这个对象的某个值赋值,就会触发setter,name就能监听到了数据变化
第二步:compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
第三步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
第四步:MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者3人,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer额Compile之间的通信桥梁,达到数据变化-》视图更新;视图交互变化(input)-》数据model变更的双向绑定效果
9、vue的diff算法理解?
1)diff算法的作用:用来修改dom的一小段,不会引起dom树的重绘
2)diff算法的实现原理:diffsuanfa 将 virtual dom的某个节点数据改变后生成的新的vnode与旧节点进行比较,并替换为新的节点,具体过程就是调用patch方法,比较新旧节点,一边比较一边给真实的dom打补丁进行替换
3)具体过程详解:
- 在采用diff算法进行新旧节点进行比较的时候,比较是按照在同级进行比较的,不会进行跨级比较:
- 当三个月后月发生改变的时候,set方法会调用dep.notify通知所有的订阅者watcher,订阅者会调用patch函数给响应的dom进行打补丁,从而更新真实的视图
- patch函数接收两个参数,第一个是旧节点,第二个是新节点,首先判断两个节点是否值的比较,值的比较则执行patchVnode函数,不值得比较则直接将旧节点替换为新节点。如果两个节点一样,就直接检查对应的子节点,如果子节点不一样就说明整个子节点全部改变,不再往下对比直接进行新旧节点的整体替换
- patchVnode函数:找到真实的dom元素;判断新旧节点是否指向同一个对象,如果是就直接返回;如果新旧节点都有文本节点,那么直接将新的文本节点赋值给dom元素并且更新旧的节点为新的节点;如果旧节点有子节点而新节点没有,则直接删除dom元素中的子节点;如果旧节点没有子节点,新节点有子节点,那么直接将新节点中的子节点更新到dom中;如果两者都有子节点,那么继续调用函数updateChildren
- updateChildren函数:抽离出新旧节点的所有子节点,并且设置新旧节点的开始指针和结束指针,人后进行两辆比较,从而更新dom(调整顺序或者插入新的内容 结束后删掉多余的内容)
10、vue组件的通信(父子组件和非父子组件)
父子通信:
- 父传子:props
- 子传父:$emit
- 父链子链:$parent/$children
- 组件实例:ref
兄弟:vuex、eventBus
跨级通信:eventBus、provide/inject、$attrs/$listenters
父子组件通信
传递参数可以用props,传递函数可以直接在调用子组件的时候传递自定义事件,并使用$emit来调用,例如:
//父组件
<div classs="parent">
<child @getinfo="myname" :userinfo="usermessage"></child>
<div>
export default {
data(){
return {
usermessage:'我是父亲'
}
},
methods:{
myname(name){
console.log('我的名字叫'+name)
}
}
}
//子组件
<div classs="child">
来源:{{userinfo}}
<button @click="getname">显示我的名字</button>
<div>
export default {
props:['userinfo'],
methods:{
getname(){
this.$emit('getinfo','bilibili')
}
}
}
兄弟组件通信
首先建立一个vue实例空白页(js文件)
import Vue from 'vue'
export default new Vue()
组件a(数据发送方)通过使用 $emit 自定义事件把数据带过去
<template>
<div>
<span>A组件->{{msg}}</span>
<input type="button" value="把a组件数据传给b" @click ="send">
</div>
</template>
<script>
import vmson from "../../../util/emptyVue"
export default {
data(){
return {
msg:{
a:'111',
b:'222'
}
}
},
methods:{
send:function(){
vmson.$emit("aevent",this.msg)
}
}
}
</script>
组件b(数据接收方)使用而通过 $on监听自定义事件的callback接收数据
<template>
<div>
<span>b组件,a传的的数据为->{{msg}}</span>
</div>
</template>
<script>
import vmson from "../../../util/emptyVue"
export default {
data(){
return {
msg:""
}
},
mounted(){
vmson.$on("aevent",(val)=>{//监听事件aevent,回调函数要使用箭头函数;
console.log(val);//打印结果:我是a组件的数据
this.msg = val;
})
}
}
</script>
11、vue的路由模式及区别?
Hash:
hash模式在浏览器中符号:“#”,#一级#后面的字符称之为hash,用window.location.hash读取;
特点:hash虽然在URL中,但不包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重新加载页面
History
history模式:history采用HTML5的新特性;
提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。history模式下,前端的URL必须和实际向后端发起请求的URL一致,否则会报404错误
12、vue与react、angular的比较?
Vue
轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十kb;
简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
双向数据绑定:保留了angular的特点,在数据操作方面更为简单;
组件化:保留了react的优点,实现了html的封装和重用,在构建单页面应用方面有着独特的优势;
视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
虚拟DOM:dom操作是非常耗费性能的, 不再使用原生的dom操作节点,极大解放dom操作,但具体操作的还是dom不过是换了另一种方式;
运行速度更快:相比较与react而言,同样是操作虚拟dom,就性能而言,vue存在很大的优势。
React
相同点:
React采用特殊的JSX语法,Vue.js在组件开发中也推崇编写.vue特殊文件格式,对文件内容都有一些约定,两者都需要编译后使用;中心思想相同:一切都是组件,组件实例之间可以嵌套;都提供合理的钩子函数,可以让开发者定制化地去处理需求;都不内置列数AJAX,Route等功能到核心包,而是以插件的方式加载;在组件开发中都支持mixins的特性。
不同点:
React采用的Virtual DOM会对渲染出来的结果做脏检查;Vue.js在模板中提供了指令,过滤器等,可以非常方便,快捷地操作Virtual DOM。
Angular
相同点:
都支持指令:内置指令和自定义指令;都支持过滤器:内置过滤器和自定义过滤器;都支持双向数据绑定;都不支持低端浏览器。
不同点:
AngularJS的学习成本高,比如增加了Dependency Injection特性,而Vue.js本身提供的API都比较简单、直观;在性能上,AngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。
13、vue-router的钩子函数?
vue路由钩子大致分为三类:
1)全局钩子:
主要包括beforeEach和afterEach,beforeEach函数有三个参数:
to:router即将进入的路由对象
from:当前导航即将离开的路由
next:Function,进行管道中的一个钩子,如果执行完了,则导航的状态就是confirmed(确认的);否则是false,终止导航。afterEatch函数不用穿next()函数这类钩子主要用于全局,一般用来判断权限,以及页面丢失时候需要执行的操作,例如:
//使用钩子函数对路由进行权限跳转
router.beforeEach((to, from, next) => {
const role = localStorage.getItem('ms_username');
if(!role && to.path !== '/login'){
next('/login');
}else if(to.meta.permission){
// 如果是管理员权限则可进入,这里只是简单的模拟管理员权限而已
role === 'admin' ? next() : next('/403');
}else{
// 简单的判断IE10及以下不进入富文本编辑器,该组件不兼容
if(navigator.userAgent.indexOf('MSIE') > -1 && to.path === '/editor'){
Vue.prototype.$alert('vue-quill-editor组件不兼容IE10及以下浏览器,
请使用更高版本的浏览器查看', '浏览器不兼容通知', {
confirmButtonText: '确定'
});
}else{
next();
}
}
2)单个路由里面的钩子
主要用于写某个指定路由跳转时需要执行的逻辑
3)组件路由
主要包括beforeRouteEnter和beforeRouteUpdate,beforeRouteLeave,这几个钩子都是写在组件里面也可以传三个参数(to,from,next),作用与前面类似.
beforeRouteEnter(to, from, next) {
next(vm => {
if (
vm.$route.meta.hasOwnProperty('auth_key') &&
vm.$route.meta.auth_key != ''
) {
if (!vm.hasPermission(vm.$route.meta.auth_key)) {
vm.$router.replace('/admin/noPermission')
}
}
})
}
14、vuex的使用
vuex是一个专为Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化,具体包括:
1)state:vuex使用单一状态树,即每个应用将仅仅包含一个store实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据
2)getter:state的计算属性,类似vue的计算属性,主要用来过滤一些数据
3)action:actions可以理解为通过将mutations里面处理数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view层通过store.dispath来分发action。可以异步函数调用
4)mutation:mutations定义的方法动态修改vuex的store中的状态或数据
5)modules:项目特别复杂的时候,可以让每一个模块拥有自己的state、mutatuin、action、getters,使得结构非常清晰,方便管理。
15、vue的filter的理解与用法?
1)全局过滤器必须写在vue实例创建之前
Vue.filter('testfilter', function (value,text) { // 返回处理后的值
return value+text
})
2)局部写法:在组件实例对象里挂载
filters: {
changemsg:(val,text)=>{ return val + text
}
}
3)使用方式:只能使用在{undefined{}}和:v-bind中,定义时第一个参数固定为预处理的树,后面的数为调用时传入的参数,电泳时参数第一个对应定义时第二个参数,依次往后类推
<h3 :title="test|changemsg(1234)">{{test|changemsg(4567)}}</h3>
//多个过滤器也可以串行使用
<h2>{{name|filter1|filter2|filter3}}</h2>
4)vue-cli项目中注册多个全局过滤器写法:
//1.创建一个单独的文件定义并暴露函数对象
const filter1 = function (val) {
return val + '--1'
}
const filter2 = function (val) {
return val + '--2'
}
const filter3 = function (val) {
return val + '--3'
}
export default {
filter1,
filter2,
filter3
}
//2.导入main.js(在vue实例之前)
import filters from './filter/filter.js'
//3.循环注册过滤器
Object.keys(filters).forEach(key=>{
Vue.filter(key,filters[key])
})
<template>
<div class="test">
<div :id="rawId">{{rawId}}</div>
<!-- 以下v-bind可省略,即v-bind:id可简写为:id -->
<div v-bind:id="rawId|filter_formatId">{{rawId|filter_formatId}}</div>
</div>
</template>
<script>
export default {
data() {
return {
rawId: 1
};
},
filters: {
filter_formatId(value) {
return value * 10;
}
}
};
</script>
全局过滤器、局部过滤器
全局过滤器:
在main.js中写入:
Vue.filter('filer_name',function(value){
return '$'+value
})
局部过滤器:
在vue示例中写入:
<template>
<div class="test">
<p>{{price}}</p>
<p>{{price | filter_addPricePrefix}}</p>
</div>
</template>
<script>
export default {
data() {
return {
price: 100
};
},
filters: {
filter_addPricePrefix(value) {
return "¥" + value;
}
}
};
</script>
过滤器中传入多个参数:
过滤器是js函数,因此可以接收参数:
{{ message | filterA('arg1', arg2) }}
这里filterA被定义为接收三个参数的过滤器函数,其中message的值为第一个参数,普通字符串‘arg1’作为第二个参数,表达式arg2的值作为第三个参数
<template>
<div class="test">
<!-- 要过滤的数据,永远是第一个参数;通过filter函数,传递的参数,依次排在后面。 -->
<p>{{ new Date() | filter_dateFormat }}</p>
<p>{{ new Date() | filter_dateFormat('YYYY-MM-DD') }}</p>
<p>{{ new Date() | filter_dateFormat('YYYY-MM-DD', count) }}</p>
</div>
</template>
<script src="https://cdn.bootcss.com/moment.js/2.24.0/moment.js"></script>
<script>
export default {
data() {
return {
count: 10
};
},
filters: {
filter_dateFormat(date, format, count) {
return (
moment(date).format(format || "YYYY-MM-DD HH:mm:ss") +
(count ? " -- " + count : "")
);
}
}
};
</script>
16、vue的keep-alive的理解?
keep-alive是vue内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染,页面第一次进入,钩子的触发顺序:created>mounted>activated,退出时触发deactivated,当再次进入(前进或者后退)时,只触发activated事件挂载的方法等,只执行一次的放在mounted中零组件每次进去执行的方法放在activated中;其中有几个属性如下:
1)include -字符串或正则表达式,只有名称匹配的组件会被缓存(注意是组件的名字,不是路由的名字)
2)exclude - 字符串或正则表达式,任何名称匹配的组件都不会被缓存(exclude属性表示除了name属性为xxx的组件不会被缓存,其它组件都会被缓存)
3)include 和 exclude 的属性允许组件有条件地缓存。二者都可以用“,”分隔字符串、正则表达式、数组。当使用正则或者是数组时,要记得使用v-bind 。
<!-- 逗号分隔字符串,只有组件a与b被缓存。-->
<keep-alive include="a,b">
<component></component>
</keep-alive>
<!-- 正则表达式 (需要使用 v-bind,符合匹配规则的都会被缓存) -->
<keep-alive :include="/a|b/">
<component></component>
</keep-alive>
<!-- Array (需要使用 v-bind,被包含的都会被缓存) -->
<keep-alive :include="['a', 'b']">
<component></component>
</keep-alive>
17、如何封装一个vue组件?
根据业务需求,建立组件的模板,先把架子搭起来,写样式,考虑好组件的基本逻辑
准备好组件的数据输入。即分析好逻辑,定好 props 里面的数据、类型。
准备好组件的数据输出。即根据组件逻辑,做好要暴露出来的方法。
封装完毕了,直接调用即可
18、vue首屏白屏如何解决?
1)路由懒加载
2)vue-cli开启打包压缩和后台配合gzip访问
3)进行cdn加速
4)开启vue服务渲染模式
5)用webpack的externals属性把不需要打包的库文件分离出去,减少打包后文件的大小
6)在生产环境中删除掉不必要的console.log
plugins: [
new webpack.optimize.UglifyJsPlugin({ //添加-删除console.log
compress: {
warnings: false,
drop_debugger: true,
drop_console: true
},
sourceMap: true
}),
7)开启nginx的gzip,在nginx.conf配置文件中配置
http { //在 http中配置如下代码,
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 8; #压缩级别
gzip_buffers 16 8k;
#gzip_http_version 1.1;
gzip_min_length 100; #不压缩临界值
gzip_types text/plain application/javascript application/x-javascript text/css
application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
}
8)添加loading效果,给用户一种进度感受
19、vue中的v-cloak的理解?
使用v-cloak指令设置样式,这些样式会在vue实例编译结束时,从绑定的HTML元素上呗移除。
一般用于解决网页闪屏的问题,在对一个标签中使用v-cloak,然后在样式中设置[v-cloak]样式,[v-cloak]需要卸载link引入的css中,或者写一个内联css样式,写在import引入的css中不起作用。
20、vue中template编译的理解?
先转化成AST树,再得到的render函数返回VNode(vue的虚拟DOM节点),具体为:
首先,通过complie编译器把templete编译成AST语法树(abstract syntax tree即源代码的抽象语法结构的树状表现形式),complie是createComlpier的返回值,createComplier是用以创建编译器的。
另外complie还负责合并option。
然后,AST会经过generate(将AST语法树转化成render functuib字符串的过程),得到render函数,render的返回值是VNode,VNode是vue的虚拟DOM节点,里面有(标签名、子节点、文本等等)
21、v-model的理解?
v-model用于表单数据的双向绑定,其实他就是一个语法糖,这个背后就做了两个操作:
1)v-bind绑定一个value属性
2)v-on指令给当前元素绑定input事件
22、computed和watch的用法和区别?
computed
1)变量不在data中定义,而是定义在computed中,写法跟写方法一样,有返回值。函数名直接在页面模板中渲染,不加小括号
2)根据传入的变量的变化 进行结果的更新
3)计算属性基于响应式依赖进行缓存。如果其中的任意一个值未发生变化,它调用的就是上一次计算缓存的数据,因此提高了程序的性能。而methods中每调用一次就会重新计算一次,为了进行不必要的资源消耗,选择用计算属性
watch
1)计算属性初始化的时候就可以被监听到并且计算,但是watch是发生改变的时候才会触发
2)当有一些数据是需要随着其它数据变动而变动时,或者当需要在数据变化时之执行异步或者开销较大的操作时,使用watch
总结:
1)计算属性变量在computed中定义,属性监听在data中定义
2)计算属性是声明式的描述一个值依赖了其他值,依赖的值改变后重新计算结果更新DOM。属性监听的是定义的变量,当定义的值发生变化时,执行相应的函数。
3)当我们需要进行述职计算,并且依赖于其他数据时,应该使用computed,因为可以利用computed的缓存特性,避免每次获取值是,都要重新计算;
4)当我们需要在数据变化时执行异步或开销较大的操作时,应该使用watch,使用watch选项允许我们执行异步操作(访问一个API),限制我们执行该操作的频率,并在我们得到最终结果钱,设置中间状态。这些都是计算属性无法做到的。
23、$nextTick的使用?
在vue中理解修改数据后,对应的dom需要一定的时间进行更新,因此为了能够准确得到更新后的dom,可以采用延迟回调的方法进行更新dom的获取,所以出现了$nextTick来进行延迟回调。即在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。
24、data为什么是一个函数?
这是有JavaScript的特性所导致,在component中,data必须以函数的形式存在,不可以是对象。
组建中的data写成一个函数,数据以函数返回值的形式定义,这样每次复用组件的时候,都会返回一份新的data,相当于每个组件实例都有自己私有的数据空间,它们只负责各自维护的数据,不会造成混乱。而单纯的写成对象形式,就是所有的组件实例共用了一个data,这样改一个全都改了
因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
25、vue单页面和传统的多页面区别?
单页面应用(SPA)
通俗一点说就是只有一个主页面的应用,浏览器一开始要加载所有的html,js,css。所有的页面内容都包含在这个所谓的主页面中。但在写的时候,还是会分开写(页面片段),然后在交互的时候由路由程序动态载入,单页面的页面跳转,仅刷新局部资源。多用于pc端。
多页面
指一个应用中有多个页面,页面跳转时是整页刷新
单页面的优点:
用户体验好,快,内容的改变不需要重新加载整个页面,基于这一点spa对服务器压力较小;前后端分离;页面效果会比较炫酷(比如切换页面内容时的专场动画)。
单页面缺点:
不利于seo;导航不可用,如果一定要导航需要自行实现前进、后退。(由于是单页面不能用浏览器的前进后退功能,所以需要自己建立堆栈管理);初次加载时耗时多;页面复杂度提高很多。
26、vue常用的修饰符?
.stop:等同于JavaScript中的event.stopPropagation(),防止事件冒泡;
.prevent:等同于JavaScript中的event.preventDefault(),防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);
.capture:与事件冒泡的方向相反,事件捕获由外到内;
.self:只会触发自己范围内的事件,不包含子元素;
.once:只会触发一次。
对于elementui的input,需要在后面加上.native,因为element对input进行了封装,原生的事件不起作用
<input v-model="form.name" placeholder="昵称" @keyup.enter="submit">
<el-input v-model="form.name" placeholder="昵称" @keyup.enter.native="submit"></el-input>
27、vue更新数组时触发视图更新的方法?
答:push();pop();shift();unshift();splice();sort();reverse()
28、route和router的区别
$router
router是VueRouter的一个对象,通过Vue.use(VueRouter)和VueRouter构造函数得到一个router的实例对象,这个对象中是一个全局的对象,他包含了所有的路由包含了许多关键的对象和属性,常见的有:
1)push:向 history 栈添加一个新的记录,当我们点击浏览器的返回按钮时可以看到之前的页面
// 字符串
this.$router.push('home')
// 对象
this.$router.push({ path: 'home' })
// 命名的路由
this.$router.push({ name: 'user', params: { userId: 123 }})
// 带查询参数,变成 /register?plan=123
this.$router.push({ path: 'register', query: { plan: '123' }})
2)go:页面路由跳转 前进或者后退
// 页面路由跳转 前进或者后退
this.$router.go(-1) // 后退
// 页面路由跳转 前进或者后退
this.$router.go(1) // 前进
3)replace:
push方法会向history栈添加一个新的记录,而replace方法是替换当前的页面,不会向history栈添加一个新的记录
$route
$route对象表示当前的路由信息,包含了当前URL解析得到的信息。包含当前的路径、参数、query对象等
1)$route.path:字符串,对应当前路由的路径,总是解析为绝对路径,如“/foo/bar”
2) $route.params:一个key/value对象,包含了动态片段和全匹配片段,如果没有路由参数,就是一个空对象。
3)$route.query:一个key/value对象,表示URL查询参数。例如,对于路径/foo?user=1,则有$route.query.user == 1,如果没有查询参数,则是一个空对象。
4)$route.hash:当前路由的hash值(不带#),如果没有hash值,则为空字符串。
5)$route.fullPath:完成解析后的URL,包含查询参数和hash的完整路径。
6)$route.matched:数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象
7)$route.name:当前路径名字
8)$route.meta:路由元信息
29、vue-router实现懒加载的方式?
vue异步组件
vue异步组件技术====异步加载
vue-router配置路由,使用vue的异步组件技术,可以实现按需加载。但是,这种情况下一个组件生成一个js文件
/* vue异步组件技术 */
{
path: '/home',
name: 'home',
component: resolve => require(['@/components/home'],resolve)
},{
path: '/index',
name: 'Index',
component: resolve => require(['@/components/index'],resolve)
},{
path: '/about',
name: 'about',
component: resolve => require(['@/components/about'],resolve)
}
es提案的import()
- 推荐使用这种方式(需要webpack>2.4)
// 下面2行代码,没有指定webpackChunkName,每个组件打包成一个js文件。
/* const Home = () => import('@/components/home')
const Index = () => import('@/components/index')
const About = () => import('@/components/about') */
// 下面2行代码,指定了相同的webpackChunkName,会合并打包成一个js文件。把组件按组分块
const Home = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/home')
const Index = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/index')
const About = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/about')
{
path: '/about',
component: About
}, {
path: '/index',
component: Index
}, {
path: '/home',
component: Home
}
webpack的require,ensure()
vue-router配置路由,使用webpack的require.ensure技术,也可以实现按需加载。这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
/* 组件懒加载方案三: webpack提供的require.ensure() */
{
path: '/home',
name: 'home',
component: r => require.ensure([], () => r(require('@/components/home')), 'demo')
}, {
path: '/index',
name: 'Index',
component: r => require.ensure([], () => r(require('@/components/index')), 'demo')
}, {
path: '/about',
name: 'about',
component: r => require.ensure([], () => r(require('@/components/about')), 'demo-01')
}
30、delete和Vue.delete删除数组的区别?
delete只是被删除的元素变成了empty/undefined其他的元素的键值还是不变。Vue.delete直接删除了数组 改变了数组的键值
var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[1]
console.log(a)
this.$delete(b,1)
console.log(b)
普通delete删除一数组中的元素,该元素会成为空值。数组长度不变。
vue.delete删除会直接删除一个数组元素,长度会减少。
31、路由跳转和location.href的区别?
使用location.href='/url'来跳转,简单方便,但是刷新了页面;
使用路由方式跳转,无刷新页面,静态跳转;
32、vue的solt的用法?
在子组件内使用特殊的元素就可以为这个子组件开启一个slot(插槽),在父组件模板里,插入在子组件标签内的所有内容将替代子组件的标签及它的内容。
简单来说就是:在子组件内部使用标签占位,当在父组件中使用子组件的时候,我们可以在子组件中插入内容,而这些插入的内容则会替换标签的位置。
当然:单个slot的时候可以不对slot进行命名,如果存在多个,则一个可以不命名,其他必须命名,在调用的时候指定名称的对应替换slot,没有指定的则直接默认无名称的slot
33、$emit、$on、$once、$off理解
$emit:
触发当前实例上的自定义事件(并将附加参数都传给监听器回调)
$on:监听实例上自定义事件并调用回调函数,监听emit触发的事件
$once:监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器
$off:用来移除自定义事件监听器。如果没有提供参数,则移除所有的事件监听器;如果只提供了事件,则移除该事件所有的监听器;如果同时提供了事件与回调,则只移除这个回调的监听器。
这四个方法的实现原理是:通过对vue实例挂载,然后分别使用对象存储数组对应的函数事件,其中emit通过循环查找存储的数组中对应的函数进行调用,once只匹配一次就结束,on是将对应的函数存储到数组中,off是删除数组中指定的元素或者所有的元素事件。
$emit:使用场景
子组件调用父组件的方法并传递数据。
子组件:
<template>
<button @click="emitEvent">点击我</button>
</template>
<script>
export default{
data(){
return{
msg:'我是子组件的数据'
}
},
methods:{
emitEvent(){
//通过按钮的点击事件触发方法,然后用$emit触发一个my-event的自定义方法,传递this.msg数据。
this.$emit('my-event',this.msg)
}
}
}
</script>
父组件:
<template>
<div id="app">
<child-a @my-event="getMyEvent"></child-a>
//父组件通过监测my-event事件执行一个方法,然后取到子组件中传递过来的值。
</div>
</template>
<script>
import childA from './child.vue';
export default {
components:{
childA
},
methods:{
getMyEvent(msg){
console.log('接收数据---'+msg);
//接收数据,我是子组件的数据
}
}
}
</script>
$on的使用场景
兄弟组件之间相互传递数据。
首先创建一个Vue的空白实例(兄弟组件的桥梁)
import Vue from 'vue';
export default new Vue();
子组件A:发送方使用$emit自定义事件把数据带过去。
<template>
<div>
<span>A组件-{{msg}}</span>
<input type="button" value="把A组件数据传递给B" @click="send">
</div>
</template>
<script>
import eventBus from './eventBus';
export default{
data(){
return{
msg:{
a:'111',
b:'222'
}
}
},
methods:{
send(){
eventBus.$emit('aevent',this.msg)
}
}
}
</script>
子组件B:接收方通过$on监听自定义事件的callback接收数据
<template>
<div>
<span>B组件,A传的数据为--{{msg}}</span>
</div>
</template>
<script>
import eventBus from './eventBus.vue'
export default {
data(){
return{
msg:''
}
},
mounted(){
eventBus.$on('aevent',(val)=>{//监听事件aevent,回调函数要使用箭头函数。
console.log(val);//打印结果;我是a组件的数据。
})
}
}
</script>
父组件:
<template>
<div>
<childa></childa>
<br/>
<childb></childb>
</div>
</template>
<script>
import childa from './childa.vue';
import childb from './childb.vue';
export default{
componets:{
childa,
childb
},
data(){
return{
msg:''
}
}
}
</script>
$once
Vue.prototype.$once = function (event, fn) {
var vm = this;
function on () {
vm.$off(event, on);
fn.apply(vm, arguments);
}
on.fn = fn;
vm.$on(event, on);
return vm
};
34、$root、$refs、$parent的使用?
$root
可以用来获取vue的根实例,比如在简单的项目中将公共数据放在vue根实例上(可以理解为一个全局store),因此可以替代vuex实现状态管理;
$refs
在子组件上使用ref特性后,this.属性 可以直接访问该子组件。可以代替时间emit和$on的作用。
使用方式是通过ref特性为这个子组件赋予一个id引用,再是通过this.$refs.testId获取指定元素
注意:$refs只会在组件渲染完成之后生效,并且他们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”---应该避免在模板或者计算属性中访问$refs
$parent
$parent属性可以用来从一个子组件访问父组件的实例,可以替代将数据以prop的方式传入子组件的方式;当变更父组件的数据的时候,容易造成调试和理解难度增加;
35、vue开发遇到的问题?
1)样式污染
在编写样式中,如果需要放置样式的污染,可以使用两种方式,一种是在组件的根元素上增加一个唯一的class或者id。然后在编写组件的样式时候在根元素对应的class或者id下进行编写;另一种方式是在对应的style上添加scoped关键字,不过该关键字对引用的框架UI无效。
2)router-link在安卓上不起作用
不起作用的原因是因为转码编译的问题,可以使用babel来进行处理,安装babel polypill插件解决
3)初始化页面出现闪屏乱码的问题
这是因为vue还没有解析的情况下会容易出现花屏显现,看到类似于{undefined{data}}的字样,可以使用两种方式来进行处理一种为:
- 在设置index的根元素的元素样式为display:none,然后在mounted中的nextTick函数中display:block展示;另一种方式是使用vue的内置指令:v-cloak,并且在css中设置样式
[v-cloak] {
display: none;
}
4)router-link上事件无效解决办法
使用@click.native来进行调用原生js的事件。原因:router-link会阻止click事件.native指直接监听一个原生事件。
36、class与style如何动态绑定?
class可以通过对象语法和数组语法进行动态绑定:
- 对象语法:
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
- 数组语法:
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
Style也可以通过对象语法和数组语法进行动态绑定:
- 对象语法:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
- 数组语法:
<div v-bind:style="[styleColor, styleSize]"></div>
37、怎样理解vue的单向数据流?
所有的prop都使得其父子prop之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中,但是反过来则不行。
这样会防止从子组件意外改变父组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父组件发生更新时,子组件中所有的prop都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变prop。如果你这样做了,Vue会在浏览器的控制台中发出警告。
子组件想修改时,只能通过$emit派发一个自定义事件,父组件接收到后,由父组件修改。
- 这个prop用来传递一个初始值;这个子组件接下来希望将其作为一个本地的prop数据来使用。在这种情况下,最好定义一个本地的data属性并将这个prop用作其初始值:
props: ['initialCounter'],
- 这个prop以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个prop的值来定义一个计算属性
props: ['size'],
38、直接给一个数组项赋值,vue能检测到变化吗?
由于JavaScript的限制,vue不能检测到一下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:vm.items.length = newLength
为了解决第一个问题,vue提供了一下操作方法:
// Vue.set
为了解决第二个问题,Vue 提供了以下操作方法:
// Array.prototype.splice
Vue.set
vue.set(target,key,value)/this.$set(target,key,value)
参数有三个,第一个参数是对象或数组,如果是数组,第二个参数就是数组中的下标,如果是对象,第二个参数就是属性名,第三个参数是修改的值
target:要更改的数据源(可以是对象或者数组)
key:要更改的具体数据
value:重新赋的值
data() {
return {
items:[
{message:"Test one",id:"1"},
{message:"Test two",id:"2"},
{message:"Test three",id:"3"}
]
}
},
methods: {
dynamicClick:function(){
//this.items[0]={message:"Change Test",id:'10'}
this.$set(this.items,0,{message:"ChangeTest",id:'10'})
console.log(this.items,'----------')
},
}
Vue.set()和this.$set()区别:
组件中可以直接使用this.$set(),如果使用Vue.set(),就要用import Vue
Array.prototype.splice()
splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。
Array.prototype.splice(start, deleteCount, ...items)各参数叙述如下:
- start:指定修改的开始位置(从0计数)。如果超出了数组的长度,则从数组末尾还是添加内容;如果是负值,则表示从数组末尾开始的第几位(从-1计数,这意味着-n是倒数第n个元素并且等价于array.length-n);如果负数的绝对值大于数组的长度,则表示开始位置为第0位
- deleteCount():整数,可选,表示要移除的数组元素的个数。如果deleteCount大于start之后的元素的总数,则从start后面的元素都将被删除 (含第start位)。如果deleteCount被省略了,或者它的值大于等于array.length - start(也就是说,如果它大于或者等于start之后的所有元素的数量),namestart之后数组的所有元素都会被删除。如果deleteCount是0或者负数,则不移除元素。这种情况下,至少应添加一个新元素;
- ...items:用逗号分隔的参数,可选,要添加进数组的元素,从start位置开始。如果不指定,则splice将不会添加数组元素,只删除数组元素
// 注:下述每个操作都是单独对nums进行的,前后没有关联
const nums = [2, 7, 11, 2, 15]
// 参数:start
// 从start开始(包含start),删除后续所有元素
nums.splice(2) // nums = [2, 7]
// 参数:start, deleteCount
// 从start开始(包含start),删除deleteCount数目的元素
nums.splice(3, 2) // nums = [2, 7, 11]
// 参数:start, 0, ..items
// 从start位置开始,插入传入的参数
nums.splice(0, 0, 1, 2) // [1, 2, 2, 7, 11, 2, 15]
// 参数:start, deleteCount, ...items
// 此时,items.length === deleteCount,从start开始,将deleteCount个元素依次修改为items
// 过程:先删除deleteCount个元素,再插入items.length个元素
nums.splice(1, 2, 8, 12) // [2, 8, 12, 2, 15]
nums.splice(1, 2, 8, 12, 13) // [2, 8, 12, 13, 2, 15]
nums.splice(1, 3, 8, 12) // [2, 8, 12, 15]
39、在哪个生命周期内调用异步请求?
可以在钩子函数created、beforeMounted、,mounted中进行调用,因为在这三个钩子函数中,data已经创建,可以将服务端返回的额数据进行赋值。但是本人推荐在created钩子函数中调用异步请求,因为在created钩子函数中调用有以下优点:
- 能更快获取到服务器端数据,减少页面loading时间;
- ssr不支持beforeMounted、mounted钩子函数,所以放在created中有助于一致性;
40、在什么阶段才能访问操作DOM?
在钩子函数mounted被调用前,vue已经将编译好的模板挂载到页面上,所以在mounted中可以访问操作DOM。
41、父组件可以监听到子组件的生命周期吗?
比如有父组件parent和子组件child,如果父组件监听到子组件挂载mounted就做一些逻辑处理,可以通过以下写法实现:
// 父组件 Parent.vue中
<Child @mounted="handleDoSomething"/>
// 子组件 Child.vue中
mounted() {
this.$emit("mounted我被触发了~");
}
缺点:以上方法虽然可行,但每次都需要手动写一次 $emit 触发父组件的事件。
更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:
// Parent.vue
<Child @hook:mounted="handleDoSomething" ></Child>
handleDoSomething() {
console.log('父组件监听到 mounted 钩子函数~');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数~');
},
42、keep-alive的了解?
keep-alive是Vue内置的一个组件,可以使被包含的组件保留状态,避免重新渲染,其有以下特性:
- 一般结合路由和动态组件一起使用,用于缓存组件
- 提供include和exclude属性,两者都支持字符串或正则表达式,include表示只有名称匹配的组件会被缓存,exclude表示任何名称匹配的组件都不会被缓存,其中exclude的优先级比include高;
- 对应两个钩子函数activated和deactivated,当组件被激活时,触发钩子函数activated当组件被移除时,触发钩子函数deactivated。
<keep-alive>
<loading></loading>
</keep-laive>
用途:列表和详情页频繁执行打开详情=》返回列表=》打开详情操作,列表和详情都是高频页面,那么就可以对列表组建使用进行缓存,这样每次返回列表都从缓存中快速渲染,而不是重新渲染。
43、Vuex
Vuex是一个专门为Vue.js应用程序开发的状态管理模式。每一个vuex应用的核心就是store(仓库)。“store”基本上就是一个容器,塔包含着你的应用中大部分的状态(state)。
(1)Vuex的状态存储是响应式的。当vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新
(2)改变store中的状态的唯一途径就是显式地提交(commit)mutation。这样使得我们可以方便地跟踪每一个状态的变化。
主要包括以下几个模块:
- state:定义了应用状态的数据结构。可以再这里设置默认的初始状态。
- getter:允许组件从store中获取数据,mapGetter辅助函数仅仅是将store中的getter映射到局部计算属性。
- Mutation:是唯一更改store中状态的方法,且必须是同步函数。
- Action:用于提交mutation,而不是直接变更状态,可以包含任意异步操作。
- Module:允许将单一的Store拆分为多个store且同时保存在单一的状态树中。
44、Vue SSR?
SSR大概意思就是vue在客户端将标签渲染成的整个html片段的工作在服务端完成,服务端形成的html片段直接返回给客户端这个过程就叫做服务端渲染。
(1)服务端渲染SSR的优点:
- 更好的SEO:因为SPA页面的内容是通过Ajax获取,而搜索引擎爬取工具并不会等待Ajax异步完成后再抓取页面内容,所以在SPA中抓取不到页面通过ajax获取到的内容;而SSR是直接由服务端返回已经渲染好的页面(数据已经包含在页面中,)所以搜索引擎爬取工具可以抓取渲染好的页面;
- 更快的内容到达时间(首屏加载更快):SPA会等待所有的vue编译后的js文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等待,所以首屏渲染需要一定的时间;SSR直接由服务端渲染好页面直接返回显示,无需等待下载js文件及再去渲染等,所以SSR又更快的内容到达时间;
(2)服务端渲染的缺点:
- 更多的开发条件限制:例如服务端渲染只支持beforeCreate和created两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序SPA不同,服务端渲染应用程序,需要处于Node.js server运行环境;
- 更多的服务器负载:在node.js中渲染完整的应用程序,显然会比仅仅提供静态文件的server更加大量占用CPU资源(CPU-intensive - CPU密集),因此如果你预料在高流量环境下使用,请准备相应的服务器负载,并明智地采用缓存策略
参考:SSR 的实践文章《Vue SSR 踩坑之旅》
45、vue-router路由模式有几种?
vue-router有三种路由模式:hash、history、abstract,对应的源码如下
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base,
this.fallback)
break;
case 'abstract':
this.history = new AbstractHistory(this,
options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
其中3 种路由模式的说明如下:
- hash:使用URL hash值来做路由。支持所有浏览器,包括不支持HTML5 History Api的浏览器;
- history:依赖HTML5 History API和服务器配置。具体可以查看HTML5 History模式;
- abstract:支持所有JavaScript运行环境,如Node.js服务器端。如果发现没有浏览器的API,路由会自动强制进入这个模式
46、vue-router中常用的hash和history路由模式实现原理?
(1)hash模式的实现原理
早期的前端路由的实现就是基于location.hash来实现的。其实现原理很简单,location.hash的值就是URL中#后面的内容。比如下面这个网站,它的location.hash的值为’#search‘:
https://www.word.com#search
hash路由模式的实现主要是基于以下几个特性:
- URL中hash值只是客户端的一种状态,也就是说当向服务器发出请求时,hash不封不会被发送
- hash值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash的切换;
- 可以通过a标签并设置href属性,当用户点击这个标签后,URL的hash值会发生改变;或者使用JavaScript来对location.hash中进行赋值,改变URL的hash值;
- 我们可以使用hashchange事件来监听hash值的变化,从而对页面进行跳转(渲染)
(2)history模式的实现原理
HTML5提供了history API来实现URL的变化。其中做最主要的API有以下两个:
history.pushState()和history.replaceState()。这两个API可以在不进行刷新的情况下,操作浏览器的历史记录。唯一不同的是,前端是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:
window.history.pushState(null, null, path);
history路由模式的实现主要基于存在下面几个特性:
- pushState和replaceState两个API来操作实现URL的变化;
- 我们可以使用popstate事件来监听url的变化,从而对页面进行跳转(渲染);
history.pushState()或history.replaceState()不会触发popstate事件,这时我们需要手动触发页面跳转(渲染)
47、MVVM
(1)view层
<div id="app">
<p>{{message}}</p>
<button v-on:click="showMessage()">Click me</button>
</div>
(2)viewmodel层
var app = new Vue({
el: '#app',
data: { // 用于描述视图状态
message: 'Hello Vue!',
},
methods: { // 用于描述视图行为
showMessage(){
let vm = this;
alert(vm.message);
}
},
created(){
let vm = this;
// Ajax 获取 Model 层的数据
ajax({
url: '/your/server/data/api',
success(res){
vm.message = res;
}
});
}
})
(3)model层
{
"url": "/your/server/data/api",
"res": {
"success": true,
"name": "IoveC",
"domain": "www.cnblogs.com"
}
}
48、vue如何实现数据双向绑定?
通过 Object.defineProperty() 对数据进行劫持
view变化更新data,可以通过事件监听的方式来实现,所以vue的数据双向绑定的工作主要是如何根据data变化更新view。
四个步骤:
- 实现一个监听器Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
- 实现一个解析器Compile:解析vue模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
- 实现一个订阅者Watcher:Watcher和Compile之间通信的桥梁,主要的任务是订阅Observer中的属性值变化的消息,当收到属性变化的消息时,触发解析器Complie中对应的更新函数。
- 实现一个订阅器Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
49、Vue框架怎么实现对象和数组的监听?
如果被问到 Vue 怎么实现数据双向绑定,大家肯定都会回答 通过 Object.defineProperty() 对数据进行劫持,但是 Object.defineProperty() 只能对属性进行数据劫持,不能对整个对象进行劫持,同理无法对数组进行劫持,但是我们在使用 Vue 框架中都知道,Vue 能检测到对象和数组(部分方法的操作)的变化,那它是怎么实现的呢?我们查看相关代码如下:
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]) // observe 功能为监测数据的变化
}
}
/**
* 对属性进行递归遍历
*/
let childOb = !shallow && observe(val) // observe 功能为监测数据的变化
通过以上vue源码部分查看,我们就能知道Vue框架是通过遍历数组和递归遍历对象,从而达到利用Object.defineProperty()也能对对象和数组(部分方法的操作)进行监听。
50、Proxy 与 Object.defineProperty 优劣对比
【proxy的优势如下】
- proxy可以直接监听对象而非属性
- proxy可以直接监听数组的变化
- proxy有多大13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等 是Object.defineProperty不具备的;
- proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改;
- proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
「Object.defineProperty 的优势如下:」
- 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。
51、vue中的$set
1、改变数组使用$set:
根据vue官网,明确说了以下两种改变数组的操作是无法做到响应式更新页面的:
- 当你利用索引直接设置一个数组项时,
例如:vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:vm.items.length = newLength
使用vue约定的方式操作数组:
使用$set修改指定下标的元素
this.$set(this.list,'0','a')
使用splice改变数组长度
this.list.splice(1)
二、使用$set操作对象:
1.往对象中新增一个属性
data(){
return{
// 原对象
obj:{
name:'zhangsan'
}
}
}
// 向对象中添加一个属性为age 值为18
this.$set(this.obj,'age','18')
2.往对象新增多个属性
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
Object.assign(target, ...source)
参数:target:目标对象;source:源对象
返回值:target,即目标对象
// 对原始对象obj添加两个新属性,得到一个新对象
this.obj = Object.assign({},this.obj,{age:18,sex:'男'})
52、虚拟DOM的优缺点?
【优点】:
- 【保证性能下限】:框架的虚拟DOM需要适配任何上层API可能产生的操作,它的一些DOM操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的DOM操作性能要好很多。因此框架的虚拟DOM至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能
- 无需手动操作DOM:只需要写好view-model的代码逻辑,框架会根据虚拟Dom和数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
- 跨平台:虚拟dom本质上是js对象,而Dom与平台强相关,相比之下虚拟dom可以进行更方便的跨平台操作,例如服务器渲染,weex开发等等。
【缺点】:
- 无法进行极致优化:虽然虚拟DOM+合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟dom无法进行针对性的极致优化。
53、vue中的key有什么作用?
key是vue中vnode的唯一标记,通过这个key,diff操作可以更准确,更快速。
【更准确】:因为带key就不是就地复用看,在sameNode函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确
【更快速】:利用key的唯一性生成map对象来获取对应节点,比遍历方式更快,源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
54、对vue项目进行哪些优化?
【1、代码层的优化】:
- v-if和v-show区分使用场景
- computed和watch区分使用场景
- v-for遍历必须为item添加key,且避免同时使用v-if
- 长列表性能优化
- 事件的销毁
- 图片资源懒加载
- 路由懒加载
- 第三方插件按需引入
- 优化无限列表的性能
- 服务端渲染SSR or预渲染
【2、webpack层面优化】
- webpack对图片进行压缩
- 减少es6转为es5的冗余代码
- 提取公共代码
- 模板预编译
- 提取组件的css
- 优化SourseMap
- 构建结果输出分析
- vue项目的编译优化
【3、基础的web技术的优化】
- 开启gzip压缩
- 浏览器缓存
- CDN的使用
- 使用chrome performance 查找性能瓶颈
55、computed和watch的区别及各自的使用
computed计算属性
1、使用方法和data中的数据一样,但是类似一个执行方法
2、调用时不加()
3、必须有return返回
4、如果函数所依赖的属性没有发生变化,从缓存中读取
5、应用场景---------求和
例:
<el-form>
<el-row>
<el-col :span="24" style="display:flex;justify-content:flex-end;">
<span><i>当前价税合计:</i>{{Total}}</span>
</el-col>
</el-row>
</el-form>
computed:{
Total(){
let price = 0;
for (let i = 0;i<this.tableData.length;i++) {
price += this.tableData[i].taxUnitPrice * this.tableData[i].invNum
}
this.detailedSend.total = price
return price;
},
},
watch监听
1、watch的函数名必须和data中的数据名一致
2、watch中的函数有两个参数,newVal和oldVal
3、watch中的函数是不需要调用的
4、watch只会监听数据的值是否发生改变,而不会去监听数据的地址是否发生改变,要深度监听需要配合deep:true属性使用
5、immediate:true页面首次加载的时候做一次监听
6、应用场景---------即时搜索
例:
<template>
<div>
vue使用watch监听实现类似百度搜索功能
<div>
<input type="text" class="search" placeholder="搜索" v-model.trim='keyword' />
</div>
<div v-show="keyword">
<ul>
<li v-for="item in cityList" :key="item.id" @click="handleCityClick(item.name)">
{{item}}
</li>
</ul>
</div>
</div>
</template>
<script>
// 防抖函数
const delay = (function () {
let timer = 0;
return function (callback, ms) {
clearTimeout(timer);
timer = setTimeout(callback.ms)
}
})()
export default {
data () {
return {
keyword: '',
cityList: [],
timer: null,
jsonData: [{
'id': 1,
'name': '北京'
},
{
'id': 2,
'name': '上海'
},
{
'id': 3,
'name': '广州'
},
{
'id': 4,
'name': '深圳'
},]
}
},
watch: {
keyword () {
//函数防抖
if (this.timer) {
clearTimeout(this.timer)
}
//删除文字 清零
if (!this.keyword) {
this.cityList = []
return
}
this.timer = setTimeout(() => {
const result = []
this.jsonData.forEach(val => {
if (val.name.indexOf(this.keyword) > -1) {
result.push(val.name)
}
});
this.cityList = result
console.log(this.cityList)
}, 100)
}
},
}
</script>
区别:
1、功能:computed是计算属性,watch是监听一个值的变化执行对应的回调
2、是否调用缓存,computed函数所依赖的属性不变的时候会调用缓存;watch每次监听的值发生变化的时候都会调用回调
3、是否调用return,conputed必须有,watch可以没有
4、使用场景,computed当一个属性受到多个属性影响的时候(例如购物车结算)
watch当一条数据影响多条数据的时候(例如搜索框)
5、是否支持异步,computed不能有异步,watch可以
56、全局指令:directive、extent、set、component
directive:自定义指令
Vue.directive('指令',{
inserted(el,binding){
// el: 绑定了指令的dom元素
// binding: value代表指令绑定表达式的值
}
})
在new Vue()之前
自定义指令的五个钩子函数
bind:初始化只调用一次(dom未创建就调用)
inserted:初始化调用(dom创建之后调用)
updated:组件更新前调用
componentupdated:组件更新后调用
unbind:只调用一次,指令与元素解绑时调用
export default {
directives: {
focus: {
bind() {
console.log("bind");
},
inserted(el) {
el.focus();
console.log("inserted");
},
update() {
console.log("update");
},
componentUpdated() {
console.log("componentUpdated");
},
unbind() {
console.log("unbind");
},
},
},
};
57、mixins和extends的区别
混合mixins和继承extends
两个都可以理解为继承,mixins接收对象数组(可理解为多继承),extends接收的是对象或函数(可理解为单继承)
const extend = {
created () {
console.log('extends created')
}
}
const mixin1 = {
created () {
console.log('mixin1 created')
}
}
const mixin2 = {
created () {
console.log('mixin2 created')
}
}
export default {
name: 'component-name',
extends: extend,
mixins: [mixin1, mixin2],
components: {
},
data () {
return {
}
},
computed: {
},
watch: {
},
methods: {
},
created () {
console.log('created')
},
mounted () {
}
}
</script>
<style scoped>
</style>
控制台输出:
extends created
mixin1 created
mixin2 created
created
结论:
- 优先调用mixins和extends继承的父类,extends触发的优先级更高,相对于是队列
- push(extends,mixin1,mixin2,本身的钩子函数)
- 经过测试,watch的值继承规则一样
继承methods
const extend = {
data () {
return {
name: 'extend name'
}
}
}
const mixin1 = {
data () {
return {
name: 'mixin1 name'
}
}
}
const mixin2 = {
data () {
return {
name: 'mixin2 name'
}
}
}
// name = 'name'
export default {
mixins: [mixin1, mixin2],
extends: extend,
name: 'app',
data () {
return {
name: 'name'
}
}
}
// 只写出子类,name = 'mixin2 name',extends优先级高会被mixins覆盖
export default {
mixins: [mixin1, mixin2],
extends: extend,
name: 'app'
}
// 只写出子类,name = 'mixin1 name',mixins后面继承会覆盖前面的
export default {
mixins: [mixin2, mixin1],
extends: extend,
name: 'app'
}
结论:
- 子类再次声明,data中的变量都会被重写,以子类的为准
- 如果子类不声明,data中的变量将会最后继承父类为准
- 经过测试,props中的属性、methods中的方法和computed的值继承规则一样
minxins:
- 调用方式:mixins:[mixin1,minxin2]
- 是对父组件的扩充,包括methods、computed、directive等
- 触发钩子函数时,先调用mixins的函数,在调用父组件的函数
- 虽然也能在创建mixin时添加data、template属性,但当父组件也拥有此属性时,以父为准
- data、methods内函数、components和directives等键值对格式的对象均以父组件/实例为准
extends
调用方式:extends:CompA
同样是对父组件的扩充,与mixins类似,但优先级均次于父组件
extend
扩展组件但构造器
当我们调用vue.component('a',{...})时自动调用
值得注意但是extend内的data为一个函数
58、Vue响应式原理及vue2.0缺点
es5的Object.defineProperty(),可以重新定义数据的属性,给属性增加setter和getter方法;
Vue的响应式原理 核心是通过 ES5 的 Object.defindeProperty 进行数据劫持 然后利用 get 和 set 方法进行数据的获取和设置 这时的data中声明的属性都将被添加到 get 和 set 中 当读取data中的数据中 自动调用 get 方法 当修改数据时 或者数据发生改变时 自动调用 set 方法 去侦听检测数据的变化 同时会通知 观察者 Wacher 观察者 Wacher 自动重新触发 render 当前组件 (子组件不会重新渲染) 生成新的虚拟 DOM 树 Vue的框架会遍历并对比新旧虚拟DOM树上面的每个节点的差别 并记录下来 最后加载操作 将所有记录下来的节点 局部修改到真实的DOM树上
因此我们可以执行以下3个步骤,实现数据的双向绑定:
1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
缺点:
1、无法监听到对象属性的动态添加和删除
2、无法监听到数组下标和数组的长度变化
this.$set(this.obj,'sex','男')
59、dependencies和devDependencies的区别
dependencies是生产环境下的依赖
devDependencies是开发环境下的依赖
使用npm安装依赖时,-save会被写到dependencies里
-save-dev会被写到devDependencies里
60、vue动态组件
动态组件是指在一个页面的位置使用多个组件,并进行动态切换。
元素is的特性;或使用v-if
方式一:元素is的特性
<template>
<div class="hello">
<h3>使用component 的 is特性</h3>
<ul>
<li
v-for="(item,index) in tabList"
:key="index"
style="cursor:pointer"
@click="changeView(index)">
{{item}}</li>
</ul>
<component :is="currentView"></component>
</div>
</template>
<script>
// 相关的组件代码在这里不展示
import shouji from "./shouji";
import pc from "./pc";
import shuma from "./shuma";
export default {
name: "HelloWorld",
components: {
shouji,
pc,
shuma,
},
data() {
return {
index: 0,
arr: ["shuma", "shouji", "pc"],
tabList: ["数码", "手机", "电脑"],
};
},
computed: {
currentView() {
return this.arr[this.index];
}
},
methods: {
changeView(index) {
this.index = index;
}
},
};
</script>
v-if
<template>
<div class="hello">
<h3>使用v-if实现</h3>
<ul>
<li v-for="(item,index) in tabList" :key="index" style="cursor:pointer" @click="change(index)">{{item}}</li>
</ul>
<div>
<shuma v-if="index===0"></shuma>
<shouji v-else-if="index===1"></shouji>
<pc v-else></pc>
</div>
</div>
</template>
<script>
// 相关的组件代码在这里不展示
import shouji from "./shouji";
import pc from "./pc";
import shuma from "./shuma";
export default {
name: "HelloWorld",
components: {
shouji,
pc,
shuma,
},
data() {
return {
index: 0,
keppIndex:0,
arr: ["shuma", "shouji", "pc"],
tabList: ["数码", "手机", "电脑"],
};
},
computed: {
},
methods: {
change(index) {
this.index = index;
}
},
};
</script>
可以使用keep-alive把组件缓存起来
生命周期
activated() {
console.log("shouji 手机页面被添加");// 被缓存的组件激活时触发
},
deactivated() {
console.log("shouji 手机页面被移除"); // 被切换到其他组件时触发
}
include表示只能允许匹配到的组件生效
exclude则相反,除了匹配到的组件之外有效
<!-- 字符串 逗号分隔字符串, a,b 分别为组件名 -->
<keep-alive include="a,b">
<component :is="currentView"></component>
</keep-alive>
<!-- 正则表达式 -->
<keep-alive :include="/a|b/">
<component :is="currentView"></component>
</keep-alive>
<!-- 数组 -->
<keep-alive :include="['a', 'b']">
<component :is="currentView"></component>
</keep-alive>
61、父子组件生命周期
父beforecreate
父created
父beforeMounted
子beforecreate
子created
子beforemounted
子mounted
父mounted
父beforeUpdate
子beforeUpdate
子updated
父updated
父beforedestroy
子beforedestroy
子destroyed
父destroyed