1、vue的双向绑定原理
vue采用数据劫持结合发布订阅模式,以MVVM作为入口,内部有三大模块,即Observer,Compiler和Watcher三者,其中Observer(数据监听器)通过Object.defineProperty来对data中的属性进行遍历拦截,拦截get和set函数,在get函数中进行依赖收集,在set中调用Dep.notify()方法通知Watcher数据发生了改变,Watcher收到通知后,触发相应的监听回调,进行视图更新。Compiler是一个指令解析器,它能够将指令模板解析成数据,以及绑定相应的更新函数。
2、什么是虚拟dom
虚拟dom是用JS来表示页面结构的一种手段,用来优化因频繁操作DOM而带来的性能问题。本质上是真实DOM和JS的中间层,当我们想用JS进行大批量的DOM修改的时候,会首先作用于这个虚拟DOM上,通过diff算法比较新旧DOM的差异,再把真正要修改的地方更新在真实DOM上。
3、虚拟dom的优缺点
优点:
- 减少了dom操作,减少了重绘和重排。不需要我们手动操作dom,只需要关注view-model的代码逻辑,框架会根据虚拟dom和双向数据绑定进行视图更新,有利于提高开发效率。
- 跨平台。虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等
缺点:
- 无法做到极致优化
- 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢
4、虚拟dom一定比操作原生dom更快吗
虚拟dom是一种性能VS可维护性的取舍。没有一种框架能够比手动优化DOM更快,因为框架的DOM操作层需要适配任何上层API可能产生的操作,所以它必须具有普适性。但是在构建一个实际应用的时候,出于可维护性的考虑,不可能对每一个地方都进行手动优化,框架能保证的是,再不需要手动优化的情况下,提供一个还过得去的性能。 主流的框架+合理的优化,能够应对绝大部分应用的性能需求,如果有对性能有极致需求的特殊情况,需要牺牲一些可维护性采用手动优化
5、介绍一下diff算法
diff算法是一种对树的同层节点进行比较的高效算法,避免了对树进行逐层变量,时间复杂度只有O(n),diff算法通过对新旧虚拟dom节点进行对比,并返回一个patch对象,用来存储两个节点不同的地方,最后利用patch局部更新DOM
比较流程如下:
-
首先对新旧vnode的开始位置和结束位置进行标记。
-
标记好后进入while循环中,这是diff算法的核心流程,循环从两边向中间靠拢,while循环的退出条件是节点的开始位置大于结束位置。while循环的处理逻辑如下。
- 当新旧vNode的start满足sameNode(节点相同可以直接复用)时,直接patchNode即可,同时新旧vNode的开始索引加1(往中间移动一位)
- 当新旧vNode的end满足sameNode(节点相同可以直接复用)时,直接patchNode即可,同时新旧vNode的开始索引减1(往中间移动一位)
- 当老 VNode 节点的 start 和新 VNode 节点的 end 满足 sameVnode 时,这时候在 patchVnode 后,需要将当前真实 dom 节点移动到 oldEndVnode 的后面,同时老 VNode 节点开始索引加1,新 VNode 节点的结束索引减1。
- 当老 VNode 节点的 end 和新 VNode 节点的 start 满足 sameVnode 时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldStartVnode 的前面,同时老 VNode 节点结束索引减1,新 VNode 节点的开始索引加1。
- 如果都不满足以上四种情形,那说明没有相同的节点可以复用,于是则通过查找事先建立好的以旧的 VNode 为 key 值,对应 index 序列为 value 值的哈希表。从这个哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点,如果两者满足 sameVnode 的条件,在进行 patchVnode 的同时会将这个真实 dom 移动到 oldStartVnode 对应的真实 dom 的前面;如果没有找到,则说明当前索引下的新的 VNode 节点在旧的 VNode 队列中不存在,无法进行节点的复用,那么就只能调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置。
-
当 while 循环结束后,根据新老节点的数目不同,做相应的节点添加或者删除。若新节点数目大于老节点则需要把多出来的节点创建出来加入到真实 dom 中,反之若老节点数目大于新节点则需要把多出来的老节点从真实 dom 中删除。至此整个 diff 过程就已经全部完成了。
6、vue中key的作用
key主要用在虚拟 Dom 的diff算法中。key是给每一个vnode的唯一id,可以依靠key,更准确, 更快的拿到oldVnode中对应的节点
准确体现在:vue在进行新旧dom的对比时,会尽可能的就地复用。就地复用会带来一些问题,比如过渡效果丢失,表单状态错位。而使用了key就不是就地复用了。
快速体现在:在vue的diff算法中,当新旧节点交叉对比没有结果时,会拿着新节点的key去对比旧节点的key,从而找到相应的旧节点,如果没找到则会新增,这是一个map映射,如果没有key的话,就是一个遍历查找的过程。相必而言,map映射的速度更快
7、使用key应注意哪些方面
应保证key的唯一性和不变性。不推荐使用对象、index等作为key值。当以index为key值时,如果数组长度发生变化,会导致key的变化,比如删除其中某一项,那么index会相应变化。所以这时候用index作为key和不加index没有什么区别,都不能提升性能。通常推荐使用server给的SQL-ID
8、vue的生命周期
- beforeCreate : 进行数据监测,初始化事件还未开始
- created : 完成数据监测,属性和方法的运算,初始化事件,$el属性还没有显示出来
- beforeMount : 编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面
- mounted : 用编译好的html内容替换el属性指向的DOM对象,渲染页面
- beforeUpdate : 在数据更新之前调用,可以在该钩子中进一步的更改状态,不会触发附加的重渲染过程
- updated : 更新后,在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用,调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。
- beforeDestroy : 在实例销毁之前调用,实例仍然完全可用
- destroyed : 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有子实例也会被销毁
- actived : 复用组件被激活时调用
- deactived : 复用组件失活时调用
- errorCaptured :当捕获一个来自子孙组件的错误时被调用。此钩子可以返回
false
以阻止该错误继续向上传播
9、vue中父子组件的生命周期过程
1、渲染的过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
2、子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
3、父组件更新过程
父beforeUpdate->父updated
4、 销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
10、vue中的nextTick
vue在修改数据后,并不会立即更新dom,而是等同一事件循环中所有数据变化完之后,再统一进行视图更新。
vue是异步执行dom更新的,一旦观察到数据变化,就会开启一个队列,把同一事件循环中观察到的数据变化的Watcher都推进这个队列中,如果这个Watcher被触发多次,只会被推到队列一次,这种缓冲行为可以有效的去掉重复数据带来的不必要的计算和DOM操作,在下一次事件循环开始时,vue会清空队列,并进行必要的dom更新。
nextTick就是用来告知dom什么时候更新好了,当dom更新完毕后,nextTick里的回调函数就会立即执行
应用场景:1.created里此时还没有进行DOM挂载,所以无法访问dom,可以使用nextTick。2、mounted里当更改数据后想要获取最新的dom进行一些操作,可以放在nextTick的回调函数里。
11、$nextTick属于宏任务还是微任务
既可以是宏任务也可以是微任务 在 Vue 2.4 之前都是使用的 microtasks(微任务),但是 microtasks 的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用 macrotasks(宏任务) 又可能会出现渲染的性能问题。所以在新版本中,会默认使用 microtasks,但在特殊情况下会使用 macrotasks。比如 v-on。只有绑定的事件函数执行时走宏任务。vue2.6版本最终又还原为走微任务,但是对事件执行做了一些改动。以阻止早期发现的一些情况下由于微任务优先级太高导致的函数执行。
12、computed和watch的区别
- computed是计算属性,具有缓存性,依赖的数据没有改变时不会重新计算。watch是监听,没有缓存性。
- computed适合一个数据依赖多个数据的场景,watch适合一个数据变化影响多个数据的场景。
- watch可以写异步操作,computed不可以
延伸:computed原理?
- computed和watch都是new Watcher生成的实例实现对数据的监听,区别是computed是惰性求值的Watcher。computed具有缓存机制,其本质是通过一个dirty属性控制的。dirty只会在响应式数据发生改变时变成true,当访问computed中的属性时,先去判断dirty属性的值,如果为true,则直接取缓存值,否则重新计算结果并替换缓存,重新计算后将dirty置为false。
13、watch中immediate:true有什么作用
当数据第一次绑定的时候不会触发watch,使用immediate:true可以时在初始绑定的时候监听到。
14、vue组件中的通信方式有哪些
- 全局vuex
- 事件中心EventBus
- 父子
props
$emit
- 父子
$refs
、$parent
、$children
- 跨级
$attrs
、$listeners
- 跨级
provide
inject
15、vuex有哪些属性
- state:存放数据
- getters:获取数据,就像计算属性一样,具有缓存性
- mutations:改变数据的唯一方式就是通过
this.$store.commit
提交mutation - actions:可以做一些异步操作,通过
this.$store.dispatch
的形式在mutation中改变数据 - modules:分模块 延伸:为什么mutation必须是同步的?
- mutation被设计成同步的,当数据改变后能够立即拿到修改后的值,便于devtool追踪数据的变化。试想mutation进行异步操作,当 mutation 触发的时候,异步回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用 —— 实质上任何在回调函数中进行的状态的改变都是不可追踪的
16、vue-router有哪几种钩子函数
- 全局钩子:
router.beforeEach
/router.beforeResolve
/router.afterEach
- 路由钩子:
beforeEnter
- 组件内钩子:
beforeRouteEnter
/beforeRouteUpdate
/beforeRouteLeave
延伸1:都有哪些参数?
- to、from、next
延伸2:说说你对next方法的理解
- next(),执行管道中的下一个钩子,如果走到最后一个钩子函数,那么导航的状态就是confirmed
- next(false),中断当前的导航,URL 地址会重置到from路由对应的地址。
- next( ’ / ‘)或者next({ paht:’ /xxx ’ }):跳转到别的地址。当前的导航被中断,然后进行一个新的导航。
- next( error ):如果传入next的参数是一个Error实例,则导航会被终止且该错误会被传递给router.onError()注册过的回调。
next
方法必须要调用,否则钩子函数无法 resolved
。并且,确保 next
函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。
延伸3:钩子函数的应用场景。
//全局钩子
//登录权限验证
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth) { //判断该路由是否需要登录权限
if (cookies('token')) {
next()
} else{
next({
path: '/login',
query: {
redirect: to.fullPath
} //将跳转的路由path作为参数,登录成功后跳转到该路由
})
}
} else {
next()
}
})
//路由钩子
const router = new VueRouter({
routes: [{
path: '/foo',
component: Foo,
meta: {
login_require: false
},
beforeEnter: (to, from, next) => {
//配合meta元信息,做一些路由的权限认证
if (to.matched.some(function (item) {
return item.meta.login_require
})) {
next('/login')
} else
next()
}
}]
})
//组件内钩子
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当钩子执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
// 1.清除定时器
// 2.页面有未保存信息,当用户要离开时,提示是否保存
if(this.saveMessage === false) {
alert('请保存信息后退出!') //弹出警告
next(false) //回到当前页面, 阻止页面跳转
}else {
next() //否则允许跳转
}
}
}
17、vue-router导航解析流程
- 导航被触发
- 失活的组件触发beforeRouteLeave
- 触发全局钩子 router.beforeEach
- 如果是复用组件,触发beforeRouteUpdate
- 触发路由钩子beforeEnter
- 解析异步路由组件
- 触发组件内钩子beforeRouteEnter
- 触发全局钩子router.beforeResolve
- 导航确认
- 触发全局钩子router.afterEach
- 更新视图
- 触发beforeRouteEnter里的next回调
18、vue-router的模式
- hash模式。hash模式会创建hashHistory对象,利用hashHistoty.push和hashHistory.replace管理路由。 url后面会带有一个#,#后面的内容不会被发送到服务器,所以刷新无碍。
- history模式。 本质是利用history.replaceState和history.pushState管理url。不会带有#字符,比较美观。但是刷新会404。因为刷新页面会重新发送请求,服务器可能吧认识这个url。所以后端需要做一下配置,即匹配不到任何资源的时候,重定向到index.html。前端会利用导航钩子函数进行拦截和跳转,就能解决404的问题。 延伸:history模式下,当点击导航栏的前进后退按钮,是调用什么API?
- history.go()、history.back()、history.forward()
19、vue-router如何传递参数,以及如何获取?
- 动态路由形式。在路由中配置path+/:id。获取时使用
this.$route.params.id
<router-link :to='{name:'detail',params:{id:123}}'>点击按钮跳转</router-link>
this.$router.push({name:'detail',params:{id:123}})
this.$router.push(`/detail/${this.$route.params.id}`)
2、query传参。路由不需要带参数,获取时使用this.$route.query.id
<router-link :to='{name:'detail',query:{id:123}}'>点击按钮跳转</router-link>
this.$router.push({name:'detail',query:{id:123}})
this.$router.push({path:'detail',query:{id:123}})
this.$router.push(`/detail?id=${this.$route.query.id}`);
20、依赖版本~和^的区别
~
会匹配最近的补丁依赖包,比如~1.2.3会匹配所有1.2.x版本,但是不包括1.3.0^
会匹配最新的次版本依赖包,比如^1.2.3会匹配所有1.x.x的包,包括1.3.0,但是不包括2.0.0
21、devDependencies和dependencies的区别
- devDependencies 用于本地开发,打包时生产环境不会打包这些依赖
- dependencies 开发环境能用,生产环境也能用。生产环境会被打包
22、非工程化项目中页面闪动问题
当页面加载比较慢时,页面会短暂出现变量名或undefined,可以使用v-clock标签解决。通过v-clock将编译期间的html标签隐藏,css设置[v-clock]:{display:none}
。
延伸:为什么工程化项目没有这个问题?
- 因为使用vue-cli下的项目根元素
<div>
是一个空标签,通过路由配置获取不同组件展示页面内容。
23、对SPA单页面应用的理解,以及优缺点
SPA单页面就是页面在初始化的时候获取全部html、js、css等资源,然后结合路由机制,根据路由配置的组件,动态更新html内容。
优点:
- 不必频繁请求页面资源,用户体验流畅、
- 前后端分离,减轻服务端的压力
缺点(如何解决):
- 由于首次加载内容较多,所以会出现首次加载渲染缓慢的问题 (解决方案:组件按需加载import()、gzip压缩、图片懒加载、第三方库使用CDN,并将源码与第三方库分离webpack的externals)
- 不利于SEO (解决方案:SSR服务端渲染)
24、.sync修饰符的原理
.sync其实是一个语法糖,可以用于父子组件,对一个 prop 进行“双向绑定”
<component :visible.sync="visible"></component>
//相当于
//父组件
<component :visible="visible" @update:visible="value=>visible=value"></component>
//子组件
export defalut {
props: { visible: Boolean },
methods: {
changeVisible(){
this.$emit('update:visible', true) }
}
}
}
25、使用delete和vue.delete删除数组的区别
- 使用delete删除数组元素,被删除的元素变成undefined/empty,其它成员的索引和数组的length不变。
- vue.delete删除数组成员,会改变后面成员的索引和数组长度
26、vue中的事件修饰符有哪些
- .once 事件只执行一次
- .prevent 阻止默认事件
- .stop 阻止事件冒泡
- .self 事件在自身dom上触发,避免事件冒泡的影响
- .capture 加了capture修饰符的dom在事件冒泡阶段优先执行,若有多个,则由外向内依次触发,其余则按顺序由内而外正常触发
- .native 使组件当成一个原生html元素,不加native修饰符,给组件绑定的事件就不会生效
27、vue的单项数据流和双向数据流绑定是什么意思?
- 单项数据流是指数据是单向的,父组件的数据传递给子组件,不可以在子组件修改父组件的数据
- 双向数据绑定:是指数据和页面进行双向绑定,相互影响
28、为什么 vue 组件中的 data 必须是函数?
因为如果默认为 data 是对象的话,对象为引用类型,这样的话,所有复用的组件都是引用的同一个数据,但是如果是函数的话,每次函数都会先创建一个新的数据,从而使每个组件的数据独立
29、keep-alive
<keep-alive></keep-alive>
是vue的内置组件。包裹动态组件时,会缓存不活动的组件实例,而不是销毁。用于保留组件状态或避免重新渲染,有效提升系统性能。
延伸1:keep-alive的生命周期
- 不使用keep-alive的情况:beforeRouteEnter --> created --> mounted --> destroyed
- 使用keep-alive,第一次进入页面的情况:beforeRouteEnter --> created --> mounted --> activated --> deactivated
- 使用keep-alive,并且再次进入了缓存页面的情况:beforeRouteEnter -->activated --> deactivated
延伸2:keep-alive使用场景,结合实际项目经验
进入列表页,获取最新数据,从列表页到详情页,再由详情页回到列表页,此时需要保持列表页之前的状态。可以用keep-alive
配合router-view
一起使用。可以给路由设置一个meta对象,meta{ keepAlive:true }
表示此组件需要缓存。列表页中拦截beforeRouteLeave
钩子,判断to.name==='detail'
,则将from.meta.keepAlive
置为true,否则置为false
延伸3: keep-alive原理(结合源码) 附上部分源码
// src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // vue在初始化生命周期的时候,为组件实例建立父子关系时会根据 abstract 属性决定是否忽略某个组件,设为true,则当前组件虚拟dom不会渲染成真实dom
props: {
include: patternTypes, // 缓存白名单
exclude: patternTypes, // 缓存黑名单
max: [String, Number] // 最大缓存组件个数
},
created() {
this.cache = Object.create(null) // 缓存虚拟dom的对象
this.keys = [] // 缓存的虚拟dom的键集合
},
destroyed() {
for (const key in this.cache) {
// 删除所有的缓存
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
// 实时监听黑白名单的变动
this.$watch('include', val => {
pruneCache(this, name => matched(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
const slot = this.$slots.defalut
const vnode: VNode = getFirstComponentChild(slot) // 找到第一个子组件对象
const componentOptions : ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) { // 存在组件参数
// check pattern
const name: ?string = getComponentName(componentOptions) // 组件名
const { include, exclude } = this
if (// 条件匹配
// not included
(include && (!name || !matches(include, name)))||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
// 定义组件的缓存key
const key: ?string = vnode.key === null ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key
if (cache[key]) { // 已经缓存过该组件
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key) // 调整key排序
} else {
cache[key] = vnode //缓存组件对象
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
//超过缓存数限制,将第一个删除
pruneCacheEntry(cahce, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true //渲染和执行被包裹组件的钩子函数需要用到,当vnode.componentInstance和keepAlive同时为true时,不再进入$mount过程,那mounted之前的所有钩子函数(beforeCreate、created、mounted)都不再执行。
}
return vnode || (slot && slot[0])
}
}
。。。
-
判断当前组件是否要被缓存 获取 keep-alive 包裹的第一个子组件对象及其组件名,根据设置的 include/exclude(如果有)进行条件匹配,决定是否缓存。如果不匹配,则直接返回组件实例。
-
命中缓存则直接获取,同时更新key的位置 根据组件id和tag生成缓存 key,并在this.cache缓存对象中查找是否已缓存过该组件实例对象,如果存在,直接取出缓存值,把上一次的DOM插入到父元素中。并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键)
-
不命中缓存则设置进缓存,同时检查缓存的实例数量是否超过 max 在this.cache对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max的设置值,超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为0的那个key)
更多细节请看这篇博客,写的那叫一个👍🏻 keep-alive实现原理
30、vue如何兼容ie版本
用vue-cli脚手架搭建的项目打包后在chrome下正常显示,IE浏览器下显示空白,这是因为缺少babel-polyfill处理器的缘故。因为IE内核版本太低不支持ES6新语法
module.exports = {
entry: ["babel-polyfill", "./app/js"]
};
31、vue2中v-if和v-for的优先级,使用时应注意什么?
v-for优先级大于v-if。如果两者同时存在一个元素上,则v-for循环的每一项都会有一个v-if判断,影响性能。 正确的做法是:
- 如果想要v-if优先,则在v-for外层使用
<template v-if>
进行包裹 - 如果想要v-for优先,则应用计算属性计算过滤掉掉不需要展示的选项,再用v-for进行渲染。
32、vue2 中 object.defineProperty与vue3 Proxy的对比
- vue2 中 object.defineProperty需要对data中的每一个属性进行遍历,拦截它的get和set函数,如果是深层引用,还需要深层遍历,递归进行Observer的创建显然会花很多时间。对所有属性的变化进行监听,也需要消耗不小的内存。而vue3采用proxy,对整个data对象的上层建立了一层拦截,不需要遍历,所以性能更好。
- proxy支持13种拦截方式,比vue2更灵活。
- vue2中object.defineProperty具有一些缺陷,比如
- 无法监听新属性的添加和删除,vue2中解决方法是采用
this.$set
/Object.assign({},obj1,obj2)
/{...obj1,...obj2}
/this.$delete
- 无法监听数组下标的变化( Object.defineProperty 本身是可以监控到数组下标的变化的,只是在 Vue 的实现中,从性能/体验的性价比考虑,放弃了这个特性)。具体表现为使用下标修改数组成员(解决:
this.$set
),以及修改数组长度(解决:arr.splice
)
- 无法监听新属性的添加和删除,vue2中解决方法是采用
- proxy是es6的新特性,会有浏览器兼容问题。
延伸:vue2对数组方法的响应式
- vue2对数组的其中方法进行了重写,使它们具有响应性。分别是splice、sort、reverse、push、pop、shift、unshift。重写的方法是将 arrExtend 对象作为拦截器,继承原始数组 Array 的所有属性,这样就不会影响到原始数组,然后对这个响应的方法函数进行改写,也就是除了对数组方法名对应的原始方法进行调用,并将执行结果返回外,还会调用
ob.dep.notify()
,将当前数组的变化通知给订阅者,从而触发视图更新。
33、 vue如何对vuex中的数据做持久缓存?
应用场景:
- 购物车,把商品放在购物车里,没有提交给后台的情况下,放在前端储存
- token,授权登录后,前端保存token
- 一些不会经常改变的数据,如登录用户权限等
state: {
logindata:JSON.parse(localStorage.getItem('logindata')),
// 初始化vuex,刷新时获取localStorage
// state改变时,也要同步改变localStorage数据的值
}
34、v-model原理
用于元素,仅限input、textArea、select
<input v-model="data"/>
//相当于
<input v-bind:value="data" @input="e=>{data=e.target.value}" />
也可以用在组件上
//父组件
<component v-model="data"></component>
//子组件
<template>
<div>
<input :value="data" @input="handleInput"/>
...
</div>
</template>
export default{
...
props:{
value:String
},
// model: { prop: 'checked', event: 'test' },可以通过model选项自定义prop值和事件名称
methods:{
handleInput(e){
this.$emit('input',e.target.value)
}
},
}
35、vue中data的属性可以和methods方法名同名嘛?
不可以,因为它们本质都是挂在this下面的,同名会覆盖。如果使用eslint检查,则会提示错误
36、怎么给vue定义全局方法
- 在mian.js直接挂在Vue.prototype属性下面
Vue.prototype.changeData = function (){
alert('执行成功');
}
//组件中调用
this.changeData();//直接通过this运行函数
- 写一个模块文件,使用Vue.use注册插件
//base.js
exports.install = function (Vue, options) {
Vue.prototype.text1 = function (){
alert('执行成功1');
};
Vue.prototype.text2 = function (){
alert('执行成功2');
};
};
//main.js
import base from './base'//引用
Vue.use(base);//将全局函数当做插件来进行注册
//组件里调用
this.text1();
this.text2();
- mixin,Vue.mixin(mixins);
37、怎么解决vue动态设置img的src不生效的问题?
因为动态添加src被当做静态资源处理了,没有进行编译,所以用require加载
export default {
data() {
return {
logo:require("./../assets/images/logo.png"),
};
}
}
38、你知道vue2.0兼容ie哪个版本以上吗?
ie8以上
39、Vue.observable
Vue.observable是vue2.6发布一个新的API,可以让一个对象可响应。可以处理一些简单的跨组件共享数据状态的问题。也可以作为最小化的跨组件状态存储器,用于简单的场景。
40、computed,watch的属性,methods的方法用箭头函数定义结果会怎么样
this是undefined,要更改的属性会报TypeError错误, Cannot read property 'xxx' of undefined,因为箭头函数默绑定父级作用域的上下文,所以不会绑定vue实例,所以 this 指向全局作用域,在严格模式下this是undefined,在非严格模式下指向window
41、对vue错误处理机制的理解
vue有两种错误处理方法,全局errorHandler和组件内errorCaptured。
- errorCaptured可以捕获来自本组件和子组件抛出的错误,接收error,vm,info三个参数,可以返回false阻止错误继续向上抛出。
- errorHandler是全局方法,vue2.6以后可以捕捉v-on和promise链的错误,可用于统一错误处理和错误兜底。
Vue.prototype.errorHandler=function(err,vm,info){}
42、表单修饰符
vue表单修饰符有.lazy .trim .number。使用.lazy的input标签,value值不是input事件触发时改变,而是change事件,在input失焦后触发。
43、vue中定时器如何销毁
- 在beforeDestroy钩子函数中销毁
- $once这个程序化事件侦听器在定义完定时器之后的位置来清除
const timer=xxx
this.$once(hook:beforeDestroy,()=>{clearTimeout(timer)})
44、vue的is属性的用法
- 限制元素
由于vue模板的限制,要求vue模板是有效的html代码片段。由于dom里面的一些HTML元素对放在里面的一些元素有限制,所以导致有些组件没办法放在一些标签中。如
等标签中,所以is特性就出现了
<ul>
<li :is="myComponent"></li>
</ul>
//不能写成以下形式,因为下面的方式会将自定义组件当做无效的内容提升到外部,导致错误的渲染结果
<ul>
<myComponent/>
<ul>
注意:在.vue文件、<script type="text/x-template">
、字符串中这个限制是不存在的
- 结合compnent内置组件,当做动态组件使用
45、对slot的理解
slot插槽是对子组件的扩展,通过slot可以向组件内部指定位置传递内容。slot分为匿名插槽,具名插槽,作用域插槽。
//具名插槽
<template v-slot:first>你好</template>
//可以缩写成
<template #first>你好</template>
//子组件
<slot name="first"></slot>
//作用域插槽,子组件的数据可以传递到父组件的插槽中去使用
<Child>
<template v-slot:default="slotProps">
<h1>{{slotProps.item}}</h1>
</template>
</Child>
//子组件
<slot v-for="item in list" :item=item></slot>
46、vue中如何使用event对象
@click=func
,默认第一个参数就是event对象@click=func(params,$event)
,如果需要传递别的参数,需要使用$event获取event对象,并显式得传入func
47、对vue组件设计原则的理解
- 容错处理,极端场景要考虑到,不能传错了一个参数就整个就挂了
- 颗粒化,组件拆分
- 场景化,比如一个提示Toast,要考虑到sucess、error、warning等不同场景下的状态
- 默认值
- 可扩展,要预留什么功能要考虑到
- 规范化
- 详细的使用文档,历史变更说明
- 可配置
- 组件命名语义化
- 阶段性,不是一次性都要做到完美
48、对v-clock和v-pre的理解
- v-clock加在标签上,配合css使用,可以把未编译好的html隐藏,待生成实例,v-clock会被自动删除。
- v-pre用来阻止预编译,含有v-pre指令的标签内部的内容不会被编译,会原样输出
49、对vue中mixin的理解
mixin可以定义一些公共的变量和方法,用于对组件的扩展。但是mixin中的数据不是共享的,也就是每个组件中的mixin实例都是不一样的,不会相互影响。mixin混入同名函数,则会递归合并为数组,两个函数都会执行,只不过优先执行mixin中的同名函数,mixin混入同名变量,则在组件中的同名变量会覆盖mixin的同名变量
50、route和router有什么区别?
- route代表当前路由对象,可以获取name,path,params,query等
- router代表整个vue实例下的路由对象,包括路由的跳转方法和钩子函数
51、vue-router如何响应路由参数的变化
- watch监听$route对象
watch: { '$route.params' (to, from) { // 对路由变化作出响应... } } }
- 钩子函数beforeRouteUpdate
52、切换到新路由时,页面要滚动到顶部或保持原先的滚动位置怎么做呢
vue-router有提供一个方法scrollBehavior,它可以使切换到新路由时,使页面滚动到顶部,或者保持原先的滚动位置
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
if (savedPosition && to.meta.keepAlive) {
return savedPosition
}
else {
return { x: 0, y: 0 } }
}
})
异步滚动: 当页面数据需要请求加载有延迟的情况下,页面如果直接滚动,会出现滚动后,页面数据请求回来,DOM重新渲染,滚动失效的情况;
scrollBehavior (to, from, savedPosition) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ x: 0, y: 0 })
}, 500)
})
}
53、vue3与vue2的区别
- 响应式原理不同,vue2使用Object.defineProperty拦截数据,vue3使用proxy。展开对比...
- vue3提供组合式API,方便将逻辑更好的封装复用,vue2中逻辑在不同的生命周期里比较分散,封装的方式是采用mixin,这种方式容易出现命名冲突,数据来源不清晰等问题
- vue3通过tree-shaking的方式,优化核心库的体积,减少不必要的代码量,写法表现为例如声明周期钩子需要显式的import导入,再进行使用
- vue3在编译方面也做了很多优化
- Fragments(升级vetur插件): template中不需要唯一根节点,可以直接放文本或者同级标签
- 静态提升(hoistStatic),当使用 hoistStatic 时,所有静态的节点都被提升到 render 方法之外.只会在应用启动的时候被创建一次,之后使用只需要应用提取的静态节点,随着每次的渲染被不停的复用。Vue2.x : 无论元素是否参与更新,每次都会重新创建。
- diff方法优化。Vue2.x 中的虚拟dom是进行全量的对比。Vue3.0 中新增了静态标记(PatchFlag):在与上次虚拟结点进行对比的时候,值对比带有patch flag的节点,并且可以通过flag 的信息得知当前节点要对比的具体内容化。patch flag, 在动态标签末尾加上相应的标记,只能带 patchFlag 的节点才被认为是动态的元素,会被追踪属性的修改,能快速的找到动态节点,而不用逐个逐层遍历,提高了虚拟dom diff的性能。
- 事件侦听器缓存 cacheHandler, 避免每次触发都要重新生成全新的function去更新之前的函数。默认情况下onClick会被视为动态绑定,所以每次都会去追踪它的变化但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可。
54、什么是函数式组件
一个组件没有管理任何状态(没有data),也没有监听任何传递给它的状态(没有watch),也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional
,这意味它无状态 (没有响应式数据),也没有实例 (没有 this
上下文)。一个函数式组件就像这样:
Vue.component('my-component', {
functional: true,
// Props 是可选的
props: { // ... },
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
//createElement创建虚拟dom
// ...
}
})
因为函数式组件只是函数,所以渲染开销也低很多。
延伸:结合项目实践说说应用场景
- 根据自己的项目结合说一下