前言
哈喽大家好,此篇文章为【精简版】前端面试宝典(Vue/微信小程序)篇,精简前端各个模块的知识点,方便熟记。也欢迎大家提出建议~
Vue
一、Vue 基础
Vue 是一个构建数据驱动的渐进性框架,它的目标是通过 API 实现响应数据绑定和视图更新。
1. Vue 优缺点
优点:数据驱动视图;组件化开发;强大且丰富的 API;生态好,社区活跃
缺点:
- 由于底层基于 Object.defineProperty 实现响应式,而这个 api 本身不支持IE8 及以下浏览器
- CSR(client-side rendering,客户端渲染) 的先天不足,首屏性能问题(白屏)
- 由于百度等搜索引擎爬虫无法爬取 js 中的内容,故 SPA 先天就对 SEO 优化心有余力不足
2. Vue 的基本原理
当一个Vue实例创建时,Vue会遍历data中的属性,用 Object.defineProperty(Vue3.0使用proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的watcher程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
3. Vue 响应式原理
所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制。
Vue的核心是数据劫持和依赖收集,主要是利用 Object.defineProperty() 实现对数据存取操作的拦截,把这个实现称为数据代理;同时我们通过对数据get方法的拦截,可以获取到对数据的依赖,并将出所有的依赖收集到一个集合中。
我们通过Object.defineProperty为对象obj添加属性,可以设置对象属性的getter和setter函数。之后我们每次通过点语法获取属性都会执行这里的getter函数,在这个函数中我们会把调用此属性的依赖收集到一个集合中 ;而在我们给属性赋值(修改属性)时,会触发这里定义的setter函数,在次函数中会去通知集合中的依赖更新,做到数据变更驱动视图变更。
Vue2.x是借助Object.defineProperty()实现的,而Vue3.x是借助Proxy实现的
Proxy 与 Object.defineProperty 优劣对比
Proxy 的优势如下:
- 可以直接监听对象而非属性;
- 可以直接监听数组的变化;
- 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
- Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
- Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
Object.defineProperty 的优势如下:
- 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。
双向数据绑定
原理:Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
v-model
v-model用于表单数据的双向绑定,是语法糖,默认情况下相当于:value和@input。使用v-model可以减少大量繁琐的事件处理代码,提高开发效率。
(1) 作用在表单元素上 动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message设置为目标值
(2) 作用在组件上 在自定义组件中,v-model 默认会利用名为 value 的 prop和名为 input 的事件,本质是一个父子组件通信的语法糖,通过prop和$emit实现。在组件的实现中,可以通过 v-model属性来配置子组件接收的prop名称,以及派发的事件名称。
在自定义组件中,要实现数据绑定,还需要$emit去触发input的事件。this.$emit('input',val)
v-model 与 .sync修饰符的区别
相关资料:v-model与.sync修饰符的区别
相同点:都是语法糖,都可以实现父子组件中的数据通信。
不同点:v-model只能使用一次,.sync可以使用多个;
格式不同:
v-model="num" :num.sync="num"v-model:@input+value :num.sync:@update:num
MVVM、MVC、MVP的区别
MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。
MVC
MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构
思想:Controller 负责将 Model 的数据用 View 显示出来
MVVM(Model-View-ViewModel)
概念:视图模型双向绑定,是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。以前是操作DOM结构更新视图,现在是数据驱动视图。
注意:Vue 并没有完全遵循 MVVM 的思想
原因:严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了$refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。
优点:
- 分离视图和模型,降低代码耦合,提高视图或者逻辑的重用性
- 提高可测试性
- 独立开发,开发和设计人员可以更加专注自己的业务
- 自动更新dom,利用双向绑定,数据更新后视图自动更新,让开发者从繁琐的⼿动dom中解放
缺点:
- 数据绑定使得 Bug 很难被调试
- ⼀个大的模块中model也会很⼤,虽然使用方便,能保证数据的⼀致性,但是当长期持有,不释放内存,就很容易造成内存泄漏
- 数据双向绑定不利于代码重用。
MVP
MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller
为什么 data 是一个函数
Object是引用数据类型,如果不用function返回,每个组件的data都是内存的同一个地址,一个数据改变了其他也改变了,这就造成了数据污染。如果data是一个函数,每个实例的data都在闭包中,就不会各自影响了
Computed 和 Watch 的区别
区别:
-
computed 计算属性 : 基于它们的响应式依赖进行缓存,只有在相关的依赖发生改变时才会重新计算。
-
watch 侦听器 : 用于观察和监听页面上vue实例的变化,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作,
运用场景:
- 当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。
- 当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。
Computed 和 Methods 的区别
共同点:可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式是相同的
不同点:
- computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;
- method:调用总会执行该函数。
常见的事件修饰符及其作用
.stop:等同于 JavaScript 中的event.stopPropagation(),防止事件冒泡;.prevent:等同于 JavaScript 中的event.preventDefault(),防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);.capture:与事件冒泡的方向相反,事件捕获由外到内;.self:只会触发自己范围内的事件,不包含子元素;.once:只会触发一次。
常见指令
- v-bind:给元素绑定属性
- v-on:给元素绑定事件
- v-html:给元素绑定数据,且该指令可以解析 html 标签
- v-text:给元素绑定数据,不解析标签
- v-model:数据双向绑定
- v-for:遍历数组
- v-if:条件渲染指令,动态在 DOM 内添加或删除 DOM 元素
- v-else:条件渲染指令,必须跟 v-if 成对使用
- v-else-if:判断多层条件,必须跟 v-if 成对使用
- v-cloak:解决插值闪烁问题
- v-once:只渲染元素或组件一次
- v-pre:跳过这个元素以及子元素的编译过程,以此来加快整个项目的编译速度
- v-show:条件渲染指令,将不符合条件的数据隐藏(display:none)
v-if、v-show、v-html 的原理
- v-if:会调用addIfCondition方法,生成vnode的时候会忽略对应节点,render的时候就不会渲染;
- v-show:会生成vnode,render的时候也会渲染成真实节点,只是在render过程中会在节点的属性中修改show属性值,也就是display属性;
- v-html:会先移除节点下的所有节点,调用html方法,通过addProp添加innerHTML属性,归根结底还是设置innerHTML为v-html的值。
v-if 和 v-show 的区别
| 区别 | v-if | v-show |
|---|---|---|
| 手段 | 动态的向DOM树内添加或者删除DOM元素 | 通过设置DOM元素的display样式属性控制显隐 |
| 编译过程 | 惰性的,如果初始条件为假,则什么也不做,只有在条件第一次变为真时才开始局部编译 | 只是简单的基于css切换 |
| 编译条件 | 切换有局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件 | 在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留 |
| 性能消耗 | 有更高的切换消耗 | 有更高的初始渲染消耗 |
| 使用场景 | 适合判断条件不大可能改变 | 适合频繁切换 |
v-for 中 key 的重要性
key 涉及到 vue 的 diff 算法,在新旧nodes对比识别 VNodes。它的作用主要是为高效的更新虚拟 DOM。vue 会基于 key 的变化重新排列元素顺序,并且会移除可以不存在的元素。有相同父元素必须有独特的 key,重复的 key 会造成渲染错误。
Vue.extend 作用和原理
官方解释:Vue.extend使用基础Vue构造器,创建一个“子类”。参数就是一个包含组件选项的对象
其实就是一个子类构造器 是Vue的核心api,实现思路就是使用原型继承的方法返回了Vue的子类并且利用mergeOptions把传入组件的options和父类的option进行了合并
slot 插槽
slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。
分类:默认插槽,具名插槽和作用域插槽。
- 默认插槽:又名匿名查抄,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
- 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。
- 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
过滤器的作用,如何实现一个过滤器
根据过滤器的名称,过滤器是用来过滤数据的,在Vue中使用filters来过滤数据,filters不会修改数据,而是过滤数据,改变用户看到的输出(计算属性 computed ,方法 methods 都是通过修改数据来处理数据格式的输出显示)。
使用场景:
- 需要格式化数据的情况,比如需要处理时间、价格等数据格式的输出 / 显示。
- 比如后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此时就可以用
fliters过滤器来处理数据。
过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在插值表达式 {{ }} 和 v-bind 表达式 中,然后放在操作符“ | ”后面进行指示。
SPA 单页面
理解:SPA(SinglePage Web Application)仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
优点:
- 用户体验好,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
- 基于上面一点,SPA 相对对服务器压力小;
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
缺点:
- 初次加载耗时多
- 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
- SEO 难度较大
Vue 单页应用与多页应用的区别
MPA多页面应用(MultiPage Application):指有多个独立页面的应用,每个页面必须重复加载 js、css等相关资源。多页应用跳转,需要整页资源刷新。
| 对比项/模式 | SPA | MPA |
|---|---|---|
| 结构 | 一个主页面+许多模块的组件 | 许多完整的页面 |
| 用户体验 | 页面切换快,体验佳;当初次加载文件过多时,需要做相关的调优 | 页面切换慢,网速慢的时候,体验尤其不好 |
| 资源文件 | 组件公用的资源只需要加载一次 | 每个页面都要自己加载公用的资源 |
| 适用场景 | 对体验度和流畅度有较高要求的应用,不利于SEO(可借助SSR优化SEO) | 适用于对SEO要求较高的应用 |
| 过渡动画 | Vue提供了transition的封装组件 | 很难实现 |
| 内容更新 | 相关组件的切换,即局部更新 | 整体HTML的切换,费钱(重复HTTP请求) |
| 路由模式 | 可以使用hash/history | 普通链接跳转 |
| 数据传递 | 因为单页面,使用全局变量就好(Vuex) | cookie、localStorage等缓存方案,URL参数,调用接口保存等 |
| 相关成本 | 前期开发成本较高,后期维护较为容易 | 前期开发成本低,后期维护就比较麻烦,因为可能一个需要改很多地方 |
Vue.mixin
在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过 Vue 的 mixin 功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
- Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。
- 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。
- 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。
mixin 和 mixins 区别
mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。
虽然文档不建议在应用中直接使用 mixin,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax 或者一些工具函数等等。
mixins 应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。 另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。
delete和Vue.delete删除数组的区别
delete只是被删除的元素变成了empty/undefined其他的元素的键值还是不变。Vue.delete直接删除了数组 改变了数组的键值。
Vue 中使用了哪些设计模式
- 工厂模式 - 传入参数即可创建实例,虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
- 单例模式 - 整个程序有且仅有一个实例,vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
- 发布-订阅模式 (vue 事件机制)
- 观察者模式 (响应式数据原理)
- 装饰模式: (@装饰器的用法)
- 策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
SSR,服务端渲染
SSR(Server-Side Render) 就是将 Vue 在客户端把标签渲染成HTML的工作放在服务端完成,然后再把 html 直接返回给客户端。
优势:更好的SEO;首屏加载速度更快
缺点:
- 开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子;
- 当需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境;
- 更多的服务端负载
二、Vue生命周期
Vue 实例有⼀个完整的⽣命周期,也就是从开始实例化创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程。
作用:它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。
1. 钩子函数
create阶段:vue实例被创建
beforeCreate: 初始化事件和生命周期函数之后,初始化props、data、methods之前,此时组件的props、data、methods未被创建,在该函数中不可用,挂载元素$el也还没有created:初始化props、data、methods 之后,模板结构生成 之前。此时组件的props、data、methods已经创建好,可以调用。也可以发起异步请求,进行数据模型的复制操作,不能做页面的DOM操作
mount阶段: vue实例被挂载到真实DOM节点
beforeMount:将要把内存中编译好的HTML结构渲染到浏览器中,此时浏览器中还没有当前组件的DOM结构,没有$elmounted: 此时已经将内存中的HTML结构成功渲染到浏览器,浏览器中已经包含了当前组件的DOM结构,有$el
update阶段:当vue实例里面的data数据变化时,触发组件的重新渲染
beforeUpdate:在data数据发生改变的时候,会触发该函数,将要 变化后的最新数据重新渲染到组件的模板结构(还没渲染)updated:已经根据最新数据完成了DOM结构的重新渲染
destroy阶段:vue实例被销毁
beforeDestroy:将要销毁此组件,还未销毁,组件正常工作,此时可以手动销毁一些方法
destroyed:已经销毁,该组件在浏览器中对应的DOM结构已经完全移除
另外还有 keep-alive 独有的生命周期,分别为activated和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数。
第一次页面加载会触发哪几个钩子:第一次页面加载时会触发 beforeCreate, created, beforeMount, mounted 这几个钩子
适合场景:
- beforecreate : 可以在这加个loading事件,在加载实例时触发
- created : 初始化完成时的事件写在这里,如在这结束loading事件,异步请求也适宜在这里调用
- mounted : 挂载元素,获取到DOM节点
- updated : 如果对数据统一处理,在这里写上相应函数
- beforeDestroy : 可以做一个确认停止事件的确认框
一般在哪个生命周期请求异步数据
可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
推荐在 created 钩子函数,优点:
- 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
- SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。
keep-alive 的了解
keep-alive 是 Vue 提供的一个内置组件,用来对组件进行缓存,在组件切换过程中将状态保留在内存中,防止重复渲染DOM,提升项目的性能。
如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。
当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated钩子函数。
具体场景例子:首页进入到详情页,如果用户在首页每次点击都是相同的,那么详情页就没必要请求n次了,直接缓存起来就就可以了,当然如果点击的不是同一个,那么就直接请求
Vue 子组件和父组件执行顺序
加载渲染过程:父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
更新过程:父 beforeUpdate->子组件 beforeUpdate->子 updated->父 updated
销毁过程:父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destoryed
三、组件通信
分类:父子组件通信、隔代组件通信、兄弟组件通信
方式:
(1) props / $emit 适用 父子
- 父组件通过
props向子组件传递数据, - 子组件通过
$emit触发事件来向父组件发送数据
(2) ref / $refs 适用 父子
ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
(3) $parent / $children 适用 父子
- 使用
$parent可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法) - 使用
$children可以让组件访问子组件的实例,但是,$children并不能保证顺序,并且访问的数据也不是响应式的。
(4) 依赖注入(provide / inject)适用于 隔代
祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
注意:依赖注入所提供的属性是非响应式的。
(5) $attrs / $listeners 适用于 隔代
$attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过v-bind="$attrs"传入内部组件。通常配合 inheritAttrs 选项一起使用。$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过v-on="$listeners"传入内部组件
(6) EventBus事件总线 ($emit / $on) 适用于 父子、隔代、兄弟组件:通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信。缺点:如果项目过大,不方便后期维护
(7) Vuex 适用于 父子、隔代、兄弟组件通信
四、路由
1. Vue-Router 的懒加载实现
方案:
- 箭头函数+import动态加载
- 箭头函数+require动态加载
- webpack的require.ensure技术,也可以实现按需加载。这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
2. Vue-Router 的模式
三种模式:hash(默认)、history、abstract
(1) hash模式
简介: hash模式是开发中默认的模式,它的URL带着一个#,例如:www.abc.com/#/vue ,它的hash值就是#/vue。
特点:hash值会出现在URL里面,但是不会出现在HTTP请求中,对后端完全没有影响。所以改变hash值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的IE浏览器也支持这种模式。hash路由被称为是前端路由,已经成为SPA(单页面应用)的标配。
原理: hash模式的主要原理就是onhashchange()事件:
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
}
使用onhashchange()事件的好处就是,在页面的hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的hash值和对应的URL关联起来了。
(2) history模式
简介: history模式的URL中没有#,它使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。
实现原理:早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容
特点:当使用history模式时,URL就像这样:abc.com/user/id。 相比hash模式更加好看。但是,history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。
API: history api可以分为两大部分,切换历史状态和修改历史状态:
- 修改历史状态:包括了 HTML5 History Interface 中新增的
pushState()和replaceState()方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了url,但浏览器不会立即向后端发送请求。如果要做到改变url但又不刷新页面的效果,就需要前端用上这两个API。 - 切换历史状态:包括
forward()、back()、go()三个方法,对应浏览器的前进,后退,跳转操作。
缺点:虽然history模式丢弃了丑陋的#。但是,它也有自己的缺点,就是在刷新页面的时候,如果没有相应的路由或资源,就会刷出404来。
3. 两种模式对比
调用 history.pushState() 相比于直接修改 hash,存在以下优势:
- pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
- pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
- pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
- pushState() 可额外设置 title 属性供后续使用。
- hash模式下,仅hash符号之前的url会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回404错误;history模式下,前端的url必须和实际向后端发起请求的url一致,如果没有对用的路由处理,将返回404错误。
hash模式和history模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。
如何获取页面的hash变化
(1)监听$route的变化
// 监听,当路由发生变化的时候执行
watch: {
$route: {
handler: function(val, oldVal){
console.log(val);
},
// 深度观察监听
deep: true
}
},
(2)window.location.hash读取#值 window.location.hash 的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。
$route 和$router 的区别
- $route:是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数
- $router:是“路由实例”对象包括了路由的跳转方法,钩子函数等。
如何定义动态路由?如何获取传过来的动态参数?
(1)param方式
- 配置路由格式:
/router/:id - 传递的方式:在path后面跟上对应的值
- 传递后形成的路径:
/router/123
1)路由定义
//在APP.vue中
<router-link :to="'/user/'+userId" replace>用户</router-link>
//在index.js
{
path: '/user/:userid',
component: User,
},
2)路由跳转
// 方法1:
<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link
// 方法2:
this.$router.push({name:'users',params:{uname:wade}})
// 方法3:
this.$router.push('/user/' + wade)
3)参数获取 通过 $route.params.userid 获取传递的值
(2)query方式
- 配置路由格式:
/router,也就是普通配置 - 传递的方式:对象中使用query的key作为传递方式
- 传递后形成的路径:
/route?id=123
1)路由定义
//方式1:直接在router-link 标签上以对象的形式
<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
// 方式2:写成按钮以点击事件形式
<button @click='profileClick'>我的</button>
profileClick(){
this.$router.push({
path: "/profile",
query: {
name: "kobi",
age: "28",
height: 198
}
});
}
2)跳转方法
// 方法1:
<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
// 方法2:
this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:
<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
// 方法4:
this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:
this.$router.push('/user?uname=' + jsmes)
3)获取参数
通过$route.query 获取传递的值
Vue-Router 的懒加载如何实现
vue-router 路由模式有几种?
vue-router 有 3 种路由模式:hash、history、abstract,对应的源码如下所示:
其中,3 种路由模式的说明如下:
- hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;
- history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;
- abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.
vue-router 中常用的 hash 和 history 路由模式实现原理吗?
(1)hash 模式的实现原理
早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search':
hash 路由模式的实现主要是基于下面几个特性:
- URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;
- hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;
- 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
- 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。
(2)history 模式的实现原理
HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:
history 路由模式的实现主要基于存在下面几个特性:
- pushState 和 repalceState 两个 API 来操作实现 URL 的变化 ;
- 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
- history.pushState() 或 history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。 一、Vue-Router导航守卫
有的时候,需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。 为此有很多种方法可以植入路由的导航过程:全局的,单个路由独享的,或者组件级的
Vue-router 路由钩子在生命周期的体现
Vue-Router导航守卫
(1) 全局路由钩子
- router.beforeEach 全局前置守卫 进入路由之前,可以用来判断是否登录了,没登录就跳转到登录页
- router.beforeResolve 全局解析守卫(2.5.0+)在 beforeRouteEnter 调用之后调用
- router.afterEach 全局后置钩子 进入路由之后,可以用来跳转之后滚动条回到顶部
(2) 单个路由独享钩子
- beforeEnter:如果不想全局配置守卫的话,可以为某些路由单独配置守卫,有三个参数∶ to、from、next
(3) 组件内钩子
下面三个钩子都有三个参数:to、from、next
- beforeRouteEnter:进入组件前触发
- beforeRouteUpdate:当前地址改变并且改组件被复用时触发
- beforeRouteLeave:离开组件被调用
注意:beforeRouteEnter组件内还访问不到this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next来访问。
to,from,next 这三个参数:
to 和 from 是将要进入和将要离开的路由对象,路由对象指的是平时通过this.$route获取到的路由对象。
next:Function 这个参数是个函数,且必须调用,否则不能进入路由(页面空白)。
- next() 进入该路由。
- next(false): 取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)。
- next 跳转新路由,当前的导航被中断,重新开始一个新的导航。
Vue路由钩子在生命周期函数的体现
完整的路由导航解析流程(不包括其他生命周期):
- 触发进入其他路由。
- 调用要离开路由的组件守卫beforeRouteLeave
- 调用局前置守卫∶ beforeEach
- 在重用的组件里调用 beforeRouteUpdate
- 调用路由独享守卫 beforeEnter。
- 解析异步路由组件。
- 在将要进入的路由组件中调用 beforeRouteEnter
- 调用全局解析守卫 beforeResolve
- 导航被确认。
- 调用全局后置钩子的 afterEach 钩子。
- 触发DOM更新(mounted)。
- 执行beforeRouteEnter 守卫中传给 next 的回调函数
触发钩子的完整顺序:
路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件∶
- beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
- beforeEach:路由全局前置守卫,可用于登录验证、全局路由loading等。
- beforeEnter:路由独享守卫
- beforeRouteEnter:路由组件的组件进入路由前钩子。
- beforeResolve:路由全局解析守卫
- afterEach:路由全局后置钩子
- beforeCreate:组件生命周期,不能访问tAis。
- created;组件生命周期,可以访问tAis,不能访问dom。
- beforeMount:组件生命周期
- deactivated:离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子。
- mounted:访问/操作dom。
- activated:进入缓存组件,进入a的嵌套子组件(如果有的话)。
- 执行beforeRouteEnter回调函数next。
导航行为被触发到导航完成的整个过程
- 导航行为被触发,此时导航未被确认。
- 在失活的组件里调用离开守卫 beforeRouteLeave。
- 调用全局的 beforeEach守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
- 在路由配置里调用 beforeEnteY。
- 解析异步路由组件(如果有)。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫(2.5+),标示解析阶段完成。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 非重用组件,开始组件实例的生命周期:beforeCreate&created、beforeMount&mounted
- 触发 DOM 更新。
- 用创建好的实例调用 beforeRouteEnter守卫中传给 next 的回调函数。
- 导航完成
Vue-router 和 location.href区别
- 使用
location.href= /url来跳转,简单方便,但是刷新了页面; - 使用
history.pushState( /url ),无刷新页面,静态跳转; - 引进 router ,然后使用
router.push( /url )来跳转,使用了diff算法,实现了按需加载,减少了 dom 的消耗。其实使用 router 跳转和使用history.pushState()没什么差别的,因为vue-router就是用了history.pushState(),尤其是在history模式下。
params 和 query 的区别
用法:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是 this.$route.query.name 和 this.$route.params.name 。
url地址显示:query更加类似于ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示
注意:query刷新不会丢失query里面的数据 params刷新会丢失 params里面的数据。
五、Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。用于多个组件中数据共享、数据缓存等。
- 无法持久化、内部核心原理是通过创造一个全局实例 new Vue
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
Vuex 五大属性
- State:vuex的基本数据,数据源存放地,用于定义共享的数据
- Getter:从基本数据派生的数据,相当于state的计算属性。
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
- Action:像一个装饰器,用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
Vuex 运行机制
Vuex提供数据(state)来驱动试图(vue components),通过dispatch派发,在其中可以做一些异步的操作,然后通过commit来提交mutations,最后mutation来更改state。
Vuex 页面刷新数据丢失
解决方案:需要做 vuex 数据持久化,一般使用本地存储的方案来保存数据 可以自己设计存储方案 也可以使用第三方插件。推荐使用 vuex-persist 插件,它就是为 Vuex 持久化存储而生的一个插件。不需要你手动存取 storage ,而是直接将状态保存至 cookie 或者 localStorage 中
Vuex 和 localStorage 的区别
(1)最重要的区别
- vuex存储在内存中
- localstorage 则以文件的方式存储在本地,只能存储字符串类型的数据,存储对象需要 JSON的stringify和parse方法进行处理。 读取内存比读取硬盘速度要快
(2)应用场景
- Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。vuex用于组件之间的传值。
- localstorage是本地存储,是将数据存储到浏览器的方法,一般是在跨页面传递数据时使用 。
- Vuex能做到数据的响应式,localstorage不能
(3)永久性
刷新页面时vuex存储的值会丢失,localstorage不会。
注意: 对于不变的数据确实可以用localstorage可以代替vuex,但是当两个组件共用一个数据源(对象或数组)时,如果其中一个组件改变了该数据源,希望另一个组件响应该变化时,localstorage无法做到,原因就是区别1。
Vuex 和 Redux 区别
(1)Redux 和 Vuex区别
- Vuex改进了Redux中的Action和Reducer函数,以mutations变化函数取代Reducer,无需switch,只需在对应的mutation函数里改变state值即可
- Vuex由于Vue自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的State即可
- Vuex数据流的顺序是∶View调用store.commit提交对应的请求到Store中对应的mutation函数->store改变(vue检测到数据变化自动渲染)
通俗点理解就是,vuex 弱化 dispatch,通过commit进行 store状态的一次更变;取消了action概念,不必传入特定的 action形式进行指定变更;弱化reducer,基于commit参数直接对数据进行转变,使得框架更加简易;
(2)共同思想
- 单—的数据源
- 变化可以预测
本质上:redux与vuex都是对mvvm思想的服务,将数据从视图中抽离的一种方案; 形式上:vuex借鉴了redux,将store作为全局的数据中心,进行mode管理;
六、虚拟DOM
Virtual DOM 是 DOM 节点在 JavaScript 中的一种抽象数据结构,之所以需要虚拟 DOM,是因为浏览器中操作 DOM 的代价比较昂贵,频繁操作 DOM 会产生性能问题。
作用:在每一次响应式数据发生变化引起页面重渲染时,Vue 对比更新前后的虚拟 DOM,匹配找出尽可能少的需要更新的真实 DOM,从而达到提升性能的目的
实现原理:
- 用
JavaScript对象模拟真实DOM树,对真实DOM进行抽象; diff算法 — 比较两棵虚拟 DOM 树的差异;pach算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。
-
通过引入vdom我们可以获得如下好处:
将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能
- 直接操作 dom 是有限制的,比如:diff、clone 等操作,一个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff 一些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了。
- 操作 dom 是比较昂贵的操作,频繁的dom操作容易引起页面的重绘和回流,但是通过抽象 VNode 进行中间处理,可以有效减少直接操作dom的次数,从而减少页面重绘和回流。
方便实现跨平台
- 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
- Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染。
-
vdom如何生成?在vue中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom。
-
挂载过程结束后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图。
优点:
- 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
- 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
- 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
缺点:
- 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
Vue中的diff算法称为patching算法,它由Snabbdom修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换。
pathching算法:通过对比新旧VNode的不同,然后找出需要更新的节点进行更新 相关资料:www.jianshu.com/p/7cf284f31… juejin.cn/post/685457…
必要性:最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOM和patching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新。
何时执行:vue中diff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作。
patch过程是一个递归过程,遵循深度优先、同层比较的策略;vue3中引入的更新策略:编译期优化patchFlags、block等
Vue中key的作用吗?
分析:
这是一道特别常见的问题,主要考查大家对虚拟DOM和patch细节的掌握程度,能够反映面试者理解层次。
思路分析:
- 给出结论,key的作用是用于优化patch性能
- key的必要性
- 实际使用方式
- 总结:可从源码层面描述一下vue如何判断两个节点是否相同
回答范例:
- key的作用主要是为了更高效的更新虚拟DOM。
- vue在patch过程中判断两个节点是否是相同节点是key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能。
- 实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bug;vue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。
- 从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key和元素类型等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的。
nextTick的使用和原理?
nextTick是等待下一次 DOM 更新刷新的工具方法。
为什么需要它:Vue有个异步更新策略,意思是如果数据变化,Vue不会立刻更新DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick。
开发时何时使用它:开发时,有两个场景我们会用到nextTick:
- created中想要获取DOM时;
- 响应式数据变化后获取DOM更新后的状态,比如希望获取列表更新后的高度。
如何使用nextTick:nextTick签名如下:function nextTick(callback?: () => void): Promise<void>, 所以我们只需要在传入的回调函数中访问最新DOM状态即可,或者我们可以await nextTick()方法返回的Promise之后做这件事。
原理解读,结合异步更新和nextTick生效方式:在Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback自然能够获取到最新的DOM值。
从 template 到 render 处理过程
vue编译器概念:Vue中有个独特的编译器模块,称为“compiler”
作用是将用户编写的template编译为js中可执行的render函数。
编译器的必要性:之所以需要这个编译过程是为了便于前端程序员能高效的编写视图模板。相比而言,我们还是更愿意用HTML来编写视图,直观且高效。手写render函数不仅效率底下,而且失去了编译期的优化能力
编译器工作流程:在Vue中编译器会先对template进行解析,这一步称为parse,结束之后会得到一个JS对象,我们成为抽象语法树AST,然后是对AST进行深加工的转换过程,这一步成为transform,最后将前面得到的AST生成为JS代码,也就是render函数。
Vue首屏加载白屏问题
解决方法:
- 预渲染
- 同构
- SSR
- 路由懒加载
- quicklink
- 使用Gzip压缩,减少文件体积,加快首屏页面打开速度
- 外链CSS,JS文件
- webpack entry
- 骨架屏
- loading
相关资料:Vue首屏加载白屏问题及解决方案
Vue 3.0
相关资料:【专题版】前端面试宝典(Vue3.0)
api层面Vue3新特性包括:Composition API、SFC Composition API语法糖、Teleport传送门、Fragments 片段、Emits选项、自定义渲染器、SFC CSS变量、Suspense
相比于Vue2.0的新特性:
(1) 监测机制的改变
- 3.0 将带来基于代理 Proxy的 observer 实现,提供全语言覆盖的反应性跟踪。
- 消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:
(2) 只能监测属性,不能监测对象
- 检测属性的添加和删除;
- 检测数组索引和长度的变更;
- 支持 Map、Set、WeakMap 和 WeakSet。
(3) 模板
- 作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
- 同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。
(4) 对象式的组件声明方式
- vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。
- 3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易
(6) 其它方面的更改
- 支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
- 支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
- 基于 tree shaking 优化,提供了更多的内置功能。
Vue3.0新特性介绍:
- 重写双向数据绑定
- VDOM性能瓶颈
- Fragments
- Tree-Shaking支持
- Composition API