面试题
Vue相关
- vue双向数据绑定
是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图,实现数据和视图同步。
第一步: 需要observer的数据对象进行递归遍历,包括子属性对象的属性,都加上 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变更的双向绑定效果。
- vue虚拟dom,diff算法
想要理解虚拟dom首先要知道什么是虚拟dom?
虚拟dom可以简单的用一句话概括,就是用普通的js对象来描述DOM结构,因为不是真实DOM,所以称之为虚拟DOM。
虚拟dom是相对于浏览器所渲染出来的真实dom而言的,在react,vue等技术出现之前,我们要改变页面展示的内容,只能通过遍历查询dom树的方式,找到需要修改的dom,然后修改样式行为或者结构,来达到更新视图的目的。
为什么要用虚拟DOM来描述真实的DOM呢?
创建真实DOM成本比较高,如果用 js对象来描述一个dom节点,成本比较低,另外我们在频繁操作dom是一种比较大的开销。所以建议用虚拟dom来描述真实dom。
Diff算法
diff算法就是进行虚拟节点对比,并返回一个patch对象,用来存储两个节点不同的地方,最后用patch记录的消息去局部更新Dom。
思考
你觉得虚拟DOM的性能一定高于常规DOM吗?答案是不一定,虚拟DOM和Diff算法的出现是为了解决由命令式编程转变为函数式编程、数据驱动后所带来的性能问题的。换句话说,直接操作DOM的性能并不会低于虚拟DOM和Diff算法,甚至还会优于。
你觉得使用了虚拟DOM就真的不操作dom元素了吗?其实不是的,只是减少用户操作dom,虚拟DOM在渲染的时候其实还是会操作dom的
- 组件通讯
1.父传子:
在父组件的子组件标签上绑定一个属性,挂载要传输的变量。在子组件中通过props来接受数据,props可以是数组也可以是对象,接受的数据可以直接使用 props:["属性名"] props:{属性名:数据类型}
2.子传父:
在父组件的子组件标签上通过绑定自定义事件,接受子组件传递过来的事件。子组件通过$emit触发父组件上的自定义事件,发送参数
3.兄弟组件传值:
通过main.js初始化一个全局的$bus,在发送事件的一方通过$bus.$emit(“事件名”,传递的参数信息)发送,在接收事件的一方通过$bus.$on("事件名",参数)接收传递的事件
- Vuex
1.vuex :是一个专为vue.js开发的状态管理器,采用集中式存储的所有组件状态,通过vuex我们可以解决组件之间数据共享的问题,后期也方便我们管理以及维护
有五个属性分别是: state、getters、mutations、actions、module
state属性: 存放状态,例如你要存放的数据
getters: 类似于共享属性,可以通过this.$store.getters来获取存放在state里面的数据 a
mutations: 唯一能改变state的状态就是通过提交mutations来改变,this.$store.commit()
actions: 异步的mutations,可以通过dispatch来分发从而改变state
2.基本使用:我通过是在根目录下新建一个store文件夹,里面创建一个index.js文件,最后在main.js中引入,并挂载到实例上,之后那个组件中需要用到vuex就调用就行
3.高级用法-数据持久化
vuex里面存放的数据,页面一经刷新会丢失:
解决办法: 存放在localStorage或者sessionStorage里面,进入页面时判断是否丢失,丢失再去localStorage或者sessionStorage里面取;
在vuex中可以通过安装vuex-persistedstate 插件,进行持久化的配置就行
4.高级用法-辅助函数(语法糖)
1. 有那几个辅助函数(4大金刚)
mapState,mapActions,mapMutations,mapGetters
2. 辅助函数可以把vuex中的数据和方法映射到vue组件中。达到简化操作的目的
3. 如何使用:
Import { mapActions, mapGetters, mapMutations, mapState } from 'vuex'
- 自定义指令,自定义过滤器
vue中的自定义指令:
vue中除了核心功能内置的指令外,也允许注册自定义指令。有的情况下,对普通DOM元素进行底层操作,这时候就会用到自定义指令。自定义指令又分为全局的自定义指令和局部自定义指令。
全局自定义指令是通过Vue.directive('第一个参数是指令的名称',{第二个参数是一个对象,这个对象上有钩子函数})
Vue.directive('focus', {
// el:指令所绑定的元素,可以用来直接操作 DOM。
//binding:一个对象,包含以下 property:
inserted: function (el) { // inserted 表示被绑定元素插入父节点时调用
el.focus();
}
0
});
局部自定义指令:
是定义在组件内部的,只能在当前组件中使用
directives: {
// 指令名称
dir1: {
inserted(el) {
// 指令中第一个参数是当前使用指令的DOM
console.log(el);
console.log(arguments);
// 对DOM进行操作
el.style.width = '200px';
el.style.height = '200px';
el.style.background = '#000';
}
},
color: { // 为元素设置指定的字体颜色
bind(el, binding) {
el.style.color = binding.value;
}
}
}
钩子函数:
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。
项目中:拖拽
vue中自定义过滤器
- 过滤器是对即将显示的数据做进一步的筛选处理,然后显示,过滤器并没有改变原来的数据,只是在原数据的基础上产生新的数据
- 过滤器分为全局过滤器和局部过滤器
全局过滤器
全局过滤器是通过Vue.filter()来定义的,定义好后,它在所有组件中都可以使用。
// global-filter是过滤器名称
// 函数第一个参数是需要过滤的数据.
// 函数第二个参数是给过滤器传递的值.
Vue.filter('global-filter',(val,...args)=>{
console.log(`需要过滤的数据是:${val}`)
return val + ' 过滤器追加的数据'
})
局部过滤器
局部过滤器,定义在组件内部 filters 属性上.它只能在此组件内部使用.
过滤器的使用方式是,在双花括号或v-bind中通过一个管道符来拼接,
项目中使用过滤器:时间,价钱
- vue-router 瑞特(路由原理?路由守卫?传参)
路由原理
- 路由就是用来解析URL以及调用对应的控制器,并返回从视图对象中提取好的网页代码给web服务器,最终返回给客户端。
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 一致,如 地址后加上/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。
路由传参:
三种:
分别是query,params,动态路由传参
接收:
通过query方式传递过来的参数一般是通过this.$route.query接收
通过params方式传递过来的参数一般是通过this.$route.params接收
通过动态路由传参方式传递过来的参数一般是通过this.$route.params接收
query使用path和name传参跳转都可以,而params只能使用name传参跳转。
传参跳转页面时,query不需要再路由上配参数就能在新的页面获取到参数,params也可以不用配,但是params不在路由配参数的话,当用户刷新当前页面的时候,参数就会消失。
也就是说使用params不在路由配参数跳转,只有第一次进入页面参数有效,刷新页面参数就会消失。
路由守卫:
2.路由守卫使用的方式有几种? 全局的 单个路由独享的 组件级的
3.vue-router全局有三个守卫:
router.beforeEach 全局前置守卫 进入路由之前
router.beforeResolve 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用 router.afterEach 全局后置钩子 进入路由之后
组件内的守卫:
beforeRouteEnter
beforeRouteUpdata(2.2新增)
beforeRouteLeave
3. 路由守卫钩子函数里面的三个参数分别是什么?
to,from,next 这三个参数:
to和from是将要进入和将要离开的路由对象,路由对象指的是平时通过this.$route获取到的路由对象。
next:Function 这个参数是个函数,且必须调用,否则不能进入路由(页面空白)。
next() 进入该路由。
next(false): 取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)。 next 跳转新路由,当前的导航被中断,重新开始一个新的导航。
- 声明周期(那几个?每一个生命周期的特点,可以做什么)
生命周期让我们在控制整个vue时更容易形成更好的逻辑,可以分为三个阶段,挂载阶段,更新阶段,销毁阶段
分别有:
创建前:beforeCreate() 只有一些实例本身的事件和生命周期函数
创建后:Created() 是最早使用data和methods中数据的钩子函数
挂载前:beforeMount() 指令已经解析完毕,内存中已经生成dom树
挂载后:Mounted() dom渲染完毕页面和内存的数据已经同步
更新前:beforeUptate() 当data的数据发生改变会执行这个钩子,内存中的数据是新的,页面是旧的
更新后:Updated() 内存和页面都是新的
销毁前:beforeDestroy() 即将销毁data和methods中的数据此时还是可以使用的,可以做一些释放内存的操作
销毁后:Destroyed() 已经销毁完毕
Vue3.0中的生命周期做了一些改动:
beforeCreate -> setup() 开始创建组件之前,创建的是data和method
created -> setup()
beforeMount -> onBeforeMount 组件挂载到节点上之前执行的函数。
mounted -> onMounted 组件挂载完成后执行的函数
beforeUpdate -> onBeforeUpdate 组件更新之前执行的函数。
updated -> onUpdated 组件更新完成之后执行的函数。
beforeDestroy -> onBeforeUnmount 组件挂载到节点上之前执行的函数。
destroyed -> onUnmounted 组件卸载之前执行的函数。
- 自定义组件
在vue中开发,都是用的组件化的思想开发的,一般在都会在搭建项目的时候,都会先建立组件的模板,把架子搭起来。也就是在组件中定义好<template>视图层,<script>逻辑层,<style>css样式层。
在vue中使用组件封装的方式可以使我们的开发效率提高,能够把页面抽象成相对独立的模块。
我一般在创建项目的时候,都会通过创建一个views目录和一个commen目录和一个feature目录,views目录中放页面级的组件,commen中放公共组件(如:head(公共头组件),foot(公共底部组件)等),feature目录内放功能组件(如:swiper(轮播功能组件),tabbar(切换功能组件)、list(上拉加载更多功能组件))。组件封装是通过定义一个组件,然后定义好props里面的数据,实现其他组件需要的逻辑化代码后,也就是封装好了,然后直接调用就可以。调用是通过在需要封装组件的组件中,通过import导入,component注册好名称,最后挂载到父组件中的template即可。
组件封装的方式解决了我们传统项目,开发效率低,难以维护,复用性低等问题。
- 常见的指令,修饰符
常用指令
在vue中提供了一些对于页面 + 数据的更为方便的输出,这些操作就叫做指令,指令中封装了一些DOM行为, 结合属性作为一个暗号, 暗号有对应的值,根据不同的值,框架会进行相关DOM操作的绑定
vue中的指令有很多,我们平时做项目常用的有:
v-if:是动态的向DOM树中添加或者删除元素;
v-else是搭配v-if使用的,它必须紧跟在v-if或者v-else-if后面,否则不起作用
v-show:是通过标签的CSS样式display的值是不是none,控制显示隐藏
区别:
1、当条件为真的时候 没有区别 当条件为假的时候 v-if通过创建或删除DOM节点来实现元素的显示隐藏,v-show通过css中的display属性来控制
2、v-if更适合数据的筛选和初始渲染 v-show更适合元素的切换
v-for: v-for是根据遍历数据来进行渲染,要配合key使用。要注意的是当v-for和v-if同处于一个节点时,v-for的优先级比v-if更高。这意味着v-if将运行在每个v-for循环中
v-on:用来绑定一个事件或者方法,简写方式是@click=""
v-bind: v-bind用来动态的绑定一个或者多个属性。没有参数时,可以绑定到一个包含键值的对象。常用于动态绑定class和style。以及href等。简写的方式是“:属性名=""”一个冒号
v-model 只能适用于在表单元素上,可以实现数据双向绑定 ,
数据双向绑定实现的原理:
vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。
主要分为四步:
第一步: 需要Observer的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter,这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。
第二步: 通过Compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
第三步: Watcher订阅者是Observer和Compile之间通信的桥梁。
第四步: MVVM作为数据绑定的入口,整合Observer、Compile、Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化->视图更新;视图交互变化(input)->数据model变更的双向绑定效果
修饰符:
在Vue中,事件修饰符处理了许多DOM事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理。在Vue中事件修饰符常用的主要有:
- .stop :阻止事件冒泡:由内而外,通俗的将就是阻止事件将向上级DOM元素传递
- .capture :事件捕获:由外而内,在捕获阶段,事件从window开始,之后是document对象,一直到触发事件的元素。
- .self :当事件作用在元素本身时才会触发
- .once :只触发一次
- .prevent: 阻止默认事件
- ·passive:告诉浏览器你不想阻止事件的默认行为
- ·trim:自动过滤用户输入的首尾空格
语法:@事件名.修饰符=“方法名”
- Vue2和vue3的区别
- 双向数据绑定原理发生了改变,使用proxy替换Object.defineProerty,使用Proxy的优势:
- 可直接监听数组类型的数据变化
- 监听的目标为对象本身,不需要像Object.defineProperty一样遍历每个属性,有一定的性能提升
- 可直接实现对象属性的新增/删除
- 默认使用懒加载
在2.x版本里。不管数据多大,都会在一开始就为其创建观察者,在数据很大时,就会造成性能的问题。在3.x中,只会对渲染出来的数据创建观察者,而且3.x的观察者更高效。
- 3.0新加入了TypeScript以及PWA支持
- 重构Virtual DOM
- 模板编译时的优化,将一些静态节点编译成常量
- Slot优化,将slot编译为lazy函数,将slot的渲染的决定权交给子组织
- 生命周期有了一定的区别
Vue2--------------vue3
beforeCreate -> setup() 开始创建组件之前,创建的是data和method
created -> setup()
beforeMount -> onBeforeMount 组件挂载到节点上之前执行的函数。
mounted -> onMounted 组件挂载完成后执行的函数
beforeUpdate -> onBeforeUpdate 组件更新之前执行的函数。
updated -> onUpdated 组件更新完成之后执行的函数。
beforeDestroy -> onBeforeUnmount 组件挂载到节点上之前执行的函数。
destroyed -> onUnmounted 组件卸载之前执行的函数。
activated -> onActivated 组件卸载完成后执行的函数
deactivated -> onDeactivated
- Keep-alive
keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。keep-alive 是一个抽象组件:它自身不会渲染成一个 DOM 元素,也不会出现在父组件链中。
在组件切换过程中 把切换出去的组件保留在内存中,防止重复渲染DOM,减少加载时间及性能消耗,提高用户体验性
被包含在 keep-alive 中创建的组件,会多出两个生命周期的钩子: activated(组件激活时使用) 与 deactivated(组价离开时调用)
如果需要缓存整个项目,直接在app.vue中用keep-alive包裹router-view即可。要缓存部分页面,需要在路由地址配置中,在meta属性中添加一个状态,在app.vue中判断一下包裹的router-view即可
例如有一个商品页面和一个详情页面,这样在两个页面切换的时候就可以用到keep-alive,在切换到详情的时候,把状态保留在内存中,而不是销毁,从而提高一个性能的优化
- 多环境变量
首先是通过在根目录下创建.env.*(配置文件)文件,development 本地开发环境配置、staging 测试环境配置、production 正式环境配置。因为我在创建的文件中并没有定义很多变量,只定义了基础的env,所以需要在src目录下创建一个config文件夹,创建对应的环境变量文件,用来管理不同的环境。在config中创建对应的文件是为了后期修改起来方便,不需要重启项目,符合开发习惯。之后就是根据需要的环境,在封装的axios中通过解构赋值的方式导入,放在baseURL中就可以使用。
- 对axios封装(url统一管理、axios请求拦截、响应拦截、函数封装)
首先要安装axios,一般我会在项目的src目录中,新建一个network文件夹,作为我们的网络请求模块,然后在里面新建一个http.js和一个api.js文件和一个reques.js。http.js文件用来封装我们的axios,api.js用来统一管理我们的接口url,
在request.js中添加请求拦截和响应拦截。在请求拦截中,会给请求头添加token字段,还有loading动画的开启。在响应拦截中,可以做一些loading动画的关闭,还有可以根据后端返回的状态码,做一些检验token是否有效或者过期的操作。接着就是做一些axios进行的api接口的封装,这里我用到了async,await封装请求接口函数,这样可以将异步操作同步化操作,代码更加友好,避免回调地域的出现。
- Element-ui和vant-ui按需引入
首先安装按需引入的插件,在babel.config.js中添加按需引入的配置,创建一个plugin文件夹,定义一个js文件用来存放按需引入的代码,之后在建好的js文件中首先导入vue,再导入需要的vant-ui插件,通过vue.use()全局注入。
修改样式可以用样式穿透 /deep/
- Sass配置
安装node-sass sass-loader
使用lang=”scss”
- Rem、vm/vh设置
- 通过安装cnpm install lib-flexible postcss-pxtorem --save-dev
- Main.js 中导入插件
- 根目录创建.postcssrc.js配置
- Webpack配置(配置跨域、路径别名、打包分析、cdn映入、去掉console.log、单独打包第三方模块、ie兼容、eslint规范、图片压缩)
- Slot插槽
插槽就是父组件往子组件中插入一些内容。
有三种方式,默认插槽,具名插槽,作用域插槽
- 默认插槽就是把父组件中的数据,显示在子组件中,子组件通过一个slot插槽标签显示父组件中的数据
- 具名插槽是在父组件中通过slot属性,给插槽命名,在子组件中通过slot标签,根据定义好的名字填充到对应的位置。
- 作用域插槽是带数据的插槽,子组件提供给父组件的参数,父组件根据子组件传过来的插槽数据来进行不同的展现和填充内容。在标签中通过slot-scope来接受数据。
- 为什么v-for使用key
key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点
- 为什么data是一个函数
1.根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况